// Source: contracts/governance/InterchainMultisig.sol pragma solidity ^0.8.0; // SPDX-License-Identifier: MIT // File contracts/interfaces/IBaseWeightedMultisig.sol interface IBaseWeightedMultisig { error InvalidSigners(); error InvalidThreshold(); error MalformedSignatures(); error LowSignaturesWeight(); error InvalidWeights(); error DuplicateSigners(bytes32 signersHash); error RedundantSignaturesProvided(uint256 required, uint256 provided); error InsufficientRotationDelay(uint256 minimumRotationDelay, uint256 lastRotationTimestamp, uint256 timeElapsed); event SignersRotated(uint256 indexed epoch, bytes32 indexed signersHash, bytes signers); /** * @dev This function returns the old signers retention period * @return uint256 The old signers retention period */ function previousSignersRetention() external view returns (uint256); /** * @dev This function returns the current signers epoch * @return uint256 The current signers epoch */ function epoch() external view returns (uint256); /** * @dev Returns the hash for a given signers epoch * @param signerEpoch The epoch to get the hash for * @return The hash for the given epoch */ function signersHashByEpoch(uint256 signerEpoch) external view returns (bytes32); /** * @dev Returns the epoch for a given hash * @param signersHash The hash to get the epoch for * @return The epoch for the given hash */ function epochBySignersHash(bytes32 signersHash) external view returns (uint256); /** * @notice This function returns the timestamp for the last signer rotation * @return uint256 The last rotation timestamp */ function lastRotationTimestamp() external view returns (uint256); /** * @notice This function returns the time elapsed (in secs) since the last rotation * @return uint256 The time since the last rotation */ function timeSinceRotation() external view returns (uint256); /** * @notice Compute the message hash that is signed by the weighted signers * @param signersHash The hash of the weighted signers that sign off on the data * @param dataHash The hash of the data * @return The message hash to be signed */ function messageHashToSign(bytes32 signersHash, bytes32 dataHash) external view returns (bytes32); } // File contracts/libs/ECDSA.sol /** * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. * * These functions can be used to verify that a message was signed by the holder * of the private keys of a given address. */ library ECDSA { error InvalidSignatureLength(); error InvalidS(); error InvalidV(); error InvalidSignature(); /** * @dev Returns the address that signed a hashed message (`hash`) with * `signature`. This address can then be used for verification purposes. * * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: * this function rejects them by requiring the `s` value to be in the lower * half order, and the `v` value to be either 27 or 28. * * IMPORTANT: `hash` _must_ be the result of a hash operation for the * verification to be secure: it is possible to craft signatures that * recover to arbitrary addresses for non-hashed data. A safe way to ensure * this is by receiving a hash of the original message (which may otherwise * be too long), and then calling {toEthSignedMessageHash} on it. */ function recover(bytes32 hash, bytes memory signature) internal pure returns (address signer) { // Check the signature length if (signature.length != 65) revert InvalidSignatureLength(); // Divide the signature in r, s and v variables bytes32 r; bytes32 s; uint8 v; // ecrecover takes the signature parameters, and the only way to get them // currently is to use assembly. // solhint-disable-next-line no-inline-assembly assembly { r := mload(add(signature, 0x20)) s := mload(add(signature, 0x40)) v := byte(0, mload(add(signature, 0x60))) } // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most // signatures from current libraries generate a unique signature with an s-value in the lower half order. // // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept // these malleable signatures as well. if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) revert InvalidS(); if (v != 27 && v != 28) revert InvalidV(); signer = ecrecover(hash, v, r, s); // If the signature is valid (and not malleable), return the signer address if (signer == address(0)) revert InvalidSignature(); } } // File contracts/types/WeightedMultisigTypes.sol /** * @notice This struct represents the weighted signer * @param signer The address of the weighted signer * @param weight The weight of the weighted singer */ struct WeightedSigner { address signer; uint128 weight; } /** * @notice This struct represents the weighted signers payload * @param signers The list of weighted signers * @param threshold The threshold for the weighted signers * @param nonce The nonce to distinguish different weighted signer sets */ struct WeightedSigners { WeightedSigner[] signers; uint128 threshold; bytes32 nonce; } /** * @notice This struct represents a proof for a message from the weighted signers * @param signers The weighted signers * @param signatures The list of signatures */ struct Proof { WeightedSigners signers; bytes[] signatures; } // File contracts/governance/BaseWeightedMultisig.sol /** @title BaseWeightedMultisig Contract @notice Base contract to build a weighted multisig verification */ abstract contract BaseWeightedMultisig is IBaseWeightedMultisig { // keccak256('BaseWeightedMultisig.Slot') - 1; bytes32 internal constant BASE_WEIGHTED_MULTISIG_SLOT = 0x457f3fc26bf430b020fe76358b1bfaba57e1657ace718da6437cda9934eabfe8; struct BaseWeightedMultisigStorage { uint256 epoch; uint256 lastRotationTimestamp; mapping(uint256 => bytes32) signersHashByEpoch; mapping(bytes32 => uint256) epochBySignersHash; } /// @dev Previous signers retention. 0 means only the current signers are valid uint256 public immutable previousSignersRetention; /// @dev The domain separator for the signer proof bytes32 public immutable domainSeparator; /// @dev The minimum delay required between rotations uint256 public immutable minimumRotationDelay; /** * @dev Initializes the contract. * @dev Ownership of this contract should be transferred to the Gateway contract after deployment. * @param previousSignersRetention_ The number of previous signers to retain * @param domainSeparator_ The domain separator for the signer proof * @param minimumRotationDelay_ The minimum delay required between rotations */ constructor( uint256 previousSignersRetention_, bytes32 domainSeparator_, uint256 minimumRotationDelay_ ) { previousSignersRetention = previousSignersRetention_; domainSeparator = domainSeparator_; minimumRotationDelay = minimumRotationDelay_; } /**********************\ |* External Functions *| \**********************/ /** * @notice This function returns the current signers epoch * @return uint256 The current signers epoch */ function epoch() external view returns (uint256) { return _baseWeightedMultisigStorage().epoch; } /** * @notice This function returns the signers hash for a given epoch * @param signerEpoch The given epoch * @return bytes32 The signers hash for the given epoch */ function signersHashByEpoch(uint256 signerEpoch) external view returns (bytes32) { return _baseWeightedMultisigStorage().signersHashByEpoch[signerEpoch]; } /** * @notice This function returns the epoch for a given signers hash * @param signersHash The signers hash * @return uint256 The epoch for the given signers hash */ function epochBySignersHash(bytes32 signersHash) external view returns (uint256) { return _baseWeightedMultisigStorage().epochBySignersHash[signersHash]; } /** * @notice This function returns the timestamp for the last signer rotation * @return uint256 The last rotation timestamp */ function lastRotationTimestamp() external view returns (uint256) { return _baseWeightedMultisigStorage().lastRotationTimestamp; } /** * @notice This function returns the time elapsed (in secs) since the last rotation * @return uint256 The time since the last rotation */ function timeSinceRotation() external view returns (uint256) { return block.timestamp - _baseWeightedMultisigStorage().lastRotationTimestamp; } /*************************\ |* Integration Functions *| \*************************/ /** * @notice This function takes dataHash and proof data and reverts if proof is invalid * @param dataHash The hash of the message that was signed * @param proof The multisig proof data * @return isLatestSigners True if the proof is from the latest signer set * @dev The proof data should have signers, weights, threshold and signatures encoded * The proof is only valid if the signers weight crosses the threshold and there are no redundant signatures * The signers and signatures should be sorted by signer address in ascending order * Example: abi.encode([0x11..., 0x22..., 0x33...], [1, 1, 1], 2, [signature1, signature3]) */ function _validateProof(bytes32 dataHash, Proof calldata proof) internal view returns (bool isLatestSigners) { BaseWeightedMultisigStorage storage slot = _baseWeightedMultisigStorage(); WeightedSigners calldata signers = proof.signers; bytes32 signersHash = keccak256(abi.encode(signers)); uint256 signerEpoch = slot.epochBySignersHash[signersHash]; uint256 currentEpoch = slot.epoch; isLatestSigners = signerEpoch == currentEpoch; if (signerEpoch == 0 || currentEpoch - signerEpoch > previousSignersRetention) revert InvalidSigners(); bytes32 messageHash = messageHashToSign(signersHash, dataHash); _validateSignatures(messageHash, signers, proof.signatures); } /** * @notice This function rotates the current signers with a new set of signers * @dev Rotation to repeated signers is not allowed. * While the individual signer addresses and weights can be repeated, the nonce must be different. * @param newSigners The new weighted signers data * @param enforceRotationDelay If true, the minimum rotation delay will be enforced * @dev The signers should be sorted by signer address in ascending order */ function _rotateSigners(WeightedSigners memory newSigners, bool enforceRotationDelay) internal { BaseWeightedMultisigStorage storage slot = _baseWeightedMultisigStorage(); _validateSigners(newSigners); _updateRotationTimestamp(enforceRotationDelay); bytes memory newSignersData = abi.encode(newSigners); bytes32 newSignersHash = keccak256(newSignersData); // assign the next epoch to the new signers uint256 newEpoch = slot.epoch + 1; slot.epoch = newEpoch; slot.signersHashByEpoch[newEpoch] = newSignersHash; // signers must be distinct, since nonce should guarantee uniqueness even if signers are repeated if (slot.epochBySignersHash[newSignersHash] != 0) revert DuplicateSigners(newSignersHash); slot.epochBySignersHash[newSignersHash] = newEpoch; emit SignersRotated(newEpoch, newSignersHash, newSignersData); } /**********************\ |* Internal Functions *| \**********************/ /** * @dev Updates the last rotation timestamp, and enforces the minimum rotation delay if specified */ function _updateRotationTimestamp(bool enforceRotationDelay) internal { uint256 lastRotationTimestamp_ = _baseWeightedMultisigStorage().lastRotationTimestamp; uint256 currentTimestamp = block.timestamp; if (enforceRotationDelay && (currentTimestamp - lastRotationTimestamp_) < minimumRotationDelay) { revert InsufficientRotationDelay( minimumRotationDelay, lastRotationTimestamp_, currentTimestamp - lastRotationTimestamp_ ); } _baseWeightedMultisigStorage().lastRotationTimestamp = currentTimestamp; } /** * @notice This function takes messageHash and proof data and reverts if proof is invalid * @param messageHash The hash of the message that was signed * @param weightedSigners The weighted signers data * @param signatures The sorted signatures data * @dev The signers and signatures should be sorted by signer address in ascending order */ function _validateSignatures( bytes32 messageHash, WeightedSigners calldata weightedSigners, bytes[] calldata signatures ) internal pure { WeightedSigner[] calldata signers = weightedSigners.signers; uint256 signersLength = signers.length; uint256 signaturesLength = signatures.length; uint256 signerIndex; uint256 totalWeight; // looking for signers within signers // this requires both signers and signatures to be sorted // having it sorted allows us to avoid the full inner loop to find a match for (uint256 i; i < signaturesLength; ++i) { address recoveredSigner = ECDSA.recover(messageHash, signatures[i]); // looping through remaining signers to find a match for (; signerIndex < signersLength && recoveredSigner != signers[signerIndex].signer; ++signerIndex) {} // checking if we are out of signers if (signerIndex == signersLength) revert MalformedSignatures(); // accumulating signatures weight totalWeight = totalWeight + signers[signerIndex].weight; // weight needs to reach threshold if (totalWeight >= weightedSigners.threshold) { // validate the proof if there are no redundant signatures if (i + 1 == signaturesLength) return; revert RedundantSignaturesProvided(i + 1, signaturesLength); } // increasing signers index if match was found ++signerIndex; } // if weight sum below threshold revert LowSignaturesWeight(); } /** * @notice Compute the message hash that is signed by the weighted signers * @dev Returns an Ethereum Signed Message, created from `domainSeparator`, `signersHash`, and `dataHash`. * This replicates the behavior of the * https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign[`eth_sign`] * JSON-RPC method. * * See {recover}. * * @param signersHash The hash of the weighted signers that sign off on the data * @param dataHash The hash of the data * @return The message hash to be signed */ function messageHashToSign(bytes32 signersHash, bytes32 dataHash) public view returns (bytes32) { // 96 is the length of the trailing bytes return keccak256(bytes.concat('\x19Ethereum Signed Message:\n96', domainSeparator, signersHash, dataHash)); } /** * @notice This function checks if the provided signers are valid, i.e sorted and contain no duplicates, with valid weights and threshold * @dev If signers are invalid, the method will revert * @param weightedSigners The weighted signers */ function _validateSigners(WeightedSigners memory weightedSigners) internal pure { WeightedSigner[] memory signers = weightedSigners.signers; uint256 length = signers.length; uint256 totalWeight; if (length == 0) revert InvalidSigners(); // since signers need to be in strictly increasing order, // this prevents address(0) from being a valid signer address prevSigner = address(0); for (uint256 i = 0; i < length; ++i) { WeightedSigner memory weightedSigner = signers[i]; address currSigner = weightedSigner.signer; if (prevSigner >= currSigner) { revert InvalidSigners(); } prevSigner = currSigner; uint256 weight = weightedSigner.weight; if (weight == 0) revert InvalidWeights(); totalWeight = totalWeight + weight; } uint128 threshold = weightedSigners.threshold; if (threshold == 0 || totalWeight < threshold) revert InvalidThreshold(); } /** * @notice Gets the specific storage location for preventing upgrade collisions * @return slot containing the BaseWeightedMultisigStorage struct */ function _baseWeightedMultisigStorage() internal pure returns (BaseWeightedMultisigStorage storage slot) { assembly { slot.slot := BASE_WEIGHTED_MULTISIG_SLOT } } } // File contracts/interfaces/ICaller.sol interface ICaller { error InvalidContract(address target); error InsufficientBalance(); error ExecutionFailed(); } // File contracts/interfaces/IInterchainMultisig.sol /** * @title IMultisig Interface * @notice This interface extends IMultisigBase by adding an execute function for multisignature transactions. */ interface IInterchainMultisig is ICaller, IBaseWeightedMultisig { error InvalidChainName(); error NotSelf(); error AlreadyExecuted(); error InvalidPayloadType(); error InvalidChainNameHash(); error InvalidVoidBatch(); error EmptyBatch(); error InvalidRecipient(); struct Call { string chainName; address executor; address target; bytes callData; uint256 nativeValue; } event BatchExecuted( bytes32 indexed batchId, bytes32 indexed batchHash, uint256 callsExecuted, uint256 indexed batchLength ); event CallExecuted(bytes32 indexed batchId, address indexed target, bytes callData, uint256 nativeValue); /** * @notice Returns the hash of the chain name * @return The hash of the chain name */ function chainNameHash() external view returns (bytes32); /** * @notice Checks if a payload has been executed * @param batchHash The hash of the payload payload * @return True if the payload has been executed */ function isBatchExecuted(bytes32 batchHash) external view returns (bool); /** * @notice This function takes dataHash and proof data and reverts if proof is invalid * @param dataHash The hash of the message that was signed * @param proof The data containing signers with signatures * @return isLatestSigners True if provided signers are the current ones */ function validateProof(bytes32 dataHash, Proof calldata proof) external view returns (bool isLatestSigners); /** * @notice Executes an external contract call. * @notice This function is protected by the onlySigners requirement. * @dev Calls a target address with specified calldata and passing provided native value. * @param batchId The batchId of the multisig * @param calls The batch of calls to execute * @param proof The multisig proof data */ function executeCalls( bytes32 batchId, Call[] calldata calls, Proof calldata proof ) external payable; /** * @notice Rotates the signers of the multisig * @notice This function is protected by the onlySelf modifier. * @param newSigners The new weighted signers encoded as bytes * @dev This function is only callable by the contract itself after signature verification */ function rotateSigners(WeightedSigners memory newSigners) external; /** * @notice Withdraws native token from the contract. * @notice This function is protected by the onlySelf modifier. * @param recipient The recipient of the native value * @param amount The amount of native value to withdraw * @dev This function is only callable by the contract itself after signature verification */ function withdraw(address recipient, uint256 amount) external; /** * @notice This function can be used to void a batch id from being executed in the future. This can be helpful to void an already signed but not yet executed batch. * @notice This function is protected by the onlySelf modifier. * @dev This function is only callable by the contract itself after signature verification */ function noop() external view; } // File contracts/libs/SafeNativeTransfer.sol error NativeTransferFailed(); /* * @title SafeNativeTransfer * @dev This library is used for performing safe native value transfers in Solidity by utilizing inline assembly. */ library SafeNativeTransfer { /* * @notice Perform a native transfer to a given address. * @param receiver The recipient address to which the amount will be sent. * @param amount The amount of native value to send. * @throws NativeTransferFailed error if transfer is not successful. */ function safeNativeTransfer(address receiver, uint256 amount) internal { bool success; assembly { success := call(gas(), receiver, amount, 0, 0, 0, 0) } if (!success) revert NativeTransferFailed(); } } // File contracts/libs/ContractAddress.sol library ContractAddress { function isContract(address contractAddress) internal view returns (bool) { bytes32 existingCodeHash = contractAddress.codehash; // https://eips.ethereum.org/EIPS/eip-1052 // keccak256('') == 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 return existingCodeHash != bytes32(0) && existingCodeHash != 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; } } // File contracts/utils/Caller.sol contract Caller is ICaller { using ContractAddress for address; /** * @dev Calls a target address with specified calldata and optionally sends value. */ function _call( address target, bytes calldata callData, uint256 nativeValue ) internal returns (bytes memory) { if (!target.isContract()) revert InvalidContract(target); if (nativeValue > address(this).balance) revert InsufficientBalance(); (bool success, bytes memory data) = target.call{ value: nativeValue }(callData); if (!success) { revert ExecutionFailed(); } return data; } } // File contracts/governance/InterchainMultisig.sol /** * @title InterchainMultisig Contract * @notice Weighted Multisig executor to call functions on any contract */ contract InterchainMultisig is Caller, BaseWeightedMultisig, IInterchainMultisig { // keccak256('InterchainMultisig.Slot') - 1 bytes32 internal constant INTERCHAIN_MULTISIG_SLOT = 0xee4c79745c2938ff2a269d76f8921d82df3b09446024c758a2e0e593fb2a65a7; using SafeNativeTransfer for address; bytes32 public immutable chainNameHash; struct InterchainMultisigStorage { mapping(bytes32 => bool) isBatchExecuted; } /** * @notice Contract constructor * @dev Sets the initial list of signers and corresponding threshold. * @param chainName The name of the chain * @param signers The weighted signers payload */ constructor( string memory chainName, bytes32 domainSeparator_, WeightedSigners memory signers ) BaseWeightedMultisig(0, domainSeparator_, 0) { if (bytes(chainName).length == 0) revert InvalidChainName(); chainNameHash = keccak256(bytes(chainName)); _rotateSigners(signers, false); } modifier onlySelf() { if (msg.sender != address(this)) revert NotSelf(); _; } /** * @notice Checks if a payload has been executed * @param batchId The hash of the payload payload * @return True if the payload has been executed */ function isBatchExecuted(bytes32 batchId) external view returns (bool) { return _interchainMultisigStorage().isBatchExecuted[batchId]; } /** * @notice Executes an external contract call. * @notice This function is protected by the onlySigners requirement. * @dev Executes a batch of calls with specified target addresses, calldata and native value. * @param batchId The batchId of the multisig * @param calls The batch of calls to execute * @param proof The multisig proof data * @dev The proof data should have signers, weights, threshold and signatures encoded * @dev The signers and signatures should be sorted by signer address in ascending order */ function executeCalls( bytes32 batchId, Call[] calldata calls, Proof calldata proof ) external payable { InterchainMultisigStorage storage slot = _interchainMultisigStorage(); bytes32 batchHash = keccak256(abi.encode(batchId, calls)); uint256 callsLength = calls.length; _validateProof(batchHash, proof); if (slot.isBatchExecuted[batchId]) revert AlreadyExecuted(); slot.isBatchExecuted[batchId] = true; uint256 callsExecuted = 0; for (uint256 i; i < callsLength; ++i) { Call calldata call = calls[i]; // check if the call is for this contract and chain if (keccak256(bytes(call.chainName)) == chainNameHash && call.executor == address(this)) { // slither-disable-next-line reentrancy-events emit CallExecuted(batchId, call.target, call.callData, call.nativeValue); _call(call.target, call.callData, call.nativeValue); ++callsExecuted; } } if (callsExecuted == 0) revert EmptyBatch(); emit BatchExecuted(batchId, batchHash, callsExecuted, callsLength); } /** * @notice This function takes dataHash and proof data and reverts if proof is invalid * @param dataHash The hash of the message that was signed * @param proof The data containing signers with signatures * @return isLatestSigners True if provided signers are the current ones */ function validateProof(bytes32 dataHash, Proof calldata proof) external view returns (bool isLatestSigners) { isLatestSigners = _validateProof(dataHash, proof); } /** * @notice Rotates the signers of the multisig * @notice This function is protected by the onlySelf modifier. * @param newSigners The new weighted signers encoded as bytes * @dev This function is only callable by the contract itself after signature verification */ function rotateSigners(WeightedSigners memory newSigners) external onlySelf { _rotateSigners(newSigners, false); } /** * @notice Withdraws native token from the contract. * @notice This function is protected by the onlySelf modifier. * @param recipient The recipient of the native value * @param amount The amount of native value to withdraw * @dev This function is only callable by the contract itself after signature verification */ function withdraw(address recipient, uint256 amount) external onlySelf { if (recipient == address(0)) revert InvalidRecipient(); if (amount > address(this).balance) revert InsufficientBalance(); recipient.safeNativeTransfer(amount); } /** * @notice This function can be used to void a batch id from being executed in the future. This can be helpful to void an already signed but not yet executed batch. * @notice This function is protected by the onlySelf modifier. * @dev This function is only callable by the contract itself after signature verification */ function noop() external view onlySelf {} /** * @notice Allow contract to be able to receive native value */ receive() external payable {} /** * @notice Get the storage slot for the InterchainMultisigStorage struct */ function _interchainMultisigStorage() private pure returns (InterchainMultisigStorage storage slot) { assembly { slot.slot := INTERCHAIN_MULTISIG_SLOT } } }