Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions base/script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,13 @@ contract DeployScript is DevOps {
}

function _deployBridgeValidator(HelperConfig.NetworkConfig memory cfg, address bridge) private returns (address) {
address bridgeValidatorImpl =
address(new BridgeValidator({partnerValidatorThreshold: cfg.partnerValidatorThreshold, bridge: bridge}));
address bridgeValidatorImpl = address(
new BridgeValidator({
partnerThreshold: cfg.partnerValidatorThreshold,
bridgeAddress: bridge,
partnerValidators: cfg.partnerValidators
})
);

return ERC1967Factory(cfg.erc1967Factory).deployAndCall({
implementation: bridgeValidatorImpl,
Expand Down
12 changes: 9 additions & 3 deletions base/script/HelperConfig.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {ERC1967FactoryConstants} from "solady/utils/ERC1967FactoryConstants.sol"
import {LibString} from "solady/utils/LibString.sol";

import {Pubkey} from "../src/libraries/SVMLib.sol";
import {MockPartnerValidators} from "../test/mocks/MockPartnerValidators.sol";

contract HelperConfig is Script {
string environment = vm.envOr("BRIDGE_ENVIRONMENT", string(""));
Expand All @@ -20,6 +21,7 @@ contract HelperConfig is Script {
uint128 baseSignatureThreshold;
address[] guardians;
uint256 partnerValidatorThreshold;
address partnerValidators;
}

NetworkConfig private _activeNetworkConfig;
Expand Down Expand Up @@ -63,7 +65,8 @@ contract HelperConfig is Script {
baseValidators: baseValidators,
baseSignatureThreshold: 1,
guardians: guardians,
partnerValidatorThreshold: 0
partnerValidatorThreshold: 0,
partnerValidators: address(0)
});
}

Expand All @@ -83,7 +86,8 @@ contract HelperConfig is Script {
baseValidators: baseValidators,
baseSignatureThreshold: 1,
guardians: guardians,
partnerValidatorThreshold: 0
partnerValidatorThreshold: 0,
partnerValidators: address(0)
});
}

Expand All @@ -93,6 +97,7 @@ contract HelperConfig is Script {
}

ERC1967Factory f = new ERC1967Factory();
MockPartnerValidators pv = new MockPartnerValidators();

address[] memory guardians = new address[](1);
address[] memory baseValidators = new address[](1);
Expand All @@ -106,7 +111,8 @@ contract HelperConfig is Script {
baseValidators: baseValidators,
baseSignatureThreshold: 1,
guardians: guardians,
partnerValidatorThreshold: 0
partnerValidatorThreshold: 0,
partnerValidators: address(pv)
});
}
}
6 changes: 5 additions & 1 deletion base/script/UpgradeScript.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,11 @@ contract UpgradeScript is DevOps {

function _upgradeBridgeValidator(HelperConfig.NetworkConfig memory cfg) internal {
address bridgeValidatorImpl = address(
new BridgeValidator({partnerValidatorThreshold: cfg.partnerValidatorThreshold, bridge: bridgeAddress})
new BridgeValidator({
partnerThreshold: cfg.partnerValidatorThreshold,
bridgeAddress: bridgeAddress,
partnerValidators: cfg.partnerValidators
})
);

console.log("Deployed new BridgeValidator implementation: %s", bridgeValidatorImpl);
Expand Down
1 change: 0 additions & 1 deletion base/script/actions/BridgeTokensToSolana.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
pragma solidity 0.8.28;

import {stdJson} from "forge-std/StdJson.sol";
import {console} from "forge-std/console.sol";
import {ERC20} from "solady/tokens/ERC20.sol";

import {Bridge} from "../../src/Bridge.sol";
Expand Down
2 changes: 1 addition & 1 deletion base/src/Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ contract Bridge is ReentrancyGuardTransient, Initializable, OwnableRoles {
address public immutable CROSS_CHAIN_ERC20_FACTORY;

/// @notice Address of the BridgeValidator contract. Messages will be pre-validated there by our oracle & bridge
/// partner.
/// partner.
address public immutable BRIDGE_VALIDATOR;

/// @notice Guardian Role to pause the bridge.
Expand Down
155 changes: 98 additions & 57 deletions base/src/BridgeValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {ECDSA} from "solady/utils/ECDSA.sol";
import {Initializable} from "solady/utils/Initializable.sol";

import {Bridge} from "./Bridge.sol";
import {IPartner} from "./interfaces/IPartner.sol";
import {VerificationLib} from "./libraries/VerificationLib.sol";

/// @title BridgeValidator
Expand All @@ -31,6 +32,9 @@ contract BridgeValidator is Initializable {
/// @notice Address of the Base Bridge contract. Used for authenticating guardian roles
address public immutable BRIDGE;

/// @notice Address of the contract holding the partner validator set
address public immutable PARTNER_VALIDATORS;

//////////////////////////////////////////////////////////////
/// Storage ///
//////////////////////////////////////////////////////////////
Expand All @@ -48,23 +52,22 @@ contract BridgeValidator is Initializable {

/// @notice Emitted when a single message is registered (pre-validated).
///
/// @param messageHashes The pre-validated message hash (derived from the inner message hash and an incremental
/// nonce) corresponding to an `IncomingMessage` in the `Bridge` contract.
event MessageRegistered(bytes32 indexed messageHashes);
/// @param messageHash The pre-validated message hash (derived from the inner message hash and an incremental
/// nonce) corresponding to an `IncomingMessage` in the `Bridge` contract.
event MessageRegistered(bytes32 indexed messageHash);

//////////////////////////////////////////////////////////////
/// Errors ///
//////////////////////////////////////////////////////////////

/// @notice Thrown when `validatorSigs` verification fails (Base and/or external signatures invalid or
/// insufficient).
error Unauthenticated();

/// @notice Thrown when the provided `validatorSigs` byte string length is not a multiple of 65
error InvalidSignatureLength();

/// @notice Thrown when the required amount of signatures is not included with a `registerMessages` call
error ThresholdNotMet();
/// @notice Thrown when the required amount of Base signatures is not included with a `registerMessages` call
error BaseThresholdNotMet();

/// @notice Thrown when the required amount of partner signatures is not included with a `registerMessages` call
error PartnerThresholdNotMet();

/// @notice Thrown when a zero address is detected
error ZeroAddress();
Expand All @@ -75,32 +78,45 @@ contract BridgeValidator is Initializable {
/// @notice Thrown when the caller of a protected function is not a Base Bridge guardian
error CallerNotGuardian();

/// @notice Thrown when a duplicate partner validator is detected during signature verification
error DuplicateSigner();

/// @notice Thrown when the recovered signers are not sorted
error UnsortedSigners();

//////////////////////////////////////////////////////////////
/// Modifiers ///
//////////////////////////////////////////////////////////////

/// @dev Restricts function to `Bridge` guardians (as defined by `GUARDIAN_ROLE`).
modifier isGuardian() {
require(OwnableRoles(BRIDGE).hasAnyRole(msg.sender, GUARDIAN_ROLE), CallerNotGuardian());
_;
}

//////////////////////////////////////////////////////////////
/// Public Functions ///
//////////////////////////////////////////////////////////////

/// @notice Deploys the BridgeValidator contract with configuration for partner signatures and the `Bridge` address.
///
/// @param partnerValidatorThreshold The number of partner (external) validator signatures required for
/// message pre-validation.
/// @param bridge The address of the `Bridge` contract used to check guardian roles.
///
/// @dev Reverts with `ThresholdTooHigh()` if `partnerValidatorThreshold` exceeds
/// @dev Reverts with `ThresholdTooHigh()` if `partnerThreshold` exceeds
/// `MAX_PARTNER_VALIDATOR_THRESHOLD`. Reverts with `ZeroAddress()` if `bridge` is the zero address.
constructor(uint256 partnerValidatorThreshold, address bridge) {
require(partnerValidatorThreshold <= MAX_PARTNER_VALIDATOR_THRESHOLD, ThresholdTooHigh());
require(bridge != address(0), ZeroAddress());
PARTNER_VALIDATOR_THRESHOLD = partnerValidatorThreshold;
BRIDGE = bridge;
///
/// @param partnerThreshold The number of partner (external) validator signatures required for message
/// pre-validation.
/// @param bridgeAddress The address of the `Bridge` contract used to check guardian roles.
/// @param partnerValidators Address of the contract holding the partner validator set
constructor(uint256 partnerThreshold, address bridgeAddress, address partnerValidators) {
require(partnerThreshold <= MAX_PARTNER_VALIDATOR_THRESHOLD, ThresholdTooHigh());
require(bridgeAddress != address(0), ZeroAddress());
require(partnerValidators != address(0), ZeroAddress());
PARTNER_VALIDATOR_THRESHOLD = partnerThreshold;
BRIDGE = bridgeAddress;
PARTNER_VALIDATORS = partnerValidators;
_disableInitializers();
}

/// @dev Restricts function to `Bridge` guardians (as defined by `GUARDIAN_ROLE`).
modifier isGuardian() {
require(OwnableRoles(BRIDGE).hasAnyRole(msg.sender, GUARDIAN_ROLE), CallerNotGuardian());
_;
}

/// @notice Initializes Base validator set and threshold.
///
/// @dev Callable only once due to `initializer` modifier.
Expand All @@ -115,7 +131,7 @@ contract BridgeValidator is Initializable {
///
/// @param innerMessageHashes An array of inner message hashes to pre-validate (hash over message data excluding the
/// nonce). Each is combined with a monotonically increasing nonce to form
/// `messageHashes`.
/// `messageHashes`.
/// @param validatorSigs A concatenated bytes array of signatures over the EIP-191 `eth_sign` digest of
/// `abi.encode(messageHashes)`, provided in strictly ascending order by signer address.
/// Must include at least `getBaseThreshold()` Base validator signatures. The external
Expand All @@ -129,7 +145,7 @@ contract BridgeValidator is Initializable {
messageHashes[i] = keccak256(abi.encode(currentNonce++, innerMessageHashes[i]));
}

require(_validatorSigsAreValid({messageHashes: messageHashes, sigData: validatorSigs}), Unauthenticated());
_validateSigs({messageHashes: messageHashes, sigData: validatorSigs});

for (uint256 i; i < len; i++) {
validMessages[messageHashes[i]] = true;
Expand Down Expand Up @@ -174,67 +190,92 @@ contract BridgeValidator is Initializable {
///
/// @param messageHashes The derived message hashes (inner hash + nonce) for the batch.
/// @param sigData Concatenated signatures over `toEthSignedMessageHash(abi.encode(messageHashes))`.
///
/// @return True if thresholds are met by valid signers with strictly ascending signer order.
function _validatorSigsAreValid(bytes32[] memory messageHashes, bytes calldata sigData)
function _validateSigs(bytes32[] memory messageHashes, bytes calldata sigData) private view {
address[] memory recoveredSigners = _getSignersFromSigs(messageHashes, sigData);
IPartner.Signer[] memory partnerValidators = IPartner(PARTNER_VALIDATORS).getSigners();
require(_countBaseSigners(recoveredSigners) >= VerificationLib.getBaseThreshold(), BaseThresholdNotMet());
require(
_countPartnerSigners(partnerValidators, recoveredSigners) >= PARTNER_VALIDATOR_THRESHOLD, PartnerThresholdNotMet()
);
}

function _getSignersFromSigs(bytes32[] memory messageHashes, bytes calldata sigData)
private
view
returns (bool)
returns (address[] memory)
{
// Check that the provided signature data is a multiple of the valid sig length
require(sigData.length % VerificationLib.SIGNATURE_LENGTH_THRESHOLD == 0, InvalidSignatureLength());

uint256 sigCount = sigData.length / VerificationLib.SIGNATURE_LENGTH_THRESHOLD;
// Placeholder for an external contract lookup to be introduced soon
address[] memory partnerValidators = new address[](0);
bytes32 signedHash = ECDSA.toEthSignedMessageHash(abi.encode(messageHashes));
address lastValidator = address(0);
address[] memory recoveredSigners = new address[](sigCount);

uint256 offset;
assembly {
offset := sigData.offset
}

uint256 baseSigners;
uint256 externalSigners;

for (uint256 i; i < sigCount; i++) {
(uint8 v, bytes32 r, bytes32 s) = VerificationLib.signatureSplit(offset, i);
address currentValidator = signedHash.recover(v, r, s);
require(currentValidator > lastValidator, UnsortedSigners());
recoveredSigners[i] = currentValidator;
lastValidator = currentValidator;
}

if (currentValidator <= lastValidator) {
return false;
}
return recoveredSigners;
}

// Verify signer is valid
if (VerificationLib.isBaseValidator(currentValidator)) {
unchecked {
baseSigners++;
}
} else if (_addressInList(partnerValidators, currentValidator)) {
function _countBaseSigners(address[] memory signers) private view returns (uint256) {
uint256 count;

for (uint256 i; i < signers.length; i++) {
if (VerificationLib.isBaseValidator(signers[i])) {
unchecked {
externalSigners++;
count++;
}
}

lastValidator = currentValidator;
}

require(baseSigners >= VerificationLib.getBaseThreshold(), ThresholdNotMet());
require(externalSigners >= PARTNER_VALIDATOR_THRESHOLD, ThresholdNotMet());
return count;
}

return true;
function _countPartnerSigners(IPartner.Signer[] memory partnerValidators, address[] memory signers)
private
pure
returns (uint256)
{
uint256 count;
uint256 signedBitMap;

for (uint256 i; i < signers.length; i++) {
uint256 partnerIndex = _indexOf(partnerValidators, signers[i]);
if (partnerIndex == partnerValidators.length) {
continue;
}

if (signedBitMap & (0x1 << partnerIndex) != 0) {
revert DuplicateSigner();
}

signedBitMap |= 0x1 << partnerIndex;
unchecked {
count++;
}
}

return count;
}

/// @dev Linear search for `addr` in memory array `addrs`.
///
/// @return True if found, false otherwise.
function _addressInList(address[] memory addrs, address addr) private pure returns (bool) {
function _indexOf(IPartner.Signer[] memory addrs, address addr) private pure returns (uint256) {
for (uint256 i; i < addrs.length; i++) {
if (addr == addrs[i]) {
return true;
if (addr == addrs[i].evmAddress || addr == addrs[i].newEVMAddress) {
return i;
}
}
return false;
return addrs.length;
}
}
4 changes: 2 additions & 2 deletions base/src/CrossChainERC20Factory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ contract CrossChainERC20Factory {
/// Public Functions ///
//////////////////////////////////////////////////////////////

/// @notice Constructs the CrossChainERC20Factory contract
/// @notice Stores the beacon address for proxies and reverts if the beacon is the zero address.
///
/// @dev Stores the beacon address for proxies and reverts if the beacon is the zero address.
/// @param beacon Address of the ERC-1967 beacon contract used by deployed proxies for CrossChainERC20.
constructor(address beacon) {
require(beacon != address(0), ZeroAddress());
BEACON = beacon;
Expand Down
2 changes: 1 addition & 1 deletion base/src/Twin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ contract Twin {
/// @notice Thrown when the caller is neither the Bridge nor the Twin itself.
error Unauthorized();

/// @notice Thrown when a zero address is detected
/// @notice Thrown when a zero address is detected.
error ZeroAddress();

//////////////////////////////////////////////////////////////
Expand Down
17 changes: 17 additions & 0 deletions base/src/interfaces/IPartner.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

/// @notice Expose a list of signers with key rotation support.
interface IPartner {
/// @notice Each signer entity always has [1, 2] keys.
struct Signer {
// Regular active EVM address of the signer.
address evmAddress;
// New candidate EVM address that each signer will sign with during key rotation.
// Upon completion of rotation, this will be promoted to evm_address.
address newEVMAddress;
}

/// @notice Returns the full signer set.
function getSigners() external view returns (Signer[] memory signers);
}
Loading