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 Tests
trevor/fallback-igp
Asa Oines 2 years ago committed by GitHub
parent 5c6d70dfc4
commit e8d90775f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 20
      solidity/.gas-snapshot
  2. 72
      solidity/contracts/isms/aggregation/AbstractAggregationIsm.sol
  3. 33
      solidity/contracts/isms/aggregation/StaticAggregationIsm.sol
  4. 16
      solidity/contracts/isms/aggregation/StaticAggregationIsmFactory.sol
  5. 124
      solidity/contracts/isms/multisig/AbstractMultisigIsm.sol
  6. 79
      solidity/contracts/isms/multisig/LegacyMultisigIsm.sol
  7. 33
      solidity/contracts/isms/multisig/StaticMultisigIsm.sol
  8. 16
      solidity/contracts/isms/multisig/StaticMultisigIsmFactory.sol
  9. 54
      solidity/contracts/libs/CheckpointLib.sol
  10. 40
      solidity/contracts/libs/MetaProxy.sol
  11. 98
      solidity/contracts/libs/StaticMOfNAddressSetFactory.sol
  12. 20
      solidity/contracts/libs/TypeCasts.sol
  13. 72
      solidity/contracts/libs/isms/AggregationIsmMetadata.sol
  14. 170
      solidity/contracts/libs/isms/LegacyMultisigIsmMetadata.sol
  15. 85
      solidity/contracts/libs/isms/MultisigIsmMetadata.sol
  16. 16
      solidity/contracts/test/TestLegacyMultisigIsm.sol
  17. 23
      solidity/contracts/test/TestMultisigIsm.sol
  18. 7
      solidity/coverage.sh
  19. 19
      solidity/interfaces/IAggregationIsm.sol
  20. 8
      solidity/interfaces/IInterchainSecurityModule.sol
  21. 8
      solidity/slither.config.json
  22. 156
      solidity/test/isms/AggregationIsm.t.sol
  23. 59
      solidity/test/isms/MOfNTestUtils.sol
  24. 106
      solidity/test/isms/MultisigIsm.t.sol
  25. 24
      solidity/test/isms/legacyMultisigIsm.test.ts
  26. 6
      solidity/test/lib/mailboxes.ts
  27. 6
      typescript/sdk/src/core/HyperlaneCoreDeployer.ts
  28. 6
      typescript/sdk/src/core/TestCoreDeployer.ts
  29. 8
      typescript/sdk/src/core/contracts.ts
  30. 4
      typescript/sdk/src/core/types.ts
  31. 4
      typescript/sdk/src/gas/types.ts
  32. 2
      typescript/utils/src/types.ts
  33. 10
      typescript/utils/src/utils.ts

@ -1,20 +1,20 @@
GasRouterTest:testDispatchWithGas(uint256) (runs: 256, μ: 413789, ~: 413789)
GasRouterTest:testQuoteGasPayment(uint256) (runs: 256, μ: 85818, ~: 85818)
GasRouterTest:testSetDestinationGas(uint256) (runs: 256, μ: 73808, ~: 75985)
InterchainAccountRouterTest:testCallRemoteWithDefault(bytes32) (runs: 256, μ: 595270, ~: 595504)
InterchainAccountRouterTest:testCallRemoteWithOverrides(bytes32) (runs: 256, μ: 500063, ~: 500297)
InterchainAccountRouterTest:testCallRemoteWithDefault(bytes32) (runs: 256, μ: 595193, ~: 595504)
InterchainAccountRouterTest:testCallRemoteWithOverrides(bytes32) (runs: 256, μ: 499986, ~: 500297)
InterchainAccountRouterTest:testCallRemoteWithoutDefaults(bytes32) (runs: 256, μ: 20408, ~: 20408)
InterchainAccountRouterTest:testConstructor() (gas: 2577062)
InterchainAccountRouterTest:testEnrollRemoteRouterAndIsm(bytes32,bytes32) (runs: 256, μ: 109207, ~: 109207)
InterchainAccountRouterTest:testEnrollRemoteRouterAndIsmImmutable(bytes32,bytes32,bytes32,bytes32) (runs: 256, μ: 106970, ~: 106970)
InterchainAccountRouterTest:testEnrollRemoteRouterAndIsmNonOwner(address,bytes32,bytes32) (runs: 256, μ: 20291, ~: 20291)
InterchainAccountRouterTest:testEnrollRemoteRouters(uint8,uint32,bytes32) (runs: 256, μ: 3827029, ~: 3234498)
InterchainAccountRouterTest:testGetLocalInterchainAccount(bytes32) (runs: 256, μ: 508382, ~: 508616)
InterchainAccountRouterTest:testEnrollRemoteRouters(uint8,uint32,bytes32) (runs: 256, μ: 4022298, ~: 3525910)
InterchainAccountRouterTest:testGetLocalInterchainAccount(bytes32) (runs: 256, μ: 508305, ~: 508616)
InterchainAccountRouterTest:testGetRemoteInterchainAccount() (gas: 120231)
InterchainAccountRouterTest:testOverrideAndCallRemote(bytes32) (runs: 256, μ: 595293, ~: 595527)
InterchainAccountRouterTest:testOverrideAndCallRemote(bytes32) (runs: 256, μ: 595216, ~: 595527)
InterchainAccountRouterTest:testReceiveValue(uint256) (runs: 256, μ: 109672, ~: 109672)
InterchainAccountRouterTest:testSendValue(uint256) (runs: 256, μ: 524292, ~: 524292)
InterchainAccountRouterTest:testSingleCallRemoteWithDefault(bytes32) (runs: 256, μ: 595986, ~: 596220)
InterchainAccountRouterTest:testSingleCallRemoteWithDefault(bytes32) (runs: 256, μ: 595909, ~: 596220)
InterchainQueryRouterTest:testCannotCallbackReverting() (gas: 1450639)
InterchainQueryRouterTest:testCannotQueryReverting() (gas: 1118311)
InterchainQueryRouterTest:testQueryAddress(address) (runs: 256, μ: 1481496, ~: 1481496)
@ -29,7 +29,7 @@ LiquidityLayerRouterTest:testDispatchWithTokensTransfersOnDestination() (gas: 78
LiquidityLayerRouterTest:testProcessingRevertsIfBridgeAdapterReverts() (gas: 596435)
LiquidityLayerRouterTest:testSendToRecipientWithoutHandleWhenSpecifyingNoMessage() (gas: 1197693)
LiquidityLayerRouterTest:testSetLiquidityLayerAdapter() (gas: 23363)
MessagingTest:testSendMessage(string) (runs: 256, μ: 277791, ~: 296095)
MessagingTest:testSendMessage(string) (runs: 256, μ: 276902, ~: 296095)
PausableReentrancyGuardTest:testNonreentrant() (gas: 9628)
PausableReentrancyGuardTest:testNonreentrantNotPaused() (gas: 14163)
PausableReentrancyGuardTest:testPause() (gas: 13635)
@ -63,6 +63,12 @@ StorageGasOracleTest:testSetRemoteGasData() (gas: 38836)
StorageGasOracleTest:testSetRemoteGasDataConfigs() (gas: 69238)
StorageGasOracleTest:testSetRemoteGasDataConfigsRevertsIfNotOwner() (gas: 12227)
StorageGasOracleTest:testSetRemoteGasDataRevertsIfNotOwner() (gas: 11275)
AggregationIsmTest:testModulesAndThreshold(uint8,uint8,bytes32) (runs: 256, μ: 1537712, ~: 1314952)
AggregationIsmTest:testVerify(uint8,uint8,bytes32) (runs: 256, μ: 1562217, ~: 1339075)
AggregationIsmTest:testVerifyIncorrectMetadata(uint8,uint8,bytes32) (runs: 256, μ: 1562134, ~: 1339762)
AggregationIsmTest:testVerifyMissingMetadata(uint8,uint8,bytes32) (runs: 256, μ: 1554932, ~: 1332121)
AggregationIsmTest:testVerifyNoMetadataRequired(uint8,uint8,uint8,bytes32) (runs: 256, μ: 1705339, ~: 1305241)
MultisigIsmTest:testVerify(uint32,bytes32,bytes,uint8,uint8,bytes32) (runs: 256, μ: 335682, ~: 327788)
PortalAdapterTest:testAdapter(uint256) (runs: 256, μ: 135467, ~: 135583)
PortalAdapterTest:testReceivingRevertsWithoutTransferCompletion(uint256) (runs: 256, μ: 140406, ~: 140522)
PortalAdapterTest:testReceivingWorks(uint256) (runs: 256, μ: 229403, ~: 229520)

@ -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;
}
}

@ -7,27 +7,30 @@ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
// ============ Internal Imports ============
import {IMultisigIsm} from "../../interfaces/isms/IMultisigIsm.sol";
import {Message} from "../libs/Message.sol";
import {MultisigIsmMetadata} from "../libs/isms/MultisigIsmMetadata.sol";
import {MerkleLib} from "../libs/Merkle.sol";
import {IInterchainSecurityModule} from "../../../interfaces/IInterchainSecurityModule.sol";
import {Message} from "../../libs/Message.sol";
import {IMultisigIsm} from "../../../interfaces/isms/IMultisigIsm.sol";
import {LegacyMultisigIsmMetadata} from "../../libs/isms/LegacyMultisigIsmMetadata.sol";
import {MerkleLib} from "../../libs/Merkle.sol";
import {CheckpointLib} from "../../libs/CheckpointLib.sol";
/**
* @title MultisigIsm
* @notice Manages an ownable set of validators that ECDSA sign checkpoints to
* reach a quorum.
*/
contract MultisigIsm is IMultisigIsm, Ownable {
contract LegacyMultisigIsm is IMultisigIsm, Ownable {
// ============ Libraries ============
using EnumerableSet for EnumerableSet.AddressSet;
using Message for bytes;
using MultisigIsmMetadata for bytes;
using LegacyMultisigIsmMetadata for bytes;
using MerkleLib for MerkleLib.Tree;
// ============ Constants ============
uint8 public constant moduleType = 3;
uint8 public constant moduleType =
uint8(IInterchainSecurityModule.Types.LEGACY_MULTISIG);
// ============ Mutable Storage ============
@ -195,7 +198,7 @@ contract MultisigIsm is IMultisigIsm, Ownable {
* @notice Verifies that a quorum of the origin domain's validators signed
* a checkpoint, and verifies the merkle proof of `_message` against that
* checkpoint.
* @param _metadata ABI encoded module metadata (see MultisigIsmMetadata.sol)
* @param _metadata ABI encoded module metadata (see LegacyMultisigIsmMetadata.sol)
* @param _message Formatted Hyperlane message (see Message.sol).
*/
function verify(bytes calldata _metadata, bytes calldata _message)
@ -284,7 +287,7 @@ contract MultisigIsm is IMultisigIsm, Ownable {
/**
* @notice Verifies the merkle proof of `_message` against the provided
* checkpoint.
* @param _metadata ABI encoded module metadata (see MultisigIsmMetadata.sol)
* @param _metadata ABI encoded module metadata (see LegacyMultisigIsmMetadata.sol)
* @param _message Formatted Hyperlane message (see Message.sol).
*/
function _verifyMerkleProof(
@ -303,7 +306,7 @@ contract MultisigIsm is IMultisigIsm, Ownable {
/**
* @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 _metadata ABI encoded module metadata (see LegacyMultisigIsmMetadata.sol)
* @param _message Formatted Hyperlane message (see Message.sol).
*/
function _verifyValidatorSignatures(
@ -324,7 +327,12 @@ contract MultisigIsm is IMultisigIsm, Ownable {
// non-zero computed commitment, and this check will fail
// as the commitment in storage will be zero.
require(_commitment == commitment[_origin], "!commitment");
_digest = _getCheckpointDigest(_metadata, _origin);
_digest = CheckpointLib.digest(
_origin,
LegacyMultisigIsmMetadata.originMailbox(_metadata),
LegacyMultisigIsmMetadata.root(_metadata),
LegacyMultisigIsmMetadata.index(_metadata)
);
}
uint256 _validatorCount = _metadata.validatorCount();
uint256 _validatorIndex = 0;
@ -344,53 +352,4 @@ contract MultisigIsm is IMultisigIsm, Ownable {
}
return true;
}
/**
* @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 _getDomainHash(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"));
}
/**
* @notice Returns the digest validators are expected to sign when signing checkpoints.
* @param _metadata ABI encoded module metadata (see MultisigIsmMetadata.sol)
* @param _origin The origin domain of the checkpoint.
* @return The digest of the checkpoint.
*/
function _getCheckpointDigest(bytes calldata _metadata, uint32 _origin)
internal
pure
returns (bytes32)
{
bytes32 _domainHash = _getDomainHash(
_origin,
_metadata.originMailbox()
);
return
ECDSA.toEthSignedMessageHash(
keccak256(
abi.encodePacked(
_domainHash,
_metadata.root(),
_metadata.index()
)
)
);
}
}

@ -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);
}
}

@ -2,26 +2,6 @@
pragma solidity >=0.6.11;
library TypeCasts {
// treat it as a null-terminated string of max 32 bytes
function coerceString(bytes32 _buf)
internal
pure
returns (string memory _newStr)
{
uint8 _slen = 0;
while (_slen < 32 && _buf[_slen] != 0) {
_slen++;
}
// solhint-disable-next-line no-inline-assembly
assembly {
_newStr := mload(0x40)
mstore(0x40, add(_newStr, 0x40)) // may end up with extra
mstore(_newStr, _slen)
mstore(add(_newStr, 0x20), _buf)
}
}
// alignment preserving cast
function addressToBytes32(address _addr) internal pure returns (bytes32) {
return bytes32(uint256(uint160(_addr)));

@ -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);
}
}

@ -7,17 +7,14 @@ pragma solidity >=0.8.0;
* [ 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
* [1092:????] Validator signatures, 65 bytes each, length == Threshold
*/
library MultisigIsmMetadata {
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 SIGNATURES_OFFSET = 1092;
uint256 private constant SIGNATURE_LENGTH = 65;
/**
@ -68,21 +65,11 @@ library MultisigIsmMetadata {
{
return
abi.decode(
_metadata[MERKLE_PROOF_OFFSET:THRESHOLD_OFFSET],
_metadata[MERKLE_PROOF_OFFSET:SIGNATURES_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
@ -101,70 +88,4 @@ library MultisigIsmMetadata {
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);
}
}

@ -12,4 +12,9 @@ rm -rf coverage && \
# Remove files we don't care about covering
lcov --remove lcov.info -o lcov.info 'contracts/test/**' 'contracts/mock/**' '**/node_modules/**' 'test/*' && \
# Print output
lcov --list lcov.info
lcov --list lcov.info && \
# Open more granular breakdown in browser
if [ "$CI" != "true" ]
then
genhtml -o coverage lcov.info && open coverage/index.html
fi

@ -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);
}

@ -2,6 +2,14 @@
pragma solidity >=0.6.11;
interface IInterchainSecurityModule {
enum Types {
UNUSED_0,
ROUTING,
AGGREGATION,
LEGACY_MULTISIG,
MULTISIG
}
/**
* @notice Returns an enum that represents the type of security model
* encoded by this ISM.

@ -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));
}
}

@ -7,10 +7,10 @@ import { Validator, types, utils } from '@hyperlane-xyz/utils';
import {
LightTestRecipient__factory,
TestLegacyMultisigIsm,
TestLegacyMultisigIsm__factory,
TestMailbox,
TestMailbox__factory,
TestMultisigIsm,
TestMultisigIsm__factory,
TestRecipient__factory,
} from '../../types';
import {
@ -26,8 +26,8 @@ const DESTINATION_DOMAIN = 4321;
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
const domainHashTestCases = require('../../../vectors/domainHash.json');
describe('MultisigIsm', async () => {
let multisigIsm: TestMultisigIsm,
describe('LegacyMultisigIsm', async () => {
let multisigIsm: TestLegacyMultisigIsm,
mailbox: TestMailbox,
signer: SignerWithAddress,
nonOwner: SignerWithAddress,
@ -46,7 +46,7 @@ describe('MultisigIsm', async () => {
});
beforeEach(async () => {
const multisigIsmFactory = new TestMultisigIsm__factory(signer);
const multisigIsmFactory = new TestLegacyMultisigIsm__factory(signer);
multisigIsm = await multisigIsmFactory.deploy();
});
@ -412,7 +412,7 @@ describe('MultisigIsm', async () => {
ORIGIN_DOMAIN,
mailbox.address,
);
const parsedMetadata = utils.parseMultisigIsmMetadata(metadata);
const parsedMetadata = utils.parseLegacyMultisigIsmMetadata(metadata);
const nonValidatorSignature = (
await signCheckpoint(
parsedMetadata.checkpointRoot,
@ -422,7 +422,7 @@ describe('MultisigIsm', async () => {
)
)[0];
parsedMetadata.signatures.push(nonValidatorSignature);
const modifiedMetadata = utils.formatMultisigIsmMetadata({
const modifiedMetadata = utils.formatLegacyMultisigIsmMetadata({
...parsedMetadata,
signatures: parsedMetadata.signatures.slice(1),
});
@ -432,8 +432,8 @@ describe('MultisigIsm', async () => {
});
it('reverts when the provided validator set does not match the stored commitment', async () => {
const parsedMetadata = utils.parseMultisigIsmMetadata(metadata);
const modifiedMetadata = utils.formatMultisigIsmMetadata({
const parsedMetadata = utils.parseLegacyMultisigIsmMetadata(metadata);
const modifiedMetadata = utils.formatLegacyMultisigIsmMetadata({
...parsedMetadata,
validators: parsedMetadata.validators.slice(1),
});
@ -443,8 +443,8 @@ describe('MultisigIsm', async () => {
});
it('reverts when an invalid merkle proof is provided', async () => {
const parsedMetadata = utils.parseMultisigIsmMetadata(metadata);
const modifiedMetadata = utils.formatMultisigIsmMetadata({
const parsedMetadata = utils.parseLegacyMultisigIsmMetadata(metadata);
const modifiedMetadata = utils.formatLegacyMultisigIsmMetadata({
...parsedMetadata,
proof: parsedMetadata.proof.reverse(),
});
@ -478,7 +478,7 @@ describe('MultisigIsm', async () => {
// hash for local domain of 1000)
for (const testCase of domainHashTestCases) {
const { expectedDomainHash } = testCase;
// This public function on TestMultisigIsm exposes
// This public function on TestLegacyMultisigIsm exposes
// the internal _domainHash on MultisigIsm.
const domainHash = await multisigIsm.getDomainHash(
testCase.originDomain,

@ -3,7 +3,7 @@ import { ethers } from 'ethers';
import { Validator, types, utils } from '@hyperlane-xyz/utils';
import { MultisigIsm, TestMailbox } from '../../types';
import { LegacyMultisigIsm, TestMailbox } from '../../types';
import { DispatchEvent } from '../../types/contracts/Mailbox';
export type MessageAndProof = {
@ -79,7 +79,7 @@ export async function signCheckpoint(
export async function dispatchMessageAndReturnMetadata(
mailbox: TestMailbox,
multisigIsm: MultisigIsm,
multisigIsm: LegacyMultisigIsm,
destination: number,
recipient: string,
messageStr: string,
@ -105,7 +105,7 @@ export async function dispatchMessageAndReturnMetadata(
orderedValidators,
);
const origin = utils.parseMessage(proofAndMessage.message).origin;
const metadata = utils.formatMultisigIsmMetadata({
const metadata = utils.formatLegacyMultisigIsmMetadata({
checkpointRoot: root,
checkpointIndex: index,
originMailbox: mailbox.address,

@ -3,8 +3,8 @@ import { ethers } from 'ethers';
import {
InterchainGasPaymaster,
LegacyMultisigIsm,
Mailbox,
MultisigIsm,
OverheadIgp,
Ownable,
Ownable__factory,
@ -222,7 +222,7 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer<
return validatorAnnounce;
}
async deployMultisigIsm(chain: ChainName): Promise<MultisigIsm> {
async deployLegacyMultisigIsm(chain: ChainName): Promise<LegacyMultisigIsm> {
const multisigIsm = await this.deployContract(chain, 'multisigIsm', []);
const configChains = Object.keys(this.configMap);
const remotes = this.multiProvider
@ -307,7 +307,7 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer<
const provider = this.multiProvider.getProvider(chain);
const startingBlockNumber = await provider.getBlockNumber();
this.startingBlockNumbers[chain] = startingBlockNumber;
const multisigIsm = await this.deployMultisigIsm(chain);
const multisigIsm = await this.deployLegacyMultisigIsm(chain);
const proxyAdmin = await this.deployContract(chain, 'proxyAdmin', []);

@ -1,7 +1,7 @@
import { ethers } from 'ethers';
import {
MultisigIsm,
LegacyMultisigIsm,
TestInterchainGasPaymaster__factory,
TestIsm__factory,
TestMailbox__factory,
@ -57,7 +57,7 @@ export class TestCoreDeployer extends HyperlaneCoreDeployer {
}
// deploy a test ISM in place of a multisig ISM
async deployMultisigIsm(chain: ChainName): Promise<MultisigIsm> {
async deployLegacyMultisigIsm(chain: ChainName): Promise<LegacyMultisigIsm> {
const testIsm = await this.deployContractFromFactory(
chain,
testCoreFactories.testIsm,
@ -65,7 +65,7 @@ export class TestCoreDeployer extends HyperlaneCoreDeployer {
[],
);
await testIsm.setAccept(true);
return testIsm as unknown as MultisigIsm;
return testIsm as unknown as LegacyMultisigIsm;
}
// TestIsm is not ownable, so we skip ownership transfer

@ -4,10 +4,10 @@ import {
InterchainGasPaymaster,
InterchainGasPaymaster__factory,
InterchainQueryRouter__factory,
LegacyMultisigIsm,
LegacyMultisigIsm__factory,
Mailbox,
Mailbox__factory,
MultisigIsm,
MultisigIsm__factory,
OverheadIgp,
OverheadIgp__factory,
ProxyAdmin,
@ -35,7 +35,7 @@ export type ConnectionClientContracts = {
export type CoreContracts = GasOracleContracts &
ConnectionClientContracts & {
mailbox: ProxiedContract<Mailbox, TransparentProxyAddresses>;
multisigIsm: MultisigIsm;
multisigIsm: LegacyMultisigIsm;
proxyAdmin: ProxyAdmin;
validatorAnnounce: ValidatorAnnounce;
};
@ -49,6 +49,6 @@ export const coreFactories = {
interchainGasPaymaster: new InterchainGasPaymaster__factory(),
defaultIsmInterchainGasPaymaster: new OverheadIgp__factory(),
storageGasOracle: new StorageGasOracle__factory(),
multisigIsm: new MultisigIsm__factory(),
multisigIsm: new LegacyMultisigIsm__factory(),
mailbox: new Mailbox__factory(),
};

@ -2,8 +2,8 @@ import { BigNumber } from 'ethers';
import {
InterchainGasPaymaster,
LegacyMultisigIsm,
Mailbox,
MultisigIsm,
OverheadIgp,
} from '@hyperlane-xyz/core';
import type { types } from '@hyperlane-xyz/utils';
@ -72,7 +72,7 @@ export interface MailboxMultisigIsmViolation extends MailboxViolation {
export interface MultisigIsmViolation extends CheckerViolation {
type: CoreViolationType.MultisigIsm;
contract: MultisigIsm;
contract: LegacyMultisigIsm;
subType: MultisigIsmViolationType;
remote: ChainName;
}

@ -1,4 +1,4 @@
import { Mailbox, MultisigIsm } from '@hyperlane-xyz/core';
import { LegacyMultisigIsm, Mailbox } from '@hyperlane-xyz/core';
import type { types } from '@hyperlane-xyz/utils';
import type { CheckerViolation } from '../deploy/types';
@ -44,7 +44,7 @@ export interface MailboxMultisigIsmViolation extends MailboxViolation {
export interface MultisigIsmViolation extends CheckerViolation {
type: CoreViolationType.MultisigIsm;
contract: MultisigIsm;
contract: LegacyMultisigIsm;
subType: MultisigIsmViolationType;
remote: ChainName;
}

@ -50,7 +50,7 @@ export type ParsedMessage = {
body: string;
};
export type ParsedMultisigIsmMetadata = {
export type ParsedLegacyMultisigIsmMetadata = {
checkpointRoot: string;
checkpointIndex: number;
originMailbox: string;

@ -5,8 +5,8 @@ import {
Checkpoint,
Domain,
HexString,
ParsedLegacyMultisigIsmMetadata,
ParsedMessage,
ParsedMultisigIsmMetadata,
} from './types';
export function assert(predicate: any, errorMessage?: string) {
@ -49,9 +49,9 @@ export function formatCallData<
);
}
export const parseMultisigIsmMetadata = (
export const parseLegacyMultisigIsmMetadata = (
metadata: string,
): ParsedMultisigIsmMetadata => {
): ParsedLegacyMultisigIsmMetadata => {
const MERKLE_ROOT_OFFSET = 0;
const MERKLE_INDEX_OFFSET = 32;
const ORIGIN_MAILBOX_OFFSET = 36;
@ -97,8 +97,8 @@ export const parseMultisigIsmMetadata = (
};
};
export const formatMultisigIsmMetadata = (
metadata: ParsedMultisigIsmMetadata,
export const formatLegacyMultisigIsmMetadata = (
metadata: ParsedLegacyMultisigIsmMetadata,
): string => {
return ethers.utils.solidityPack(
[

Loading…
Cancel
Save