Add Aggregation ISM (#1737)
### Description This PR introduces the Aggregation ISM, which requires interchain messages to be verified by an m-of-n ISM set. This PR also introduces a new MultisigISM, which does not make use of the "commitment" optimization in its metadata. The previous MultisigISM has been moved to LegacyMultisigISM. Both AggregationISM and MultisigISM have abstract contracts that require `valuesAndThreshold` implementations. This PR implements versions with static m-of-n sets, using the EIP-3448 MetaProxy standard. These implementations are intended to be combined with a future RoutingISM implementation to replace LegacyMultisigISM. ### Drive-by changes - genhtml in solidity code coverage ### Related issues - Fixes #1522 ### Backward compatibility _Are these changes backward compatible?_ Yes ### Testing _What kind of testing have these changes undergone? Unit Teststrevor/fallback-igp
parent
5c6d70dfc4
commit
e8d90775f7
@ -0,0 +1,72 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
// ============ External Imports ============ |
||||
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; |
||||
|
||||
// ============ Internal Imports ============ |
||||
import {IInterchainSecurityModule} from "../../../interfaces/IInterchainSecurityModule.sol"; |
||||
import {IAggregationIsm} from "../../../interfaces/IAggregationIsm.sol"; |
||||
import {AggregationIsmMetadata} from "../../libs/isms/AggregationIsmMetadata.sol"; |
||||
|
||||
/** |
||||
* @title AggregationIsm |
||||
* @notice Manages per-domain m-of-n ISM sets that are used to verify |
||||
* interchain messages. |
||||
*/ |
||||
abstract contract AbstractAggregationIsm is IAggregationIsm { |
||||
// ============ Constants ============ |
||||
|
||||
uint8 public constant moduleType = |
||||
uint8(IInterchainSecurityModule.Types.MULTISIG); |
||||
|
||||
// ============ Virtual Functions ============ |
||||
// ======= OVERRIDE THESE TO IMPLEMENT ======= |
||||
|
||||
/** |
||||
* @notice Returns the set of ISMs responsible for verifying _message |
||||
* and the number of ISMs that must verify |
||||
* @dev Can change based on the content of _message |
||||
* @param _message Hyperlane formatted interchain message |
||||
* @return modules The array of ISM addresses |
||||
* @return threshold The number of ISMs needed to verify |
||||
*/ |
||||
function modulesAndThreshold(bytes calldata _message) |
||||
public |
||||
view |
||||
virtual |
||||
returns (address[] memory, uint8); |
||||
|
||||
// ============ Public Functions ============ |
||||
|
||||
/** |
||||
* @notice Requires that m-of-n ISMs verify the provided interchain message. |
||||
* @param _metadata ABI encoded module metadata (see AggregationIsmMetadata.sol) |
||||
* @param _message Formatted Hyperlane message (see Message.sol). |
||||
*/ |
||||
function verify(bytes calldata _metadata, bytes calldata _message) |
||||
public |
||||
returns (bool) |
||||
{ |
||||
(address[] memory _isms, uint8 _threshold) = modulesAndThreshold( |
||||
_message |
||||
); |
||||
uint256 _count = _isms.length; |
||||
for (uint8 i = 0; i < _count; i++) { |
||||
if (!AggregationIsmMetadata.hasMetadata(_metadata, i)) continue; |
||||
IInterchainSecurityModule _ism = IInterchainSecurityModule( |
||||
_isms[i] |
||||
); |
||||
require( |
||||
_ism.verify( |
||||
AggregationIsmMetadata.metadataAt(_metadata, i), |
||||
_message |
||||
), |
||||
"!verify" |
||||
); |
||||
_threshold -= 1; |
||||
} |
||||
require(_threshold == 0, "!threshold"); |
||||
return true; |
||||
} |
||||
} |
@ -0,0 +1,33 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
// ============ Internal Imports ============ |
||||
import {AbstractAggregationIsm} from "./AbstractAggregationIsm.sol"; |
||||
import {AggregationIsmMetadata} from "../../libs/isms/AggregationIsmMetadata.sol"; |
||||
import {MetaProxy} from "../../libs/MetaProxy.sol"; |
||||
|
||||
/** |
||||
* @title StaticAggregationIsm |
||||
* @notice Manages per-domain m-of-n ISM sets that are used to verify |
||||
* interchain messages. |
||||
*/ |
||||
contract StaticAggregationIsm is AbstractAggregationIsm { |
||||
// ============ Public Functions ============ |
||||
|
||||
/** |
||||
* @notice Returns the set of ISMs responsible for verifying _message |
||||
* and the number of ISMs that must verify |
||||
* @dev Can change based on the content of _message |
||||
* @return modules The array of ISM addresses |
||||
* @return threshold The number of ISMs needed to verify |
||||
*/ |
||||
function modulesAndThreshold(bytes calldata) |
||||
public |
||||
view |
||||
virtual |
||||
override |
||||
returns (address[] memory, uint8) |
||||
{ |
||||
return abi.decode(MetaProxy.metadata(), (address[], uint8)); |
||||
} |
||||
} |
@ -0,0 +1,16 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
// ============ Internal Imports ============ |
||||
import {StaticAggregationIsm} from "./StaticAggregationIsm.sol"; |
||||
import {StaticMOfNAddressSetFactory} from "../../libs/StaticMOfNAddressSetFactory.sol"; |
||||
|
||||
contract StaticAggregationIsmFactory is StaticMOfNAddressSetFactory { |
||||
function _deployImplementation() |
||||
internal |
||||
virtual |
||||
override |
||||
returns (address) |
||||
{ |
||||
return address(new StaticAggregationIsm()); |
||||
} |
||||
} |
@ -0,0 +1,124 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
// ============ External Imports ============ |
||||
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; |
||||
|
||||
// ============ Internal Imports ============ |
||||
import {IInterchainSecurityModule} from "../../../interfaces/IInterchainSecurityModule.sol"; |
||||
import {IMultisigIsm} from "../../../interfaces/isms/IMultisigIsm.sol"; |
||||
import {Message} from "../../libs/Message.sol"; |
||||
import {MultisigIsmMetadata} from "../../libs/isms/MultisigIsmMetadata.sol"; |
||||
import {CheckpointLib} from "../../libs/CheckpointLib.sol"; |
||||
import {MerkleLib} from "../../libs/Merkle.sol"; |
||||
|
||||
/** |
||||
* @title MultisigIsm |
||||
* @notice Manages per-domain m-of-n Validator sets that are used to verify |
||||
* interchain messages. |
||||
*/ |
||||
abstract contract AbstractMultisigIsm is IMultisigIsm { |
||||
// ============ Constants ============ |
||||
|
||||
uint8 public constant moduleType = |
||||
uint8(IInterchainSecurityModule.Types.MULTISIG); |
||||
|
||||
// ============ Virtual Functions ============ |
||||
// ======= OVERRIDE THESE TO IMPLEMENT ======= |
||||
|
||||
/** |
||||
* @notice Returns the set of validators responsible for verifying _message |
||||
* and the number of signatures required |
||||
* @dev Can change based on the content of _message |
||||
* @param _message Hyperlane formatted interchain message |
||||
* @return validators The array of validator addresses |
||||
* @return threshold The number of validator signatures needed |
||||
*/ |
||||
function validatorsAndThreshold(bytes calldata _message) |
||||
public |
||||
view |
||||
virtual |
||||
returns (address[] memory, uint8); |
||||
|
||||
// ============ Public Functions ============ |
||||
|
||||
/** |
||||
* @notice Requires that m-of-n validators verify a merkle root, |
||||
* and verifies a merkle proof of `_message` against that root. |
||||
* @param _metadata ABI encoded module metadata (see MultisigIsmMetadata.sol) |
||||
* @param _message Formatted Hyperlane message (see Message.sol). |
||||
*/ |
||||
function verify(bytes calldata _metadata, bytes calldata _message) |
||||
public |
||||
view |
||||
returns (bool) |
||||
{ |
||||
require(_verifyMerkleProof(_metadata, _message), "!merkle"); |
||||
require(_verifyValidatorSignatures(_metadata, _message), "!sigs"); |
||||
return true; |
||||
} |
||||
|
||||
// ============ Internal Functions ============ |
||||
|
||||
/** |
||||
* @notice Verifies the merkle proof of `_message` against the provided |
||||
* checkpoint. |
||||
* @param _metadata ABI encoded module metadata (see MultisigIsmMetadata.sol) |
||||
* @param _message Formatted Hyperlane message (see Message.sol). |
||||
*/ |
||||
function _verifyMerkleProof( |
||||
bytes calldata _metadata, |
||||
bytes calldata _message |
||||
) internal pure returns (bool) { |
||||
// calculate the expected root based on the proof |
||||
bytes32 _calculatedRoot = MerkleLib.branchRoot( |
||||
Message.id(_message), |
||||
MultisigIsmMetadata.proof(_metadata), |
||||
Message.nonce(_message) |
||||
); |
||||
return _calculatedRoot == MultisigIsmMetadata.root(_metadata); |
||||
} |
||||
|
||||
/** |
||||
* @notice Verifies that a quorum of the origin domain's validators signed |
||||
* the provided checkpoint. |
||||
* @param _metadata ABI encoded module metadata (see MultisigIsmMetadata.sol) |
||||
* @param _message Formatted Hyperlane message (see Message.sol). |
||||
*/ |
||||
function _verifyValidatorSignatures( |
||||
bytes calldata _metadata, |
||||
bytes calldata _message |
||||
) internal view returns (bool) { |
||||
( |
||||
address[] memory _validators, |
||||
uint8 _threshold |
||||
) = validatorsAndThreshold(_message); |
||||
require(_threshold > 0, "No MultisigISM threshold present for message"); |
||||
bytes32 _digest = CheckpointLib.digest( |
||||
Message.origin(_message), |
||||
MultisigIsmMetadata.originMailbox(_metadata), |
||||
MultisigIsmMetadata.root(_metadata), |
||||
MultisigIsmMetadata.index(_metadata) |
||||
); |
||||
uint256 _validatorCount = _validators.length; |
||||
uint256 _validatorIndex = 0; |
||||
// Assumes that signatures are ordered by validator |
||||
for (uint256 i = 0; i < _threshold; ++i) { |
||||
address _signer = ECDSA.recover( |
||||
_digest, |
||||
MultisigIsmMetadata.signatureAt(_metadata, i) |
||||
); |
||||
// Loop through remaining validators until we find a match |
||||
for ( |
||||
; |
||||
_validatorIndex < _validatorCount && |
||||
_signer != _validators[_validatorIndex]; |
||||
++_validatorIndex |
||||
) {} |
||||
// Fail if we never found a match |
||||
require(_validatorIndex < _validatorCount, "!threshold"); |
||||
++_validatorIndex; |
||||
} |
||||
return true; |
||||
} |
||||
} |
@ -0,0 +1,33 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
// ============ Internal Imports ============ |
||||
import {AbstractMultisigIsm} from "./AbstractMultisigIsm.sol"; |
||||
import {MultisigIsmMetadata} from "../../libs/isms/MultisigIsmMetadata.sol"; |
||||
import {MetaProxy} from "../../libs/MetaProxy.sol"; |
||||
|
||||
/** |
||||
* @title StaticMultisigIsm |
||||
* @notice Manages per-domain m-of-n Validator sets that are used |
||||
* to verify interchain messages. |
||||
*/ |
||||
contract StaticMultisigIsm is AbstractMultisigIsm { |
||||
// ============ Public Functions ============ |
||||
|
||||
/** |
||||
* @notice Returns the set of validators responsible for verifying _message |
||||
* and the number of signatures required |
||||
* @dev Can change based on the content of _message |
||||
* @return validators The array of validator addresses |
||||
* @return threshold The number of validator signatures needed |
||||
*/ |
||||
function validatorsAndThreshold(bytes calldata) |
||||
public |
||||
view |
||||
virtual |
||||
override |
||||
returns (address[] memory, uint8) |
||||
{ |
||||
return abi.decode(MetaProxy.metadata(), (address[], uint8)); |
||||
} |
||||
} |
@ -0,0 +1,16 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
// ============ Internal Imports ============ |
||||
import {StaticMultisigIsm} from "./StaticMultisigIsm.sol"; |
||||
import {StaticMOfNAddressSetFactory} from "../../libs/StaticMOfNAddressSetFactory.sol"; |
||||
|
||||
contract StaticMultisigIsmFactory is StaticMOfNAddressSetFactory { |
||||
function _deployImplementation() |
||||
internal |
||||
virtual |
||||
override |
||||
returns (address) |
||||
{ |
||||
return address(new StaticMultisigIsm()); |
||||
} |
||||
} |
@ -0,0 +1,54 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
// ============ External Imports ============ |
||||
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; |
||||
|
||||
library CheckpointLib { |
||||
/** |
||||
* @notice Returns the digest validators are expected to sign when signing checkpoints. |
||||
* @param _origin The origin domain of the checkpoint. |
||||
* @param _originMailbox The address of the origin mailbox as bytes32. |
||||
* @return The digest of the checkpoint. |
||||
*/ |
||||
function digest( |
||||
uint32 _origin, |
||||
bytes32 _originMailbox, |
||||
bytes32 _checkpointRoot, |
||||
uint32 _checkpointIndex |
||||
) internal pure returns (bytes32) { |
||||
bytes32 _domainHash = domainHash(_origin, _originMailbox); |
||||
return |
||||
ECDSA.toEthSignedMessageHash( |
||||
keccak256( |
||||
abi.encodePacked( |
||||
_domainHash, |
||||
_checkpointRoot, |
||||
_checkpointIndex |
||||
) |
||||
) |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the domain hash that validators are expected to use |
||||
* when signing checkpoints. |
||||
* @param _origin The origin domain of the checkpoint. |
||||
* @param _originMailbox The address of the origin mailbox as bytes32. |
||||
* @return The domain hash. |
||||
*/ |
||||
function domainHash(uint32 _origin, bytes32 _originMailbox) |
||||
internal |
||||
pure |
||||
returns (bytes32) |
||||
{ |
||||
// Including the origin mailbox address in the signature allows the slashing |
||||
// protocol to enroll multiple mailboxes. Otherwise, a valid signature for |
||||
// mailbox A would be indistinguishable from a fraudulent signature for mailbox |
||||
// B. |
||||
// The slashing protocol should slash if validators sign attestations for |
||||
// anything other than a whitelisted mailbox. |
||||
return |
||||
keccak256(abi.encodePacked(_origin, _originMailbox, "HYPERLANE")); |
||||
} |
||||
} |
@ -0,0 +1,40 @@ |
||||
// SPDX-License-Identifier: CC0-1.0 |
||||
pragma solidity >=0.7.6; |
||||
|
||||
/// @dev Adapted from https://eips.ethereum.org/EIPS/eip-3448 |
||||
library MetaProxy { |
||||
bytes32 constant PREFIX = |
||||
hex"600b380380600b3d393df3363d3d373d3d3d3d60368038038091363936013d73"; |
||||
bytes13 constant SUFFIX = hex"5af43d3d93803e603457fd5bf3"; |
||||
|
||||
function bytecode(address _implementation, bytes memory _metadata) |
||||
internal |
||||
pure |
||||
returns (bytes memory) |
||||
{ |
||||
return |
||||
abi.encodePacked( |
||||
PREFIX, |
||||
bytes20(_implementation), |
||||
SUFFIX, |
||||
_metadata, |
||||
_metadata.length |
||||
); |
||||
} |
||||
|
||||
function metadata() internal pure returns (bytes memory) { |
||||
bytes memory data; |
||||
assembly { |
||||
let posOfMetadataSize := sub(calldatasize(), 32) |
||||
let size := calldataload(posOfMetadataSize) |
||||
let dataPtr := sub(posOfMetadataSize, size) |
||||
data := mload(64) |
||||
// increment free memory pointer by metadata size + 32 bytes (length) |
||||
mstore(64, add(data, add(size, 32))) |
||||
mstore(data, size) |
||||
let memPtr := add(data, 32) |
||||
calldatacopy(memPtr, dataPtr, size) |
||||
} |
||||
return data; |
||||
} |
||||
} |
@ -0,0 +1,98 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
// ============ External Imports ============ |
||||
import {Address} from "@openzeppelin/contracts/utils/Address.sol"; |
||||
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; |
||||
|
||||
// ============ Internal Imports ============ |
||||
import {MetaProxy} from "./MetaProxy.sol"; |
||||
|
||||
abstract contract StaticMOfNAddressSetFactory { |
||||
// ============ Immutables ============ |
||||
address private immutable _implementation; |
||||
|
||||
// ============ Constructor ============ |
||||
|
||||
constructor() { |
||||
_implementation = _deployImplementation(); |
||||
} |
||||
|
||||
function _deployImplementation() internal virtual returns (address); |
||||
|
||||
/** |
||||
* @notice Deploys a StaticMOfNAddressSet contract address for the given |
||||
* values |
||||
* @dev Consider sorting addresses to ensure contract reuse |
||||
* @param _values An array of addresses |
||||
* @param _threshold The threshold value to use |
||||
* @return set The contract address representing this StaticMOfNAddressSet |
||||
*/ |
||||
function deploy(address[] calldata _values, uint8 _threshold) |
||||
external |
||||
returns (address) |
||||
{ |
||||
(bytes32 _salt, bytes memory _bytecode) = _saltAndBytecode( |
||||
_values, |
||||
_threshold |
||||
); |
||||
address _set = _getAddress(_salt, _bytecode); |
||||
if (!Address.isContract(_set)) { |
||||
_set = Create2.deploy(0, _salt, _bytecode); |
||||
} |
||||
return _set; |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the StaticMOfNAddressSet contract address for the given |
||||
* values |
||||
* @dev Consider sorting addresses to ensure contract reuse |
||||
* @param _values An array of addresses |
||||
* @param _threshold The threshold value to use |
||||
* @return set The contract address representing this StaticMOfNAddressSet |
||||
*/ |
||||
function getAddress(address[] calldata _values, uint8 _threshold) |
||||
external |
||||
view |
||||
returns (address) |
||||
{ |
||||
(bytes32 _salt, bytes memory _bytecode) = _saltAndBytecode( |
||||
_values, |
||||
_threshold |
||||
); |
||||
return _getAddress(_salt, _bytecode); |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the StaticMOfNAddressSet contract address for the given |
||||
* values |
||||
* @param _salt The salt used in Create2 |
||||
* @param _bytecode The metaproxy bytecode used in Create2 |
||||
* @return set The contract address representing this StaticMOfNAddressSet |
||||
*/ |
||||
function _getAddress(bytes32 _salt, bytes memory _bytecode) |
||||
private |
||||
view |
||||
returns (address) |
||||
{ |
||||
bytes32 _bytecodeHash = keccak256(_bytecode); |
||||
return Create2.computeAddress(_salt, _bytecodeHash); |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the create2 salt and bytecode for the given values |
||||
* @param _values An array of addresses |
||||
* @param _threshold The threshold value to use |
||||
* @return _salt The salt used in Create2 |
||||
* @return _bytecode The metaproxy bytecode used in Create2 |
||||
*/ |
||||
function _saltAndBytecode(address[] calldata _values, uint8 _threshold) |
||||
private |
||||
view |
||||
returns (bytes32, bytes memory) |
||||
{ |
||||
bytes memory _metadata = abi.encode(_values, _threshold); |
||||
bytes memory _bytecode = MetaProxy.bytecode(_implementation, _metadata); |
||||
bytes32 _salt = keccak256(_metadata); |
||||
return (_salt, _bytecode); |
||||
} |
||||
} |
@ -0,0 +1,72 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
/** |
||||
* Format of metadata: |
||||
* |
||||
* [????:????] Metadata start/end uint32 ranges, packed as uint64 |
||||
* [????:????] ISM metadata, packed encoding |
||||
*/ |
||||
library AggregationIsmMetadata { |
||||
uint256 private constant RANGE_SIZE = 4; |
||||
|
||||
/** |
||||
* @notice Returns whether or not metadata was provided for the ISM at |
||||
* `_index` |
||||
* @dev Callers must ensure _index is less than the number of metadatas |
||||
* provided |
||||
* @param _metadata Encoded Aggregation ISM metadata |
||||
* @param _index The index of the ISM to check for metadata for |
||||
* @return Whether or not metadata was provided for the ISM at `_index` |
||||
*/ |
||||
function hasMetadata(bytes calldata _metadata, uint8 _index) |
||||
internal |
||||
pure |
||||
returns (bool) |
||||
{ |
||||
(uint32 _start, ) = _metadataRange(_metadata, _index); |
||||
return _start > 0; |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the metadata provided for the ISM at `_index` |
||||
* @dev Callers must ensure _index is less than the number of metadatas |
||||
* provided |
||||
* @dev Callers must ensure `hasMetadata(_metadata, _index)` |
||||
* @param _metadata Encoded Aggregation ISM metadata |
||||
* @param _index The index of the ISM to return metadata for |
||||
* @return The metadata provided for the ISM at `_index` |
||||
*/ |
||||
function metadataAt(bytes calldata _metadata, uint8 _index) |
||||
internal |
||||
pure |
||||
returns (bytes calldata) |
||||
{ |
||||
(uint32 _start, uint32 _end) = _metadataRange(_metadata, _index); |
||||
return _metadata[_start:_end]; |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the range of the metadata provided for the ISM at |
||||
* `_index`, or zeroes if not provided |
||||
* @dev Callers must ensure _index is less than the number of metadatas |
||||
* provided |
||||
* @param _metadata Encoded Aggregation ISM metadata |
||||
* @param _index The index of the ISM to return metadata range for |
||||
* @return The range of the metadata provided for the ISM at `_index`, or |
||||
* zeroes if not provided |
||||
*/ |
||||
function _metadataRange(bytes calldata _metadata, uint8 _index) |
||||
private |
||||
pure |
||||
returns (uint32, uint32) |
||||
{ |
||||
uint256 _start = (uint32(_index) * RANGE_SIZE * 2); |
||||
uint256 _mid = _start + RANGE_SIZE; |
||||
uint256 _end = _mid + RANGE_SIZE; |
||||
return ( |
||||
uint32(bytes4(_metadata[_start:_mid])), |
||||
uint32(bytes4(_metadata[_mid:_end])) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,170 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
/** |
||||
* Format of metadata: |
||||
* [ 0: 32] Merkle root |
||||
* [ 32: 36] Root index |
||||
* [ 36: 68] Origin mailbox address |
||||
* [ 68:1092] Merkle proof |
||||
* [1092:1093] Threshold |
||||
* [1093:????] Validator signatures, 65 bytes each, length == Threshold |
||||
* [????:????] Addresses of the entire validator set, left padded to bytes32 |
||||
*/ |
||||
library LegacyMultisigIsmMetadata { |
||||
uint256 private constant MERKLE_ROOT_OFFSET = 0; |
||||
uint256 private constant MERKLE_INDEX_OFFSET = 32; |
||||
uint256 private constant ORIGIN_MAILBOX_OFFSET = 36; |
||||
uint256 private constant MERKLE_PROOF_OFFSET = 68; |
||||
uint256 private constant THRESHOLD_OFFSET = 1092; |
||||
uint256 private constant SIGNATURES_OFFSET = 1093; |
||||
uint256 private constant SIGNATURE_LENGTH = 65; |
||||
|
||||
/** |
||||
* @notice Returns the merkle root of the signed checkpoint. |
||||
* @param _metadata ABI encoded Multisig ISM metadata. |
||||
* @return Merkle root of the signed checkpoint |
||||
*/ |
||||
function root(bytes calldata _metadata) internal pure returns (bytes32) { |
||||
return bytes32(_metadata[MERKLE_ROOT_OFFSET:MERKLE_INDEX_OFFSET]); |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the index of the signed checkpoint. |
||||
* @param _metadata ABI encoded Multisig ISM metadata. |
||||
* @return Index of the signed checkpoint |
||||
*/ |
||||
function index(bytes calldata _metadata) internal pure returns (uint32) { |
||||
return |
||||
uint32( |
||||
bytes4(_metadata[MERKLE_INDEX_OFFSET:ORIGIN_MAILBOX_OFFSET]) |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the origin mailbox of the signed checkpoint as bytes32. |
||||
* @param _metadata ABI encoded Multisig ISM metadata. |
||||
* @return Origin mailbox of the signed checkpoint as bytes32 |
||||
*/ |
||||
function originMailbox(bytes calldata _metadata) |
||||
internal |
||||
pure |
||||
returns (bytes32) |
||||
{ |
||||
return bytes32(_metadata[ORIGIN_MAILBOX_OFFSET:MERKLE_PROOF_OFFSET]); |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the merkle proof branch of the message. |
||||
* @dev This appears to be more gas efficient than returning a calldata |
||||
* slice and using that. |
||||
* @param _metadata ABI encoded Multisig ISM metadata. |
||||
* @return Merkle proof branch of the message. |
||||
*/ |
||||
function proof(bytes calldata _metadata) |
||||
internal |
||||
pure |
||||
returns (bytes32[32] memory) |
||||
{ |
||||
return |
||||
abi.decode( |
||||
_metadata[MERKLE_PROOF_OFFSET:THRESHOLD_OFFSET], |
||||
(bytes32[32]) |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the number of required signatures. Verified against |
||||
* the commitment stored in the module. |
||||
* @param _metadata ABI encoded Multisig ISM metadata. |
||||
* @return The number of required signatures. |
||||
*/ |
||||
function threshold(bytes calldata _metadata) internal pure returns (uint8) { |
||||
return uint8(bytes1(_metadata[THRESHOLD_OFFSET:SIGNATURES_OFFSET])); |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the validator ECDSA signature at `_index`. |
||||
* @dev Assumes signatures are sorted by validator |
||||
* @dev Assumes `_metadata` encodes `threshold` signatures. |
||||
* @dev Assumes `_index` is less than `threshold` |
||||
* @param _metadata ABI encoded Multisig ISM metadata. |
||||
* @param _index The index of the signature to return. |
||||
* @return The validator ECDSA signature at `_index`. |
||||
*/ |
||||
function signatureAt(bytes calldata _metadata, uint256 _index) |
||||
internal |
||||
pure |
||||
returns (bytes calldata) |
||||
{ |
||||
uint256 _start = SIGNATURES_OFFSET + (_index * SIGNATURE_LENGTH); |
||||
uint256 _end = _start + SIGNATURE_LENGTH; |
||||
return _metadata[_start:_end]; |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the validator address at `_index`. |
||||
* @dev Assumes `_index` is less than the number of validators |
||||
* @param _metadata ABI encoded Multisig ISM metadata. |
||||
* @param _index The index of the validator to return. |
||||
* @return The validator address at `_index`. |
||||
*/ |
||||
function validatorAt(bytes calldata _metadata, uint256 _index) |
||||
internal |
||||
pure |
||||
returns (address) |
||||
{ |
||||
// Validator addresses are left padded to bytes32 in order to match |
||||
// abi.encodePacked(address[]). |
||||
uint256 _start = _validatorsOffset(_metadata) + (_index * 32) + 12; |
||||
uint256 _end = _start + 20; |
||||
return address(bytes20(_metadata[_start:_end])); |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the validator set encoded as bytes. Verified against the |
||||
* commitment stored in the module. |
||||
* @dev Validator addresses are encoded as tightly packed array of bytes32, |
||||
* sorted to match the enumerable set stored by the module. |
||||
* @param _metadata ABI encoded Multisig ISM metadata. |
||||
* @return The validator set encoded as bytes. |
||||
*/ |
||||
function validators(bytes calldata _metadata) |
||||
internal |
||||
pure |
||||
returns (bytes calldata) |
||||
{ |
||||
return _metadata[_validatorsOffset(_metadata):]; |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the size of the validator set encoded in the metadata |
||||
* @dev Validator addresses are encoded as tightly packed array of bytes32, |
||||
* sorted to match the enumerable set stored by the module. |
||||
* @param _metadata ABI encoded Multisig ISM metadata. |
||||
* @return The size of the validator set encoded in the metadata |
||||
*/ |
||||
function validatorCount(bytes calldata _metadata) |
||||
internal |
||||
pure |
||||
returns (uint256) |
||||
{ |
||||
return (_metadata.length - _validatorsOffset(_metadata)) / 32; |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the offset in bytes of the list of validators within |
||||
* `_metadata`. |
||||
* @param _metadata ABI encoded Multisig ISM metadata. |
||||
* @return The index at which the list of validators starts |
||||
*/ |
||||
function _validatorsOffset(bytes calldata _metadata) |
||||
private |
||||
pure |
||||
returns (uint256) |
||||
{ |
||||
return |
||||
SIGNATURES_OFFSET + |
||||
(uint256(threshold(_metadata)) * SIGNATURE_LENGTH); |
||||
} |
||||
} |
@ -0,0 +1,16 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
// ============ Internal Imports ============ |
||||
import {LegacyMultisigIsm} from "../isms/multisig/LegacyMultisigIsm.sol"; |
||||
import {CheckpointLib} from "../libs/CheckpointLib.sol"; |
||||
|
||||
contract TestLegacyMultisigIsm is LegacyMultisigIsm { |
||||
function getDomainHash(uint32 _origin, bytes32 _originMailbox) |
||||
external |
||||
pure |
||||
returns (bytes32) |
||||
{ |
||||
return CheckpointLib.domainHash(_origin, _originMailbox); |
||||
} |
||||
} |
@ -1,23 +0,0 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
// ============ Internal Imports ============ |
||||
import {MultisigIsm} from "../isms/MultisigIsm.sol"; |
||||
|
||||
contract TestMultisigIsm is MultisigIsm { |
||||
function getDomainHash(uint32 _origin, bytes32 _originMailbox) |
||||
external |
||||
pure |
||||
returns (bytes32) |
||||
{ |
||||
return _getDomainHash(_origin, _originMailbox); |
||||
} |
||||
|
||||
function getCheckpointDigest(bytes calldata _metadata, uint32 _origin) |
||||
external |
||||
pure |
||||
returns (bytes32) |
||||
{ |
||||
return _getCheckpointDigest(_metadata, _origin); |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.0; |
||||
|
||||
import {IInterchainSecurityModule} from "./IInterchainSecurityModule.sol"; |
||||
|
||||
interface IAggregationIsm is IInterchainSecurityModule { |
||||
/** |
||||
* @notice Returns the set of modules responsible for verifying _message |
||||
* and the number of modules that must verify |
||||
* @dev Can change based on the content of _message |
||||
* @param _message Hyperlane formatted interchain message |
||||
* @return modules The array of ISM addresses |
||||
* @return threshold The number of modules needed to verify |
||||
*/ |
||||
function modulesAndThreshold(bytes calldata _message) |
||||
external |
||||
view |
||||
returns (address[] memory modules, uint8 threshold); |
||||
} |
@ -1,5 +1,5 @@ |
||||
{ |
||||
"filter_paths": "lib|node_modules|test|Mock*|Test*", |
||||
"compile_force_framework": "foundry", |
||||
"exclude_informational": true |
||||
} |
||||
"filter_paths": "lib|node_modules|test|Mock*|Test*", |
||||
"compile_force_framework": "foundry", |
||||
"exclude_informational": true |
||||
} |
||||
|
@ -0,0 +1,156 @@ |
||||
// SPDX-License-Identifier: Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
|
||||
import "forge-std/Test.sol"; |
||||
|
||||
import {IAggregationIsm} from "../../interfaces/IAggregationIsm.sol"; |
||||
import {StaticAggregationIsmFactory} from "../../contracts/isms/aggregation/StaticAggregationIsmFactory.sol"; |
||||
import {AggregationIsmMetadata} from "../../contracts/libs/isms/AggregationIsmMetadata.sol"; |
||||
import {MOfNTestUtils} from "./MOfNTestUtils.sol"; |
||||
|
||||
contract TestIsm { |
||||
bytes public requiredMetadata; |
||||
|
||||
constructor(bytes memory _requiredMetadata) { |
||||
setRequiredMetadata(_requiredMetadata); |
||||
} |
||||
|
||||
function setRequiredMetadata(bytes memory _requiredMetadata) public { |
||||
requiredMetadata = _requiredMetadata; |
||||
} |
||||
|
||||
function verify(bytes calldata _metadata, bytes calldata) |
||||
external |
||||
view |
||||
returns (bool) |
||||
{ |
||||
return keccak256(_metadata) == keccak256(requiredMetadata); |
||||
} |
||||
} |
||||
|
||||
contract AggregationIsmTest is Test { |
||||
StaticAggregationIsmFactory factory; |
||||
IAggregationIsm ism; |
||||
|
||||
function setUp() public { |
||||
factory = new StaticAggregationIsmFactory(); |
||||
} |
||||
|
||||
function deployIsms( |
||||
uint8 m, |
||||
uint8 n, |
||||
bytes32 seed |
||||
) internal returns (address[] memory) { |
||||
bytes32 randomness = seed; |
||||
address[] memory isms = new address[](n); |
||||
for (uint256 i = 0; i < n; i++) { |
||||
randomness = keccak256(abi.encode(randomness)); |
||||
TestIsm subIsm = new TestIsm(abi.encode(randomness)); |
||||
isms[i] = address(subIsm); |
||||
} |
||||
ism = IAggregationIsm(factory.deploy(isms, m)); |
||||
return isms; |
||||
} |
||||
|
||||
function getMetadata(uint8 m, bytes32 seed) |
||||
private |
||||
view |
||||
returns (bytes memory) |
||||
{ |
||||
(address[] memory choices, ) = ism.modulesAndThreshold(""); |
||||
address[] memory chosen = MOfNTestUtils.choose(m, choices, seed); |
||||
bytes memory offsets; |
||||
uint32 start = 8 * uint32(choices.length); |
||||
bytes memory metametadata; |
||||
for (uint256 i = 0; i < choices.length; i++) { |
||||
bool included = false; |
||||
for (uint256 j = 0; j < chosen.length; j++) { |
||||
included = included || choices[i] == chosen[j]; |
||||
} |
||||
if (included) { |
||||
bytes memory requiredMetadata = TestIsm(choices[i]) |
||||
.requiredMetadata(); |
||||
uint32 end = start + uint32(requiredMetadata.length); |
||||
uint64 offset = (uint64(start) << 32) | uint64(end); |
||||
offsets = bytes.concat(offsets, abi.encodePacked(offset)); |
||||
start = end; |
||||
metametadata = abi.encodePacked(metametadata, requiredMetadata); |
||||
} else { |
||||
uint64 offset = 0; |
||||
offsets = bytes.concat(offsets, abi.encodePacked(offset)); |
||||
} |
||||
} |
||||
return abi.encodePacked(offsets, metametadata); |
||||
} |
||||
|
||||
function testVerify( |
||||
uint8 m, |
||||
uint8 n, |
||||
bytes32 seed |
||||
) public { |
||||
vm.assume(0 < m && m <= n && n < 10); |
||||
deployIsms(m, n, seed); |
||||
|
||||
bytes memory metadata = getMetadata(m, seed); |
||||
assertTrue(ism.verify(metadata, "")); |
||||
} |
||||
|
||||
function testVerifyNoMetadataRequired( |
||||
uint8 m, |
||||
uint8 n, |
||||
uint8 i, |
||||
bytes32 seed |
||||
) public { |
||||
vm.assume(0 < m && m <= n && n < 10 && i < n); |
||||
deployIsms(m, n, seed); |
||||
(address[] memory modules, ) = ism.modulesAndThreshold(""); |
||||
bytes memory noMetadata; |
||||
TestIsm(modules[i]).setRequiredMetadata(noMetadata); |
||||
|
||||
bytes memory metadata = getMetadata(m, seed); |
||||
assertTrue(ism.verify(metadata, "")); |
||||
} |
||||
|
||||
function testVerifyMissingMetadata( |
||||
uint8 m, |
||||
uint8 n, |
||||
bytes32 seed |
||||
) public { |
||||
vm.assume(0 < m && m <= n && n < 10); |
||||
deployIsms(m, n, seed); |
||||
|
||||
// Populate metadata for one fewer ISMs than needed. |
||||
bytes memory metadata = getMetadata(m - 1, seed); |
||||
vm.expectRevert(bytes("!threshold")); |
||||
ism.verify(metadata, ""); |
||||
} |
||||
|
||||
function testVerifyIncorrectMetadata( |
||||
uint8 m, |
||||
uint8 n, |
||||
bytes32 seed |
||||
) public { |
||||
vm.assume(0 < m && m <= n && n < 10); |
||||
deployIsms(m, n, seed); |
||||
|
||||
bytes memory metadata = getMetadata(m, seed); |
||||
// Modify the last byte in metadata. This should affect |
||||
// the content of the metadata passed to the last ISM. |
||||
metadata[metadata.length - 1] = ~metadata[metadata.length - 1]; |
||||
vm.expectRevert(bytes("!verify")); |
||||
ism.verify(metadata, ""); |
||||
} |
||||
|
||||
function testModulesAndThreshold( |
||||
uint8 m, |
||||
uint8 n, |
||||
bytes32 seed |
||||
) public { |
||||
vm.assume(0 < m && m <= n && n < 10); |
||||
address[] memory expectedIsms = deployIsms(m, n, seed); |
||||
(address[] memory actualIsms, uint8 actualThreshold) = ism |
||||
.modulesAndThreshold(""); |
||||
assertEq(abi.encode(actualIsms), abi.encode(expectedIsms)); |
||||
assertEq(actualThreshold, m); |
||||
} |
||||
} |
@ -0,0 +1,59 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
library MOfNTestUtils { |
||||
function choose( |
||||
uint8 m, |
||||
uint256[] memory choices, |
||||
bytes32 seed |
||||
) internal pure returns (uint256[] memory) { |
||||
uint256 bitmask = _bitmask(m, uint8(choices.length), seed); |
||||
uint256[] memory ret = new uint256[](m); |
||||
uint256 j = 0; |
||||
for (uint256 i = 0; i < choices.length; i++) { |
||||
bool chosen = (bitmask & (1 << i)) > 0; |
||||
if (chosen) { |
||||
ret[j] = choices[i]; |
||||
j += 1; |
||||
} |
||||
} |
||||
return ret; |
||||
} |
||||
|
||||
function choose( |
||||
uint8 m, |
||||
address[] memory choices, |
||||
bytes32 seed |
||||
) internal pure returns (address[] memory) { |
||||
uint256 bitmask = _bitmask(m, uint8(choices.length), seed); |
||||
address[] memory ret = new address[](m); |
||||
uint256 j = 0; |
||||
for (uint256 i = 0; i < choices.length; i++) { |
||||
bool chosen = (bitmask & (1 << i)) > 0; |
||||
if (chosen) { |
||||
ret[j] = choices[i]; |
||||
j += 1; |
||||
} |
||||
} |
||||
return ret; |
||||
} |
||||
|
||||
function _bitmask( |
||||
uint8 m, |
||||
uint8 n, |
||||
bytes32 seed |
||||
) private pure returns (uint256) { |
||||
uint8 chosen = 0; |
||||
uint256 bitmask = 0; |
||||
bytes32 randomness = seed; |
||||
while (chosen < m) { |
||||
randomness = keccak256(abi.encodePacked(randomness)); |
||||
uint256 choice = (1 << (uint256(randomness) % n)); |
||||
if ((bitmask & choice) == 0) { |
||||
bitmask = bitmask | choice; |
||||
chosen += 1; |
||||
} |
||||
} |
||||
return bitmask; |
||||
} |
||||
} |
@ -0,0 +1,106 @@ |
||||
// SPDX-License-Identifier: Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
|
||||
import "forge-std/Test.sol"; |
||||
|
||||
import {IMultisigIsm} from "../../interfaces/isms/IMultisigIsm.sol"; |
||||
import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; |
||||
import {StaticMultisigIsmFactory} from "../../contracts/isms/multisig/StaticMultisigIsmFactory.sol"; |
||||
import {CheckpointLib} from "../../contracts/libs/CheckpointLib.sol"; |
||||
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; |
||||
import {Message} from "../../contracts/libs/Message.sol"; |
||||
import {MOfNTestUtils} from "./MOfNTestUtils.sol"; |
||||
|
||||
contract MultisigIsmTest is Test { |
||||
uint32 constant ORIGIN = 11; |
||||
StaticMultisigIsmFactory factory; |
||||
IMultisigIsm ism; |
||||
TestMailbox mailbox; |
||||
|
||||
function setUp() public { |
||||
mailbox = new TestMailbox(ORIGIN); |
||||
factory = new StaticMultisigIsmFactory(); |
||||
} |
||||
|
||||
function addValidators( |
||||
uint8 m, |
||||
uint8 n, |
||||
bytes32 seed |
||||
) private returns (uint256[] memory) { |
||||
uint256[] memory keys = new uint256[](n); |
||||
address[] memory addresses = new address[](n); |
||||
for (uint256 i = 0; i < n; i++) { |
||||
uint256 key = uint256(keccak256(abi.encode(seed, i))); |
||||
keys[i] = key; |
||||
addresses[i] = vm.addr(key); |
||||
} |
||||
ism = IMultisigIsm(factory.deploy(addresses, m)); |
||||
return keys; |
||||
} |
||||
|
||||
function getMessage( |
||||
uint32 destination, |
||||
bytes32 recipient, |
||||
bytes calldata body |
||||
) internal returns (bytes memory) { |
||||
uint8 version = mailbox.VERSION(); |
||||
uint32 origin = mailbox.localDomain(); |
||||
bytes32 sender = TypeCasts.addressToBytes32(address(this)); |
||||
uint32 nonce = mailbox.count(); |
||||
mailbox.dispatch(destination, recipient, body); |
||||
bytes memory message = Message.formatMessage( |
||||
version, |
||||
nonce, |
||||
origin, |
||||
sender, |
||||
destination, |
||||
recipient, |
||||
body |
||||
); |
||||
return message; |
||||
} |
||||
|
||||
function getMetadata( |
||||
uint8 m, |
||||
uint8 n, |
||||
bytes32 seed |
||||
) private returns (bytes memory) { |
||||
uint32 domain = mailbox.localDomain(); |
||||
uint256[] memory keys = addValidators(m, n, seed); |
||||
uint256[] memory signers = MOfNTestUtils.choose(m, keys, seed); |
||||
bytes32 mailboxAsBytes32 = TypeCasts.addressToBytes32(address(mailbox)); |
||||
bytes32 checkpointRoot = mailbox.root(); |
||||
uint32 checkpointIndex = uint32(mailbox.count() - 1); |
||||
bytes memory metadata = abi.encodePacked( |
||||
checkpointRoot, |
||||
checkpointIndex, |
||||
mailboxAsBytes32, |
||||
mailbox.proof() |
||||
); |
||||
bytes32 digest = CheckpointLib.digest( |
||||
domain, |
||||
mailboxAsBytes32, |
||||
checkpointRoot, |
||||
checkpointIndex |
||||
); |
||||
for (uint256 i = 0; i < signers.length; i++) { |
||||
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signers[i], digest); |
||||
metadata = abi.encodePacked(metadata, r, s, v); |
||||
} |
||||
return metadata; |
||||
} |
||||
|
||||
function testVerify( |
||||
uint32 destination, |
||||
bytes32 recipient, |
||||
bytes calldata body, |
||||
uint8 m, |
||||
uint8 n, |
||||
bytes32 seed |
||||
) public { |
||||
vm.assume(0 < m && m <= n && n < 10); |
||||
bytes memory message = getMessage(destination, recipient, body); |
||||
bytes memory metadata = getMetadata(m, n, seed); |
||||
assertTrue(ism.verify(metadata, message)); |
||||
} |
||||
} |
Loading…
Reference in new issue