diff --git a/.changeset/routing-ism-batch-ops.md b/.changeset/routing-ism-batch-ops.md new file mode 100644 index 00000000000..fb731ba2956 --- /dev/null +++ b/.changeset/routing-ism-batch-ops.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/core': minor +--- + +Added `setBatch(DomainModule[])` and `removeBatch(uint32[])` to `DomainRoutingIsm` so routing ISM owners can enroll or unenroll many domains in a single transaction after initialization. Mirrors the `setHooks(HookConfig[])` pattern on `DomainRoutingHook`. Inherited by `IncrementalDomainRoutingIsm` (where `removeBatch` reverts, consistent with `remove`) and `DefaultFallbackRoutingIsm`. diff --git a/solidity/contracts/isms/routing/DomainRoutingIsm.sol b/solidity/contracts/isms/routing/DomainRoutingIsm.sol index a627df4b866..20175a1c70e 100644 --- a/solidity/contracts/isms/routing/DomainRoutingIsm.sol +++ b/solidity/contracts/isms/routing/DomainRoutingIsm.sol @@ -32,6 +32,12 @@ contract DomainRoutingIsm is // ============ Mutable Storage ============ EnumerableMapExtended.UintToBytes32Map internal _modules; + // ============ Structs ============ + struct DomainModule { + uint32 domain; + IInterchainSecurityModule module; + } + // ============ External Functions ============ /** @@ -74,6 +80,18 @@ contract DomainRoutingIsm is _set(_domain, address(_module)); } + /** + * @notice Sets the ISMs to be used for the specified origin domains + * @param _domainModules The origin domains and ISMs to enroll + */ + function setBatch( + DomainModule[] calldata _domainModules + ) external onlyOwner { + for (uint256 i = 0; i < _domainModules.length; ++i) { + _set(_domainModules[i].domain, address(_domainModules[i].module)); + } + } + /** * @notice Removes the specified origin domain * @param _domain The origin domain @@ -82,6 +100,16 @@ contract DomainRoutingIsm is _remove(_domain); } + /** + * @notice Removes the specified origin domains + * @param _domains The origin domains to remove + */ + function removeBatch(uint32[] calldata _domains) external onlyOwner { + for (uint256 i = 0; i < _domains.length; ++i) { + _remove(_domains[i]); + } + } + function domains() external view returns (uint256[] memory) { return _modules.keys(); } diff --git a/solidity/test/isms/DomainRoutingIsm.t.sol b/solidity/test/isms/DomainRoutingIsm.t.sol index c18f3294175..2ec053d5869 100644 --- a/solidity/test/isms/DomainRoutingIsm.t.sol +++ b/solidity/test/isms/DomainRoutingIsm.t.sol @@ -37,6 +37,76 @@ contract DomainRoutingIsmTest is Test { assertEq(address(ism.module(domain)), address(_ism)); } + function buildDomainModules( + uint32 domain, + uint8 count + ) internal returns (DomainRoutingIsm.DomainModule[] memory) { + DomainRoutingIsm.DomainModule[] + memory domainModules = new DomainRoutingIsm.DomainModule[](count); + for (uint32 i = 0; i < count; ++i) { + unchecked { + domainModules[i] = DomainRoutingIsm.DomainModule({ + domain: domain + i, + module: IInterchainSecurityModule( + address(deployTestIsm(bytes32(0))) + ) + }); + } + } + return domainModules; + } + + function testSetBatch(uint32 domain, uint8 count) public { + vm.assume(count > 0); + DomainRoutingIsm.DomainModule[] + memory domainModules = buildDomainModules(domain, count); + + ism.setBatch(domainModules); + for (uint256 i = 0; i < count; ++i) { + assertEq( + address(ism.module(domainModules[i].domain)), + address(domainModules[i].module) + ); + } + } + + function testSetBatchNonOwner(uint32 domain) public { + DomainRoutingIsm.DomainModule[] + memory domainModules = new DomainRoutingIsm.DomainModule[](1); + domainModules[0] = DomainRoutingIsm.DomainModule({ + domain: domain, + module: IInterchainSecurityModule(address(0)) + }); + vm.prank(NON_OWNER); + vm.expectRevert("Ownable: caller is not the owner"); + ism.setBatch(domainModules); + } + + function testRemoveBatch(uint32 domain, uint8 count) public virtual { + vm.assume(count > 0); + DomainRoutingIsm.DomainModule[] + memory domainModules = buildDomainModules(domain, count); + ism.setBatch(domainModules); + + uint32[] memory domains = new uint32[](count); + for (uint256 i = 0; i < count; ++i) { + domains[i] = domainModules[i].domain; + } + ism.removeBatch(domains); + for (uint256 i = 0; i < count; ++i) { + vm.expectRevert(); + ism.module(domains[i]); + } + } + + function testRemoveBatchNonOwner(uint32 domain) public { + uint32[] memory domains = new uint32[](1); + domains[0] = domain; + vm.prank(NON_OWNER); + vm.expectRevert("Ownable: caller is not the owner"); + ism.removeBatch(domains); + } + function testRemove(uint32 domain) public virtual { vm.expectRevert(); ism.remove(domain); @@ -123,6 +193,22 @@ contract DefaultFallbackRoutingIsmTest is DomainRoutingIsmTest { new DefaultFallbackRoutingIsm(address(0)); } + function testRemoveBatch(uint32 domain, uint8 count) public override { + vm.assume(count > 0); + DomainRoutingIsm.DomainModule[] + memory domainModules = buildDomainModules(domain, count); + ism.setBatch(domainModules); + + uint32[] memory domains = new uint32[](count); + for (uint256 i = 0; i < count; ++i) { + domains[i] = domainModules[i].domain; + } + ism.removeBatch(domains); + for (uint256 i = 0; i < count; ++i) { + assertEq(address(ism.module(domains[i])), address(defaultIsm)); + } + } + function testVerifyNoIsm(uint32 domain, bytes32 seed) public override { vm.assume(domain > 0); ism.set(domain, deployTestIsm(seed)); diff --git a/solidity/test/isms/IncrementalDomainRoutingIsm.t.sol b/solidity/test/isms/IncrementalDomainRoutingIsm.t.sol index 8d77dfae361..42b993ca8e4 100644 --- a/solidity/test/isms/IncrementalDomainRoutingIsm.t.sol +++ b/solidity/test/isms/IncrementalDomainRoutingIsm.t.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import {IncrementalDomainRoutingIsm} from "../../contracts/isms/routing/IncrementalDomainRoutingIsm.sol"; import {IncrementalDomainRoutingIsmFactory} from "../../contracts/isms/routing/IncrementalDomainRoutingIsmFactory.sol"; +import {DomainRoutingIsm} from "../../contracts/isms/routing/DomainRoutingIsm.sol"; import {DomainRoutingIsmTest} from "./DomainRoutingIsm.t.sol"; import {IInterchainSecurityModule} from "../../contracts/interfaces/IInterchainSecurityModule.sol"; import {TestIsm} from "./IsmTestUtils.sol"; @@ -29,6 +30,18 @@ contract IncrementalDomainRoutingIsmTest is DomainRoutingIsmTest { ism.remove(domain); } + function testRemoveBatch(uint32 domain, uint8 count) public override { + vm.assume(count > 0); + uint32[] memory domains = new uint32[](count); + for (uint32 i = 0; i < count; ++i) { + unchecked { + domains[i] = domain + i; + } + } + vm.expectRevert("IncrementalDomainRoutingIsm: removal not supported"); + ism.removeBatch(domains); + } + function testSetTwiceReverts(uint32 domain) public { TestIsm _ism = deployTestIsm(bytes32(0)); TestIsm _ism2 = deployTestIsm(bytes32(uint256(1)));