feat: mutable storage ISMs (#4577)
### Description Some chains like zkSync do not support eip1167 (minimal/meta) proxies. This PR adds an alternative storage based multisig and aggregation ISM for use on these chains. ### Drive-by changes Simplify CLI multisig interactive config builder. Remove stale multisig config. ### Related issues None ### Backward compatibility Yes, relayer already supports this module type ### Testing Contract unit tests Manual CLI tests ![Screenshot 2024-10-02 at 4 05 08 PM](https://github.com/user-attachments/assets/c7fec896-ea7c-4fd9-a313-463168e66a82)pull/4834/head
parent
0264f709e4
commit
836060240b
@ -0,0 +1,7 @@ |
||||
--- |
||||
'@hyperlane-xyz/cli': minor |
||||
'@hyperlane-xyz/sdk': minor |
||||
'@hyperlane-xyz/core': minor |
||||
--- |
||||
|
||||
Add storage based multisig ISM types |
@ -0,0 +1,9 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
interface IThresholdAddressFactory { |
||||
function deploy( |
||||
address[] calldata _values, |
||||
uint8 _threshold |
||||
) external returns (address); |
||||
} |
@ -0,0 +1,89 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
// ============ Internal Imports ============ |
||||
import {AbstractAggregationIsm} from "./AbstractAggregationIsm.sol"; |
||||
import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; |
||||
import {IThresholdAddressFactory} from "../../interfaces/IThresholdAddressFactory.sol"; |
||||
import {MinimalProxy} from "../../libs/MinimalProxy.sol"; |
||||
import {PackageVersioned} from "../../PackageVersioned.sol"; |
||||
|
||||
// ============ External Imports ============ |
||||
import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; |
||||
|
||||
contract StorageAggregationIsm is |
||||
AbstractAggregationIsm, |
||||
Ownable2StepUpgradeable |
||||
{ |
||||
address[] public modules; |
||||
uint8 public threshold; |
||||
|
||||
event ModulesAndThresholdSet(address[] modules, uint8 threshold); |
||||
|
||||
constructor( |
||||
address[] memory _modules, |
||||
uint8 _threshold |
||||
) Ownable2StepUpgradeable() { |
||||
modules = _modules; |
||||
threshold = _threshold; |
||||
_disableInitializers(); |
||||
} |
||||
|
||||
function initialize( |
||||
address _owner, |
||||
address[] memory _modules, |
||||
uint8 _threshold |
||||
) external initializer { |
||||
__Ownable2Step_init(); |
||||
setModulesAndThreshold(_modules, _threshold); |
||||
_transferOwnership(_owner); |
||||
} |
||||
|
||||
function setModulesAndThreshold( |
||||
address[] memory _modules, |
||||
uint8 _threshold |
||||
) public onlyOwner { |
||||
require( |
||||
0 < _threshold && _threshold <= _modules.length, |
||||
"Invalid threshold" |
||||
); |
||||
modules = _modules; |
||||
threshold = _threshold; |
||||
emit ModulesAndThresholdSet(_modules, _threshold); |
||||
} |
||||
|
||||
function modulesAndThreshold( |
||||
bytes calldata /* _message */ |
||||
) public view override returns (address[] memory, uint8) { |
||||
return (modules, threshold); |
||||
} |
||||
} |
||||
|
||||
contract StorageAggregationIsmFactory is |
||||
IThresholdAddressFactory, |
||||
PackageVersioned |
||||
{ |
||||
address public immutable implementation; |
||||
|
||||
constructor() { |
||||
implementation = address( |
||||
new StorageAggregationIsm(new address[](1), 1) |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* @notice Emitted when a multisig module is deployed |
||||
* @param module The deployed ISM |
||||
*/ |
||||
event ModuleDeployed(address module); |
||||
|
||||
// ============ External Functions ============ |
||||
function deploy( |
||||
address[] calldata _modules, |
||||
uint8 _threshold |
||||
) external returns (address ism) { |
||||
ism = MinimalProxy.create(implementation); |
||||
emit ModuleDeployed(ism); |
||||
StorageAggregationIsm(ism).initialize(msg.sender, _modules, _threshold); |
||||
} |
||||
} |
@ -0,0 +1,143 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
// ============ Internal Imports ============ |
||||
import {AbstractMultisigIsm} from "./AbstractMultisigIsm.sol"; |
||||
import {AbstractMerkleRootMultisigIsm} from "./AbstractMerkleRootMultisigIsm.sol"; |
||||
import {AbstractMessageIdMultisigIsm} from "./AbstractMessageIdMultisigIsm.sol"; |
||||
import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; |
||||
import {IThresholdAddressFactory} from "../../interfaces/IThresholdAddressFactory.sol"; |
||||
import {MinimalProxy} from "../../libs/MinimalProxy.sol"; |
||||
import {PackageVersioned} from "../../PackageVersioned.sol"; |
||||
|
||||
// ============ External Imports ============ |
||||
import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; |
||||
|
||||
abstract contract AbstractStorageMultisigIsm is |
||||
AbstractMultisigIsm, |
||||
Ownable2StepUpgradeable |
||||
{ |
||||
address[] public validators; |
||||
uint8 public threshold; |
||||
|
||||
event ValidatorsAndThresholdSet(address[] validators, uint8 threshold); |
||||
|
||||
constructor( |
||||
address[] memory _validators, |
||||
uint8 _threshold |
||||
) Ownable2StepUpgradeable() { |
||||
validators = _validators; |
||||
threshold = _threshold; |
||||
_disableInitializers(); |
||||
} |
||||
|
||||
function initialize( |
||||
address _owner, |
||||
address[] memory _validators, |
||||
uint8 _threshold |
||||
) external initializer { |
||||
__Ownable2Step_init(); |
||||
setValidatorsAndThreshold(_validators, _threshold); |
||||
_transferOwnership(_owner); |
||||
} |
||||
|
||||
function setValidatorsAndThreshold( |
||||
address[] memory _validators, |
||||
uint8 _threshold |
||||
) public onlyOwner { |
||||
require( |
||||
0 < _threshold && _threshold <= _validators.length, |
||||
"Invalid threshold" |
||||
); |
||||
validators = _validators; |
||||
threshold = _threshold; |
||||
emit ValidatorsAndThresholdSet(_validators, _threshold); |
||||
} |
||||
|
||||
function validatorsAndThreshold( |
||||
bytes calldata /* _message */ |
||||
) public view override returns (address[] memory, uint8) { |
||||
return (validators, threshold); |
||||
} |
||||
} |
||||
|
||||
contract StorageMerkleRootMultisigIsm is |
||||
AbstractMerkleRootMultisigIsm, |
||||
AbstractStorageMultisigIsm |
||||
{ |
||||
uint8 public constant moduleType = |
||||
uint8(IInterchainSecurityModule.Types.MERKLE_ROOT_MULTISIG); |
||||
|
||||
constructor( |
||||
address[] memory _validators, |
||||
uint8 _threshold |
||||
) AbstractStorageMultisigIsm(_validators, _threshold) {} |
||||
} |
||||
|
||||
contract StorageMessageIdMultisigIsm is |
||||
AbstractMessageIdMultisigIsm, |
||||
AbstractStorageMultisigIsm |
||||
{ |
||||
uint8 public constant moduleType = |
||||
uint8(IInterchainSecurityModule.Types.MESSAGE_ID_MULTISIG); |
||||
|
||||
constructor( |
||||
address[] memory _validators, |
||||
uint8 _threshold |
||||
) AbstractStorageMultisigIsm(_validators, _threshold) {} |
||||
} |
||||
|
||||
abstract contract StorageMultisigIsmFactory is |
||||
IThresholdAddressFactory, |
||||
PackageVersioned |
||||
{ |
||||
/** |
||||
* @notice Emitted when a multisig module is deployed |
||||
* @param module The deployed ISM |
||||
*/ |
||||
event ModuleDeployed(address module); |
||||
|
||||
// ============ External Functions ============ |
||||
function deploy( |
||||
address[] calldata _validators, |
||||
uint8 _threshold |
||||
) external returns (address ism) { |
||||
ism = MinimalProxy.create(implementation()); |
||||
emit ModuleDeployed(ism); |
||||
AbstractStorageMultisigIsm(ism).initialize( |
||||
msg.sender, |
||||
_validators, |
||||
_threshold |
||||
); |
||||
} |
||||
|
||||
function implementation() public view virtual returns (address); |
||||
} |
||||
|
||||
contract StorageMerkleRootMultisigIsmFactory is StorageMultisigIsmFactory { |
||||
address internal immutable _implementation; |
||||
|
||||
constructor() { |
||||
_implementation = address( |
||||
new StorageMerkleRootMultisigIsm(new address[](0), 0) |
||||
); |
||||
} |
||||
|
||||
function implementation() public view override returns (address) { |
||||
return _implementation; |
||||
} |
||||
} |
||||
|
||||
contract StorageMessageIdMultisigIsmFactory is StorageMultisigIsmFactory { |
||||
address internal immutable _implementation; |
||||
|
||||
constructor() { |
||||
_implementation = address( |
||||
new StorageMessageIdMultisigIsm(new address[](0), 0) |
||||
); |
||||
} |
||||
|
||||
function implementation() public view override returns (address) { |
||||
return _implementation; |
||||
} |
||||
} |
@ -1,41 +0,0 @@ |
||||
import { expect } from 'chai'; |
||||
|
||||
import { ChainMap, MultisigConfig } from '@hyperlane-xyz/sdk'; |
||||
|
||||
import { readMultisigConfig } from '../config/multisig.js'; |
||||
|
||||
describe('readMultisigConfig', () => { |
||||
it('parses and validates example correctly', () => { |
||||
const multisig = readMultisigConfig('examples/ism.yaml'); |
||||
|
||||
const exampleMultisigConfig: ChainMap<MultisigConfig> = { |
||||
anvil1: { |
||||
threshold: 1, |
||||
validators: ['0xa0Ee7A142d267C1f36714E4a8F75612F20a79720'], |
||||
}, |
||||
anvil2: { |
||||
threshold: 1, |
||||
validators: ['0xa0Ee7A142d267C1f36714E4a8F75612F20a79720'], |
||||
}, |
||||
}; |
||||
expect(multisig).to.deep.equal(exampleMultisigConfig); |
||||
}); |
||||
|
||||
it('parsing failure', () => { |
||||
expect(function () { |
||||
readMultisigConfig('src/tests/multisig/safe-parse-fail.yaml'); |
||||
}).to.throw('Invalid multisig config: anvil2,validators => Required'); |
||||
}); |
||||
|
||||
it('threshold cannot be greater than the # of validators', () => { |
||||
expect(function () { |
||||
readMultisigConfig('src/tests/multisig/threshold-gt-fail.yaml'); |
||||
}).to.throw('Threshold cannot be greater than number of validators'); |
||||
}); |
||||
|
||||
it('invalid address', () => { |
||||
expect(function () { |
||||
readMultisigConfig('src/tests/multisig/invalid-address-fail.yaml'); |
||||
}).to.throw('Invalid multisig config: anvil2,validators,0 => Invalid'); |
||||
}); |
||||
}); |
@ -1,8 +0,0 @@ |
||||
anvil1: |
||||
threshold: 1 # Number: Signatures required to approve a message |
||||
validators: # Array: List of validator addresses |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
anvil2: |
||||
threshold: 1 |
||||
validators: |
||||
- '0xa0ee7a142d267c1n36714e4a8f7561f20a79720' |
@ -1,6 +0,0 @@ |
||||
anvil1: |
||||
threshold: 1 # Number: Signatures required to approve a message |
||||
validators: # Array: List of validator addresses |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
anvil2: |
||||
threshold: 1 |
@ -1,8 +0,0 @@ |
||||
anvil1: |
||||
threshold: 1 # Number: Signatures required to approve a message |
||||
validators: # Array: List of validator addresses |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
anvil2: |
||||
threshold: 3 |
||||
validators: |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
Loading…
Reference in new issue