Skip to content

Commit 6c715a7

Browse files
authored
feat: MinimalInterchainAccountRouter for Igra (#8323)
1 parent 94a3231 commit 6c715a7

20 files changed

Lines changed: 1120 additions & 770 deletions

.changeset/slim-ica-core.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperlane-xyz/core': minor
3+
---
4+
5+
Added MinimalInterchainAccountRouter for chains with tight deployment size limits.

.changeset/slim-ica-router-type.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperlane-xyz/sdk': minor
3+
---
4+
5+
Added support for MinimalInterchainAccountRouter deployment and detection.

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pnpm build
2929
pnpm test
3030

3131
# Lint and format
32-
pnpm lint && pnpm prettier
32+
pnpm lint && pnpm format
3333

3434
# Solidity tests (both Hardhat and Forge)
3535
pnpm -C solidity test
@@ -51,7 +51,7 @@ pnpm -C typescript/cli test:radix:e2e
5151
```bash
5252
# TypeScript/Solidity
5353
pnpm lint # Must pass
54-
pnpm prettier # Auto-formats code
54+
pnpm format # Auto-formats code
5555
pnpm test # Run relevant tests
5656
pnpm changeset # Add changeset if modifying published packages
5757

@@ -205,7 +205,7 @@ cd rust/main && cargo test --release --package run-locally --features sealevel -
205205

206206
```bash
207207
pnpm lint # Lint all packages
208-
pnpm prettier # Format all packages
208+
pnpm format # Format all packages
209209
pnpm -C solidity lint # Solidity linting (solhint)
210210
cd rust/main && cargo clippy # Rust linting
211211
cd rust/main && cargo fmt # Rust formatting

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"build:zk": "turbo run build:zk",
1010
"clean": "turbo run clean",
1111
"format": "turbo run format",
12+
"prettier": "turbo run format",
1213
"format:check": "oxfmt --check . && prettier --check 'solidity/contracts/**/*.sol' 'solidity/test/**/*.sol'",
1314
"lint": "oxlint -c oxlint.json && turbo run lint",
1415
"lint:catalog": "tsx scripts/lint-catalog-usage.ts",
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.13;
3+
4+
/*@@@@@@@ @@@@@@@@@
5+
@@@@@@@@@ @@@@@@@@@
6+
@@@@@@@@@ @@@@@@@@@
7+
@@@@@@@@@ @@@@@@@@@
8+
@@@@@@@@@@@@@@@@@@@@@@@@@
9+
@@@@@ HYPERLANE @@@@@@@
10+
@@@@@@@@@@@@@@@@@@@@@@@@@
11+
@@@@@@@@@ @@@@@@@@@
12+
@@@@@@@@@ @@@@@@@@@
13+
@@@@@@@@@ @@@@@@@@@
14+
@@@@@@@@@ @@@@@@@@*/
15+
16+
// ============ Internal Imports ============
17+
import {OwnableMulticall} from "./libs/OwnableMulticall.sol";
18+
import {InterchainAccountMessage} from "./libs/InterchainAccountMessage.sol";
19+
import {CallLib} from "./libs/Call.sol";
20+
import {MinimalProxy} from "../libs/MinimalProxy.sol";
21+
import {TypeCasts} from "../libs/TypeCasts.sol";
22+
import {StandardHookMetadata} from "../hooks/libs/StandardHookMetadata.sol";
23+
import {Router} from "../client/Router.sol";
24+
import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol";
25+
import {IInterchainSecurityModule} from "../interfaces/IInterchainSecurityModule.sol";
26+
27+
// ============ External Imports ============
28+
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
29+
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
30+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
31+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
32+
33+
// solhint-disable-next-line hyperlane/enumerable-domain-mapping
34+
abstract contract AbstractInterchainAccountRouter is Router {
35+
using TypeCasts for address;
36+
using TypeCasts for bytes32;
37+
using StandardHookMetadata for bytes;
38+
using SafeERC20 for IERC20;
39+
40+
// ============ Constants ============
41+
42+
address public immutable implementation;
43+
bytes32 public immutable bytecodeHash;
44+
45+
// ============ Public Storage ============
46+
mapping(uint32 destinationDomain => bytes32 ism) public isms;
47+
48+
// ============ Upgrade Gap ============
49+
50+
uint256[47] private __GAP;
51+
52+
// ============ Events ============
53+
54+
event RemoteIsmEnrolled(uint32 indexed domain, bytes32 ism);
55+
56+
event RemoteCallDispatched(
57+
uint32 indexed destination,
58+
address indexed owner,
59+
bytes32 router,
60+
bytes32 ism,
61+
bytes32 salt
62+
);
63+
64+
event InterchainAccountCreated(
65+
address indexed account,
66+
uint32 origin,
67+
bytes32 router,
68+
bytes32 owner,
69+
address ism,
70+
bytes32 salt
71+
);
72+
73+
// solhint-disable-next-line hyperlane/no-virtual-override
74+
function interchainSecurityModule()
75+
external
76+
view
77+
virtual
78+
override
79+
returns (IInterchainSecurityModule)
80+
{
81+
return IInterchainSecurityModule(address(this));
82+
}
83+
84+
function enrollRemoteRouterAndIsm(
85+
uint32 _destination,
86+
bytes32 _router,
87+
bytes32 _ism
88+
) external onlyOwner {
89+
_enrollRemoteRouterAndIsm(_destination, _router, _ism);
90+
}
91+
92+
function enrollRemoteRouterAndIsms(
93+
uint32[] calldata _destinations,
94+
bytes32[] calldata _routers,
95+
bytes32[] calldata _isms
96+
) external onlyOwner {
97+
require(
98+
_destinations.length == _routers.length &&
99+
_destinations.length == _isms.length,
100+
"length mismatch"
101+
);
102+
for (uint256 i = 0; i < _destinations.length; i++) {
103+
_enrollRemoteRouterAndIsm(_destinations[i], _routers[i], _isms[i]);
104+
}
105+
}
106+
107+
receive() external payable {}
108+
109+
function approveFeeTokenForHook(address _feeToken, address _hook) external {
110+
IERC20(_feeToken).forceApprove(_hook, type(uint256).max);
111+
}
112+
113+
function getDeployedInterchainAccount(
114+
uint32 _origin,
115+
address _owner,
116+
address _router,
117+
address _ism
118+
) public virtual returns (OwnableMulticall) {
119+
return
120+
getDeployedInterchainAccount(
121+
_origin,
122+
_owner.addressToBytes32(),
123+
_router.addressToBytes32(),
124+
_ism,
125+
InterchainAccountMessage.EMPTY_SALT
126+
);
127+
}
128+
129+
function getDeployedInterchainAccount(
130+
uint32 _origin,
131+
bytes32 _owner,
132+
bytes32 _router,
133+
address _ism,
134+
bytes32 _userSalt
135+
) public virtual returns (OwnableMulticall) {
136+
bytes32 _deploySalt = _getSalt(
137+
_origin,
138+
_owner,
139+
_router,
140+
_ism.addressToBytes32(),
141+
_userSalt
142+
);
143+
address payable _account = _getLocalInterchainAccount(_deploySalt);
144+
if (!Address.isContract(_account)) {
145+
bytes memory _bytecode = MinimalProxy.bytecode(implementation);
146+
_account = payable(Create2.deploy(0, _deploySalt, _bytecode));
147+
emit InterchainAccountCreated(
148+
_account,
149+
_origin,
150+
_router,
151+
_owner,
152+
_ism,
153+
_userSalt
154+
);
155+
}
156+
return OwnableMulticall(_account);
157+
}
158+
159+
function getLocalInterchainAccount(
160+
uint32 _origin,
161+
address _owner,
162+
address _router,
163+
address _ism
164+
) external view virtual returns (OwnableMulticall) {
165+
return
166+
OwnableMulticall(
167+
_getLocalInterchainAccount(
168+
_getSalt(
169+
_origin,
170+
_owner.addressToBytes32(),
171+
_router.addressToBytes32(),
172+
_ism.addressToBytes32(),
173+
InterchainAccountMessage.EMPTY_SALT
174+
)
175+
)
176+
);
177+
}
178+
179+
function quoteGasPayment(
180+
uint32 _destination,
181+
uint256 _gasLimit
182+
) public view virtual returns (uint256 _gasPayment) {
183+
return
184+
_Router_quoteDispatch(
185+
_destination,
186+
new bytes(0),
187+
StandardHookMetadata.overrideGasLimit(_gasLimit),
188+
address(hook)
189+
);
190+
}
191+
192+
function callRemoteWithOverrides(
193+
uint32 _destination,
194+
bytes32 _router,
195+
bytes32 _ism,
196+
CallLib.Call[] calldata _calls,
197+
bytes memory _hookMetadata
198+
) public payable virtual returns (bytes32) {
199+
emit RemoteCallDispatched(
200+
_destination,
201+
msg.sender,
202+
_router,
203+
_ism,
204+
InterchainAccountMessage.EMPTY_SALT
205+
);
206+
bytes memory _body = InterchainAccountMessage.encode(
207+
msg.sender,
208+
_ism,
209+
_calls
210+
);
211+
return
212+
_dispatchMessageWithValue(
213+
_destination,
214+
_router,
215+
_body,
216+
_hookMetadata,
217+
hook,
218+
msg.value
219+
);
220+
}
221+
222+
function _dispatchMessageWithValue(
223+
uint32 _destination,
224+
bytes32 _router,
225+
bytes memory _body,
226+
bytes memory _hookMetadata,
227+
IPostDispatchHook _hook,
228+
uint256 _value
229+
) internal returns (bytes32) {
230+
require(_router != bytes32(0), "no router specified for destination");
231+
232+
address _feeToken = _hookMetadata.feeToken();
233+
if (_feeToken != address(0)) {
234+
uint256 _fee = _Router_quoteDispatch(
235+
_destination,
236+
bytes(""),
237+
_hookMetadata,
238+
address(_hook)
239+
);
240+
241+
IERC20(_feeToken).safeTransferFrom(msg.sender, address(this), _fee);
242+
IERC20(_feeToken).forceApprove(address(_hook), type(uint256).max);
243+
}
244+
245+
return
246+
mailbox.dispatch{value: _value}(
247+
_destination,
248+
_router,
249+
_body,
250+
_hookMetadata,
251+
_hook
252+
);
253+
}
254+
255+
function _implementationBytecode(
256+
address router
257+
) internal pure returns (bytes memory) {
258+
return
259+
abi.encodePacked(
260+
type(OwnableMulticall).creationCode,
261+
abi.encode(router)
262+
);
263+
}
264+
265+
function _proxyBytecodeHash(
266+
address _implementation
267+
) internal pure returns (bytes32) {
268+
return keccak256(MinimalProxy.bytecode(_implementation));
269+
}
270+
271+
/// @dev Required for use of Router, compiler will not include this function in the bytecode
272+
function _handle(uint32, bytes32, bytes calldata) internal pure override {
273+
assert(false);
274+
}
275+
276+
// solhint-disable-next-line hyperlane/no-virtual-override
277+
function _enrollRemoteRouter(
278+
uint32 _destination,
279+
bytes32 _address
280+
) internal virtual override {
281+
_enrollRemoteRouterAndIsm(
282+
_destination,
283+
_address,
284+
InterchainAccountMessage.EMPTY_SALT
285+
);
286+
}
287+
288+
function _enrollRemoteIsm(uint32 _destination, bytes32 _ism) internal {
289+
isms[_destination] = _ism;
290+
emit RemoteIsmEnrolled(_destination, _ism);
291+
}
292+
293+
function _enrollRemoteRouterAndIsm(
294+
uint32 _destination,
295+
bytes32 _router,
296+
bytes32 _ism
297+
) internal {
298+
require(
299+
routers(_destination) == InterchainAccountMessage.EMPTY_SALT &&
300+
isms[_destination] == InterchainAccountMessage.EMPTY_SALT,
301+
"router and ISM defaults are immutable once set"
302+
);
303+
Router._enrollRemoteRouter(_destination, _router);
304+
_enrollRemoteIsm(_destination, _ism);
305+
}
306+
307+
function _getSalt(
308+
uint32 _origin,
309+
bytes32 _owner,
310+
bytes32 _router,
311+
bytes32 _ism,
312+
bytes32 _userSalt
313+
) internal pure returns (bytes32) {
314+
return
315+
keccak256(
316+
abi.encodePacked(_origin, _owner, _router, _ism, _userSalt)
317+
);
318+
}
319+
320+
function _getLocalInterchainAccount(
321+
bytes32 _salt
322+
) internal view returns (address payable) {
323+
return payable(Create2.computeAddress(_salt, bytecodeHash));
324+
}
325+
}

0 commit comments

Comments
 (0)