feat: add `IStaticWeightedIsm` implementation (#4170)
### Description - Adding the static variation of the IWeightedIsm config where you configure validators with their corresponding weights and store it as bytecode. ### Drive-by changes None ### Related issues - fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4160 ### Backward compatibility Yes ### Testing Fuzz testspull/4263/head
parent
d6ede7816c
commit
d396e2c553
@ -0,0 +1,39 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
|
||||
/*@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@@@@@@@@@@@@@@@@@ |
||||
@@@@@ HYPERLANE @@@@@@@ |
||||
@@@@@@@@@@@@@@@@@@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@*/ |
||||
|
||||
import {IInterchainSecurityModule} from "../IInterchainSecurityModule.sol"; |
||||
|
||||
interface IStaticWeightedMultisigIsm is IInterchainSecurityModule { |
||||
// ============ Structs ============ |
||||
|
||||
// ValidatorInfo contains the signing address and weight of a validator |
||||
struct ValidatorInfo { |
||||
address signingAddress; |
||||
uint96 weight; |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the validators and threshold weight for this ISM. |
||||
* @param _message The message to be verified |
||||
* @return validators The validators and their weights |
||||
* @return thresholdWeight The threshold weight required to pass verification |
||||
*/ |
||||
function validatorsAndThresholdWeight( |
||||
bytes calldata _message |
||||
) |
||||
external |
||||
view |
||||
returns (ValidatorInfo[] memory validators, uint96 thresholdWeight); |
||||
} |
@ -0,0 +1,100 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
/*@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@@@@@@@@@@@@@@@@@ |
||||
@@@@@ HYPERLANE @@@@@@@ |
||||
@@@@@@@@@@@@@@@@@@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@*/ |
||||
|
||||
// ============ External Imports ============ |
||||
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; |
||||
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; |
||||
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; |
||||
// ============ Internal Imports ============ |
||||
import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; |
||||
import {IStaticWeightedMultisigIsm} from "../../interfaces/isms/IWeightedMultisigIsm.sol"; |
||||
import {Message} from "../../libs/Message.sol"; |
||||
|
||||
import {MerkleLib} from "../../libs/Merkle.sol"; |
||||
import {AbstractMultisig} from "./AbstractMultisigIsm.sol"; |
||||
|
||||
/** |
||||
* @title AbstractStaticWeightedMultisigIsm |
||||
* @notice Manages per-domain m-of-n Validator sets with stake weights that are used to verify |
||||
* interchain messages. |
||||
*/ |
||||
abstract contract AbstractStaticWeightedMultisigIsm is |
||||
AbstractMultisig, |
||||
IStaticWeightedMultisigIsm |
||||
{ |
||||
// ============ Constants ============ |
||||
|
||||
// total weight of all validators |
||||
uint96 public constant TOTAL_WEIGHT = 1e10; |
||||
|
||||
/** |
||||
* @inheritdoc IStaticWeightedMultisigIsm |
||||
*/ |
||||
function validatorsAndThresholdWeight( |
||||
bytes calldata /* _message*/ |
||||
) public view virtual returns (ValidatorInfo[] memory, uint96); |
||||
|
||||
/** |
||||
* @inheritdoc IInterchainSecurityModule |
||||
*/ |
||||
function verify( |
||||
bytes calldata _metadata, |
||||
bytes calldata _message |
||||
) public view virtual returns (bool) { |
||||
bytes32 _digest = digest(_metadata, _message); |
||||
( |
||||
ValidatorInfo[] memory _validators, |
||||
uint96 _thresholdWeight |
||||
) = validatorsAndThresholdWeight(_message); |
||||
|
||||
require( |
||||
_thresholdWeight > 0 && _thresholdWeight <= TOTAL_WEIGHT, |
||||
"Invalid threshold weight" |
||||
); |
||||
|
||||
uint256 _validatorCount = Math.min( |
||||
_validators.length, |
||||
signatureCount(_metadata) |
||||
); |
||||
uint256 _validatorIndex = 0; |
||||
uint96 _totalWeight = 0; |
||||
|
||||
// assumes that signatures are ordered by validator |
||||
for ( |
||||
uint256 i = 0; |
||||
_totalWeight < _thresholdWeight && i < _validatorCount; |
||||
++i |
||||
) { |
||||
address _signer = ECDSA.recover(_digest, signatureAt(_metadata, i)); |
||||
// loop through remaining validators until we find a match |
||||
while ( |
||||
_validatorIndex < _validatorCount && |
||||
_signer != _validators[_validatorIndex].signingAddress |
||||
) { |
||||
++_validatorIndex; |
||||
} |
||||
// fail if we never found a match |
||||
require(_validatorIndex < _validatorCount, "Invalid signer"); |
||||
|
||||
// add the weight of the current validator |
||||
_totalWeight += _validators[_validatorIndex].weight; |
||||
} |
||||
require( |
||||
_totalWeight >= _thresholdWeight, |
||||
"Insufficient validator weight" |
||||
); |
||||
return true; |
||||
} |
||||
} |
@ -0,0 +1,67 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
/*@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@@@@@@@@@@@@@@@@@ |
||||
@@@@@ HYPERLANE @@@@@@@ |
||||
@@@@@@@@@@@@@@@@@@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@@ |
||||
@@@@@@@@@ @@@@@@@@*/ |
||||
|
||||
import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; |
||||
import {AbstractMerkleRootMultisigIsm} from "./AbstractMerkleRootMultisigIsm.sol"; |
||||
import {AbstractMessageIdMultisigIsm} from "./AbstractMessageIdMultisigIsm.sol"; |
||||
import {AbstractStaticWeightedMultisigIsm} from "./AbstractWeightedMultisigIsm.sol"; |
||||
import {AbstractMultisigIsm} from "./AbstractMultisigIsm.sol"; |
||||
import {StaticWeightedValidatorSetFactory} from "../../libs/StaticWeightedValidatorSetFactory.sol"; |
||||
import {MetaProxy} from "../../libs/MetaProxy.sol"; |
||||
|
||||
abstract contract AbstractMetaProxyWeightedMultisigIsm is |
||||
AbstractStaticWeightedMultisigIsm |
||||
{ |
||||
/** |
||||
* @inheritdoc AbstractStaticWeightedMultisigIsm |
||||
*/ |
||||
function validatorsAndThresholdWeight( |
||||
bytes calldata /* _message*/ |
||||
) public pure override returns (ValidatorInfo[] memory, uint96) { |
||||
return abi.decode(MetaProxy.metadata(), (ValidatorInfo[], uint96)); |
||||
} |
||||
} |
||||
|
||||
contract StaticMerkleRootWeightedMultisigIsm is |
||||
AbstractMerkleRootMultisigIsm, |
||||
AbstractMetaProxyWeightedMultisigIsm |
||||
{ |
||||
uint8 public constant moduleType = |
||||
uint8(IInterchainSecurityModule.Types.WEIGHT_MERKLE_ROOT_MULTISIG); |
||||
} |
||||
|
||||
contract StaticMessageIdWeightedMultisigIsm is |
||||
AbstractMessageIdMultisigIsm, |
||||
AbstractMetaProxyWeightedMultisigIsm |
||||
{ |
||||
uint8 public constant moduleType = |
||||
uint8(IInterchainSecurityModule.Types.WEIGHT_MESSAGE_ID_MULTISIG); |
||||
} |
||||
|
||||
contract StaticMerkleRootWeightedMultisigIsmFactory is |
||||
StaticWeightedValidatorSetFactory |
||||
{ |
||||
function _deployImplementation() internal override returns (address) { |
||||
return address(new StaticMerkleRootWeightedMultisigIsm()); |
||||
} |
||||
} |
||||
|
||||
contract StaticMessageIdWeightedMultisigIsmFactory is |
||||
StaticWeightedValidatorSetFactory |
||||
{ |
||||
function _deployImplementation() internal override returns (address) { |
||||
return address(new StaticMessageIdWeightedMultisigIsm()); |
||||
} |
||||
} |
@ -0,0 +1,97 @@ |
||||
// 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"; |
||||
import {IStaticWeightedMultisigIsm} from "../interfaces/isms/IWeightedMultisigIsm.sol"; |
||||
|
||||
// ============ Internal Imports ============ |
||||
import {MetaProxy} from "./MetaProxy.sol"; |
||||
|
||||
abstract contract StaticWeightedValidatorSetFactory { |
||||
// ============ Immutables ============ |
||||
address public immutable implementation; |
||||
|
||||
// ============ Constructor ============ |
||||
|
||||
constructor() { |
||||
implementation = _deployImplementation(); |
||||
} |
||||
|
||||
function _deployImplementation() internal virtual returns (address); |
||||
|
||||
/** |
||||
* @notice Deploys a StaticWeightedValidatorSet contract address for the given |
||||
* values |
||||
* @dev Consider sorting addresses to ensure contract reuse |
||||
* @param _validators An array of addresses |
||||
* @param _thresholdWeight The threshold weight value to use |
||||
* @return set The contract address representing this StaticWeightedValidatorSet |
||||
*/ |
||||
function deploy( |
||||
IStaticWeightedMultisigIsm.ValidatorInfo[] calldata _validators, |
||||
uint96 _thresholdWeight |
||||
) public returns (address) { |
||||
(bytes32 _salt, bytes memory _bytecode) = _saltAndBytecode( |
||||
_validators, |
||||
_thresholdWeight |
||||
); |
||||
address _set = _getAddress(_salt, _bytecode); |
||||
if (!Address.isContract(_set)) { |
||||
_set = Create2.deploy(0, _salt, _bytecode); |
||||
} |
||||
return _set; |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the StaticWeightedValidatorSet contract address for the given |
||||
* values |
||||
* @dev Consider sorting addresses to ensure contract reuse |
||||
* @param _validators An array of addresses |
||||
* @param _thresholdWeight The threshold weight value to use |
||||
* @return set The contract address representing this StaticWeightedValidatorSet |
||||
*/ |
||||
function getAddress( |
||||
IStaticWeightedMultisigIsm.ValidatorInfo[] calldata _validators, |
||||
uint96 _thresholdWeight |
||||
) external view returns (address) { |
||||
(bytes32 _salt, bytes memory _bytecode) = _saltAndBytecode( |
||||
_validators, |
||||
_thresholdWeight |
||||
); |
||||
return _getAddress(_salt, _bytecode); |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the StaticWeightedValidatorSet 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 StaticWeightedValidatorSet |
||||
*/ |
||||
function _getAddress( |
||||
bytes32 _salt, |
||||
bytes memory _bytecode |
||||
) internal view returns (address) { |
||||
bytes32 _bytecodeHash = keccak256(_bytecode); |
||||
return Create2.computeAddress(_salt, _bytecodeHash); |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the create2 salt and bytecode for the given values |
||||
* @param _validators An array of addresses |
||||
* @param _thresholdWeight The threshold weight value to use |
||||
* @return _salt The salt used in Create2 |
||||
* @return _bytecode The metaproxy bytecode used in Create2 |
||||
*/ |
||||
function _saltAndBytecode( |
||||
IStaticWeightedMultisigIsm.ValidatorInfo[] calldata _validators, |
||||
uint96 _thresholdWeight |
||||
) internal view returns (bytes32, bytes memory) { |
||||
bytes memory _metadata = abi.encode(_validators, _thresholdWeight); |
||||
bytes memory _bytecode = MetaProxy.bytecode(implementation, _metadata); |
||||
bytes32 _salt = keccak256(_metadata); |
||||
return (_salt, _bytecode); |
||||
} |
||||
} |
@ -0,0 +1,230 @@ |
||||
// SPDX-License-Identifier: Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
|
||||
import {IInterchainSecurityModule} from "../../contracts/interfaces/IInterchainSecurityModule.sol"; |
||||
import {Message} from "../../contracts/libs/Message.sol"; |
||||
import {IStaticWeightedMultisigIsm} from "../../contracts/interfaces/isms/IWeightedMultisigIsm.sol"; |
||||
import {StaticMerkleRootWeightedMultisigIsmFactory, StaticMessageIdWeightedMultisigIsmFactory} from "../../contracts/isms/multisig/WeightedMultisigIsm.sol"; |
||||
|
||||
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; |
||||
import {CheckpointLib} from "../../contracts/libs/CheckpointLib.sol"; |
||||
import {StaticWeightedValidatorSetFactory} from "../../contracts/libs/StaticWeightedValidatorSetFactory.sol"; |
||||
import {AbstractStaticWeightedMultisigIsm} from "../../contracts/isms/multisig/AbstractWeightedMultisigIsm.sol"; |
||||
import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; |
||||
import {TestMerkleTreeHook} from "../../contracts/test/TestMerkleTreeHook.sol"; |
||||
import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; |
||||
import {MessageIdMultisigIsmMetadata} from "../../contracts/isms/libs/MessageIdMultisigIsmMetadata.sol"; |
||||
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; |
||||
|
||||
import {AbstractMultisigIsmTest, MerkleRootMultisigIsmTest, MessageIdMultisigIsmTest} from "./MultisigIsm.t.sol"; |
||||
|
||||
abstract contract AbstractStaticWeightedMultisigIsmTest is |
||||
AbstractMultisigIsmTest |
||||
{ |
||||
using Math for uint256; |
||||
using Message for bytes; |
||||
using TypeCasts for address; |
||||
|
||||
StaticWeightedValidatorSetFactory weightedFactory; |
||||
AbstractStaticWeightedMultisigIsm weightedIsm; |
||||
|
||||
uint96 public constant TOTAL_WEIGHT = 1e10; |
||||
|
||||
function addValidators( |
||||
uint96 threshold, |
||||
uint8 n, |
||||
bytes32 seed |
||||
) |
||||
internal |
||||
returns ( |
||||
uint256[] memory, |
||||
IStaticWeightedMultisigIsm.ValidatorInfo[] memory |
||||
) |
||||
{ |
||||
bound(threshold, 0, TOTAL_WEIGHT); |
||||
uint256[] memory keys = new uint256[](n); |
||||
IStaticWeightedMultisigIsm.ValidatorInfo[] |
||||
memory validators = new IStaticWeightedMultisigIsm.ValidatorInfo[]( |
||||
n |
||||
); |
||||
|
||||
uint256 remainingWeight = TOTAL_WEIGHT; |
||||
for (uint256 i = 0; i < n; i++) { |
||||
uint256 key = uint256(keccak256(abi.encode(seed, i))); |
||||
keys[i] = key; |
||||
validators[i].signingAddress = vm.addr(key); |
||||
|
||||
if (i == n - 1) { |
||||
validators[i].weight = uint96(remainingWeight); |
||||
} else { |
||||
uint256 weight = (uint256( |
||||
keccak256(abi.encode(seed, "weight", i)) |
||||
) % (remainingWeight + 1)); |
||||
validators[i].weight = uint96(weight); |
||||
remainingWeight -= weight; |
||||
} |
||||
} |
||||
|
||||
// ism = IInterchainSecurityModule(deployedIsm); |
||||
ism = IInterchainSecurityModule( |
||||
weightedFactory.deploy(validators, threshold) |
||||
); |
||||
weightedIsm = AbstractStaticWeightedMultisigIsm(address(ism)); |
||||
return (keys, validators); |
||||
} |
||||
|
||||
function getMetadata( |
||||
uint8 m, |
||||
uint8 n, |
||||
bytes32 seed, |
||||
bytes memory message |
||||
) internal virtual override returns (bytes memory) { |
||||
bytes32 digest; |
||||
{ |
||||
uint32 domain = mailbox.localDomain(); |
||||
(bytes32 root, uint32 index) = merkleTreeHook.latestCheckpoint(); |
||||
bytes32 messageId = message.id(); |
||||
|
||||
bytes32 merkleTreeAddress = address(merkleTreeHook) |
||||
.addressToBytes32(); |
||||
digest = CheckpointLib.digest( |
||||
domain, |
||||
merkleTreeAddress, |
||||
root, |
||||
index, |
||||
messageId |
||||
); |
||||
} |
||||
|
||||
uint96 threshold = uint96( |
||||
(uint256(m)).mulDiv(TOTAL_WEIGHT, type(uint8).max) |
||||
); |
||||
|
||||
( |
||||
uint256[] memory keys, |
||||
IStaticWeightedMultisigIsm.ValidatorInfo[] memory allValidators |
||||
) = addValidators(threshold, n, seed); |
||||
|
||||
(, uint96 thresholdWeight) = weightedIsm.validatorsAndThresholdWeight( |
||||
message |
||||
); |
||||
|
||||
bytes memory metadata = metadataPrefix(message); |
||||
fixtureInit(); |
||||
|
||||
uint96 totalWeight = 0; |
||||
uint256 signerCount = 0; |
||||
|
||||
while ( |
||||
totalWeight < thresholdWeight && signerCount < allValidators.length |
||||
) { |
||||
(uint8 v, bytes32 r, bytes32 s) = vm.sign( |
||||
keys[signerCount], |
||||
digest |
||||
); |
||||
|
||||
metadata = abi.encodePacked(metadata, r, s, v); |
||||
|
||||
fixtureAppendSignature(signerCount, v, r, s); |
||||
|
||||
totalWeight += allValidators[signerCount].weight; |
||||
signerCount++; |
||||
} |
||||
|
||||
writeFixture(metadata, uint8(signerCount), n); |
||||
|
||||
return metadata; |
||||
} |
||||
|
||||
function testVerify_revertInsufficientWeight( |
||||
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, message); |
||||
|
||||
uint256 signatureCount = weightedIsm.signatureCount(metadata); |
||||
vm.assume(signatureCount >= 1); |
||||
|
||||
uint256 newLength = metadata.length - 65; |
||||
bytes memory insufficientMetadata = new bytes(newLength); |
||||
|
||||
for (uint256 i = 0; i < newLength; i++) { |
||||
insufficientMetadata[i] = metadata[i]; |
||||
} |
||||
|
||||
vm.expectRevert("Insufficient validator weight"); |
||||
ism.verify(insufficientMetadata, message); |
||||
} |
||||
} |
||||
|
||||
contract StaticMerkleRootWeightedMultisigIsmTest is |
||||
MerkleRootMultisigIsmTest, |
||||
AbstractStaticWeightedMultisigIsmTest |
||||
{ |
||||
function setUp() public override { |
||||
mailbox = new TestMailbox(ORIGIN); |
||||
merkleTreeHook = new TestMerkleTreeHook(address(mailbox)); |
||||
noopHook = new TestPostDispatchHook(); |
||||
weightedFactory = new StaticMerkleRootWeightedMultisigIsmFactory(); |
||||
mailbox.setDefaultHook(address(merkleTreeHook)); |
||||
mailbox.setRequiredHook(address(noopHook)); |
||||
} |
||||
|
||||
function getMetadata( |
||||
uint8 m, |
||||
uint8 n, |
||||
bytes32 seed, |
||||
bytes memory message |
||||
) |
||||
internal |
||||
override(AbstractMultisigIsmTest, AbstractStaticWeightedMultisigIsmTest) |
||||
returns (bytes memory) |
||||
{ |
||||
return |
||||
AbstractStaticWeightedMultisigIsmTest.getMetadata( |
||||
m, |
||||
n, |
||||
seed, |
||||
message |
||||
); |
||||
} |
||||
} |
||||
|
||||
contract StaticMessageIdWeightedMultisigIsmTest is |
||||
MessageIdMultisigIsmTest, |
||||
AbstractStaticWeightedMultisigIsmTest |
||||
{ |
||||
function setUp() public override { |
||||
mailbox = new TestMailbox(ORIGIN); |
||||
merkleTreeHook = new TestMerkleTreeHook(address(mailbox)); |
||||
noopHook = new TestPostDispatchHook(); |
||||
weightedFactory = new StaticMessageIdWeightedMultisigIsmFactory(); |
||||
mailbox.setDefaultHook(address(merkleTreeHook)); |
||||
mailbox.setRequiredHook(address(noopHook)); |
||||
} |
||||
|
||||
function getMetadata( |
||||
uint8 m, |
||||
uint8 n, |
||||
bytes32 seed, |
||||
bytes memory message |
||||
) |
||||
internal |
||||
override(AbstractMultisigIsmTest, AbstractStaticWeightedMultisigIsmTest) |
||||
returns (bytes memory) |
||||
{ |
||||
return |
||||
AbstractStaticWeightedMultisigIsmTest.getMetadata( |
||||
m, |
||||
n, |
||||
seed, |
||||
message |
||||
); |
||||
} |
||||
} |
Loading…
Reference in new issue