@hyperlane-xyz/multicollateral 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/contracts/{MultiCollateral.sol → CrossCollateralRouter.sol} +131 -46
  2. package/contracts/{MultiCollateralRoutingFee.sol → CrossCollateralRoutingFee.sol} +9 -6
  3. package/contracts/TokenBridgeOft.sol +338 -0
  4. package/contracts/interfaces/{IMultiCollateralFee.sol → ICrossCollateralFee.sol} +1 -1
  5. package/contracts/interfaces/layerzero/IOFT.sol +85 -0
  6. package/dist/typechain/{MultiCollateral.d.ts → CrossCollateralRouter.d.ts} +89 -79
  7. package/dist/typechain/CrossCollateralRouter.d.ts.map +1 -0
  8. package/dist/typechain/CrossCollateralRouter.js +2 -0
  9. package/dist/typechain/CrossCollateralRouter.js.map +1 -0
  10. package/dist/typechain/{MultiCollateralRoutingFee.d.ts → CrossCollateralRoutingFee.d.ts} +13 -13
  11. package/dist/typechain/{MultiCollateralRoutingFee.d.ts.map → CrossCollateralRoutingFee.d.ts.map} +1 -1
  12. package/dist/typechain/CrossCollateralRoutingFee.js +2 -0
  13. package/dist/typechain/CrossCollateralRoutingFee.js.map +1 -0
  14. package/dist/typechain/TokenBridgeOft.d.ts +293 -0
  15. package/dist/typechain/TokenBridgeOft.d.ts.map +1 -0
  16. package/dist/typechain/TokenBridgeOft.js +2 -0
  17. package/dist/typechain/TokenBridgeOft.js.map +1 -0
  18. package/dist/typechain/factories/{MultiCollateral__factory.d.ts → CrossCollateralRouter__factory.d.ts} +97 -87
  19. package/dist/typechain/factories/{MultiCollateral__factory.d.ts.map → CrossCollateralRouter__factory.d.ts.map} +1 -1
  20. package/dist/typechain/factories/CrossCollateralRouter__factory.js +1868 -0
  21. package/dist/typechain/factories/CrossCollateralRouter__factory.js.map +1 -0
  22. package/dist/typechain/factories/{MultiCollateralRoutingFee__factory.d.ts → CrossCollateralRoutingFee__factory.d.ts} +12 -12
  23. package/dist/typechain/factories/{MultiCollateralRoutingFee__factory.d.ts.map → CrossCollateralRoutingFee__factory.d.ts.map} +1 -1
  24. package/dist/typechain/factories/{MultiCollateralRoutingFee__factory.js → CrossCollateralRoutingFee__factory.js} +6 -6
  25. package/dist/typechain/factories/{MultiCollateralRoutingFee__factory.js.map → CrossCollateralRoutingFee__factory.js.map} +1 -1
  26. package/dist/typechain/factories/TokenBridgeOft__factory.d.ts +312 -0
  27. package/dist/typechain/factories/TokenBridgeOft__factory.d.ts.map +1 -0
  28. package/dist/typechain/factories/TokenBridgeOft__factory.js +417 -0
  29. package/dist/typechain/factories/TokenBridgeOft__factory.js.map +1 -0
  30. package/dist/typechain/factories/index.d.ts +3 -2
  31. package/dist/typechain/factories/index.d.ts.map +1 -1
  32. package/dist/typechain/factories/index.js +3 -2
  33. package/dist/typechain/factories/index.js.map +1 -1
  34. package/dist/typechain/index.d.ts +6 -4
  35. package/dist/typechain/index.d.ts.map +1 -1
  36. package/dist/typechain/index.js +3 -2
  37. package/dist/typechain/index.js.map +1 -1
  38. package/package.json +7 -6
  39. package/dist/typechain/MultiCollateral.d.ts.map +0 -1
  40. package/dist/typechain/MultiCollateral.js +0 -2
  41. package/dist/typechain/MultiCollateral.js.map +0 -1
  42. package/dist/typechain/MultiCollateralRoutingFee.js +0 -2
  43. package/dist/typechain/MultiCollateralRoutingFee.js.map +0 -1
  44. package/dist/typechain/factories/MultiCollateral__factory.js +0 -1855
  45. package/dist/typechain/factories/MultiCollateral__factory.js.map +0 -1
@@ -24,36 +24,50 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol
24
24
  import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
25
25
 
26
26
  // ============ Local Imports ============
27
- import {IMultiCollateralFee} from "./interfaces/IMultiCollateralFee.sol";
27
+ import {ICrossCollateralFee} from "./interfaces/ICrossCollateralFee.sol";
28
28
 
29
29
  /**
30
- * @title MultiCollateral
30
+ * @title CrossCollateralRouter
31
31
  * @notice Multi-router collateral: direct 1-message atomic transfers between
32
32
  * collateral routers, both cross-chain and same-chain.
33
33
  * @dev Extends HypERC20Collateral. Each deployed instance holds collateral for
34
- * one ERC20. Enrolled routers are other MultiCollateral instances (same or
34
+ * one ERC20. Enrolled routers are other CrossCollateralRouter instances (same or
35
35
  * different token) that this instance trusts to send/receive transfers.
36
+ * CrossCollateralRouter assumes standard ERC20 behavior with exact transfer
37
+ * amounts. Rebasing tokens, fee-on-transfer tokens, and ERC777 tokens are not
38
+ * supported due to exact-amount accounting in transfer/handle flows.
36
39
  *
37
40
  * Overrides:
38
41
  * - handle(): accepts messages from the mailbox (cross-chain) or directly
39
42
  * from enrolled routers on the same chain.
40
43
  */
41
- contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
44
+ contract CrossCollateralRouter is HypERC20Collateral, ICrossCollateralFee {
42
45
  using TypeCasts for address;
43
46
  using TypeCasts for bytes32;
44
47
  using SafeERC20 for IERC20;
45
48
  using EnumerableSet for EnumerableSet.Bytes32Set;
49
+ using EnumerableSet for EnumerableSet.UintSet;
46
50
 
47
51
  // ============ Events ============
48
52
 
49
- event RouterEnrolled(uint32 indexed domain, bytes32 indexed router);
50
- event RouterUnenrolled(uint32 indexed domain, bytes32 indexed router);
53
+ event CrossCollateralRouterEnrolled(
54
+ uint32 indexed domain,
55
+ bytes32 indexed router
56
+ );
57
+ event CrossCollateralRouterUnenrolled(
58
+ uint32 indexed domain,
59
+ bytes32 indexed router
60
+ );
51
61
 
52
62
  // ============ Storage ============
53
63
 
54
64
  /// @notice Additional enrolled routers by domain (beyond the standard
55
65
  /// enrolled remote router). Local routers use localDomain as key.
56
- mapping(uint32 => EnumerableSet.Bytes32Set) private _enrolledRouters;
66
+ mapping(uint32 => EnumerableSet.Bytes32Set) private _crossCollateralRouters;
67
+
68
+ /// @notice Tracks which domains have at least one CrossCollateral-enrolled router,
69
+ /// enabling on-chain enumeration for the SDK reader.
70
+ EnumerableSet.UintSet private _crossCollateralDomains;
57
71
 
58
72
  // ============ Constructor ============
59
73
 
@@ -66,43 +80,91 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
66
80
 
67
81
  // ============ Router Management (onlyOwner) ============
68
82
 
69
- function enrollRouters(
83
+ function enrollCrossCollateralRouters(
70
84
  uint32[] calldata _domains,
71
85
  bytes32[] calldata _routers
72
86
  ) external onlyOwner {
73
- require(_domains.length == _routers.length, "MC: length mismatch");
87
+ require(_domains.length == _routers.length, "CCR: length mismatch");
74
88
  for (uint256 i = 0; i < _domains.length; i++) {
75
- if (_enrolledRouters[_domains[i]].add(_routers[i])) {
76
- emit RouterEnrolled(_domains[i], _routers[i]);
89
+ if (_crossCollateralRouters[_domains[i]].add(_routers[i])) {
90
+ _crossCollateralDomains.add(uint256(_domains[i]));
91
+ emit CrossCollateralRouterEnrolled(_domains[i], _routers[i]);
77
92
  }
78
93
  }
79
94
  }
80
95
 
81
- function unenrollRouters(
96
+ function unenrollCrossCollateralRouters(
82
97
  uint32[] calldata _domains,
83
98
  bytes32[] calldata _routers
84
99
  ) external onlyOwner {
85
- require(_domains.length == _routers.length, "MC: length mismatch");
100
+ require(_domains.length == _routers.length, "CCR: length mismatch");
86
101
  for (uint256 i = 0; i < _domains.length; i++) {
87
- if (_enrolledRouters[_domains[i]].remove(_routers[i])) {
88
- emit RouterUnenrolled(_domains[i], _routers[i]);
102
+ if (_crossCollateralRouters[_domains[i]].remove(_routers[i])) {
103
+ if (_crossCollateralRouters[_domains[i]].length() == 0) {
104
+ _crossCollateralDomains.remove(uint256(_domains[i]));
105
+ }
106
+ emit CrossCollateralRouterUnenrolled(_domains[i], _routers[i]);
89
107
  }
90
108
  }
91
109
  }
92
110
 
93
- function enrolledRouters(
111
+ function crossCollateralRouters(
94
112
  uint32 _domain,
95
113
  bytes32 _router
96
114
  ) external view returns (bool) {
97
- return _enrolledRouters[_domain].contains(_router);
115
+ return _crossCollateralRouters[_domain].contains(_router);
98
116
  }
99
117
 
100
118
  // ============ Enumeration ============
101
119
 
102
- function getEnrolledRouters(
120
+ function getCrossCollateralRouters(
103
121
  uint32 _domain
104
122
  ) external view returns (bytes32[] memory) {
105
- return _enrolledRouters[_domain].values();
123
+ return _crossCollateralRouters[_domain].values();
124
+ }
125
+
126
+ /// @notice Returns all domains that have at least one CrossCollateral-enrolled router.
127
+ function getCrossCollateralDomains()
128
+ external
129
+ view
130
+ returns (uint32[] memory domains)
131
+ {
132
+ uint256 len = _crossCollateralDomains.length();
133
+ domains = new uint32[](len);
134
+ for (uint256 i = 0; i < len; i++) {
135
+ domains[i] = uint32(_crossCollateralDomains.at(i));
136
+ }
137
+ }
138
+
139
+ // ============ Destination Gas Override ============
140
+
141
+ /// @dev Overrides GasRouter._setDestinationGas to also accept CrossCollateral-enrolled
142
+ /// domains (not just default Router._routers). Excludes localDomain since
143
+ /// same-chain transfers skip mailbox dispatch.
144
+ function _setDestinationGas(uint32 domain, uint256 gas) internal override {
145
+ require(domain != localDomain, "CCR: no gas for local domain");
146
+ require(
147
+ routers(domain) != bytes32(0) ||
148
+ _crossCollateralRouters[domain].length() > 0,
149
+ "CCR: domain has no routers"
150
+ );
151
+ destinationGas[domain] = gas;
152
+ emit GasSet(domain, gas);
153
+ }
154
+
155
+ // ============ Internal Helpers ============
156
+
157
+ /// @dev Reverts unless `_router` is enrolled for `_domain` (either via the
158
+ /// standard Router._routers map or via the CrossCollateral-specific _crossCollateralRouters set).
159
+ function _requireAuthorizedRouter(
160
+ uint32 _domain,
161
+ bytes32 _router
162
+ ) internal view {
163
+ require(
164
+ _isRemoteRouter(_domain, _router) ||
165
+ _crossCollateralRouters[_domain].contains(_router),
166
+ "CCR: unauthorized router"
167
+ );
106
168
  }
107
169
 
108
170
  // ============ Handle Override ============
@@ -119,18 +181,14 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
119
181
  ) external payable override {
120
182
  if (msg.sender == address(mailbox)) {
121
183
  // Cross-chain via mailbox: sender must be enrolled
122
- require(
123
- _isRemoteRouter(_origin, _sender) ||
124
- _enrolledRouters[_origin].contains(_sender),
125
- "MC: unauthorized router"
126
- );
184
+ _requireAuthorizedRouter(_origin, _sender);
127
185
  } else {
128
186
  // Same-chain direct call: caller must be an enrolled router
129
187
  require(
130
- _enrolledRouters[localDomain].contains(
188
+ _crossCollateralRouters[localDomain].contains(
131
189
  TypeCasts.addressToBytes32(msg.sender)
132
190
  ),
133
- "MC: unauthorized router"
191
+ "CCR: unauthorized router"
134
192
  );
135
193
  }
136
194
  _handle(_origin, _sender, _message);
@@ -138,7 +196,7 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
138
196
 
139
197
  // ============ Per-Router Fee Lookup ============
140
198
  // Mirrors TokenRouter._feeRecipientAndAmount but routes through
141
- // IMultiCollateralFee.quoteTransferRemoteTo (which includes _targetRouter)
199
+ // ICrossCollateralFee.quoteTransferRemoteTo (which includes _targetRouter)
142
200
  // instead of ITokenFee.quoteTransferRemote (destination-only).
143
201
 
144
202
  function _feeRecipientAndAmountForRouter(
@@ -151,7 +209,7 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
151
209
  if (_feeRecipient == address(0)) return (_feeRecipient, 0);
152
210
 
153
211
  // Only difference from base: quoteTransferRemoteTo with _targetRouter
154
- Quote[] memory quotes = IMultiCollateralFee(_feeRecipient)
212
+ Quote[] memory quotes = ICrossCollateralFee(_feeRecipient)
155
213
  .quoteTransferRemoteTo(
156
214
  _destination,
157
215
  _recipient,
@@ -162,7 +220,7 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
162
220
 
163
221
  require(
164
222
  quotes.length == 1 && quotes[0].token == token(),
165
- "MC: fee must match token"
223
+ "CCR: fee must match token"
166
224
  );
167
225
  feeAmount = quotes[0].amount;
168
226
  }
@@ -197,11 +255,12 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
197
255
  // Same-domain transferRemoteTo calls handle() directly and does not dispatch
198
256
  // through mailbox hooks, so do not charge hook fees in that path.
199
257
  if (_feeHook != address(0) && _destination != localDomain) {
200
- uint256 hookFee = _quoteGasPayment(
258
+ uint256 hookFee = _quoteGasPaymentTo(
201
259
  _destination,
202
260
  _recipient,
203
- _amount,
204
- _token
261
+ _outboundAmount(_amount),
262
+ _token,
263
+ _targetRouter
205
264
  );
206
265
  if (hookFee > 0) {
207
266
  if (_token != address(this)) {
@@ -231,10 +290,10 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
231
290
  /**
232
291
  * @notice Transfers tokens to the primary enrolled router for `_destination`.
233
292
  * @dev Uses the enrolled primary remote router for `_destination` and routes through
234
- * router-aware fee lookup (`IMultiCollateralFee`) via `transferRemoteTo`.
293
+ * router-aware fee lookup (`ICrossCollateralFee`) via `transferRemoteTo`.
235
294
  * @dev This override is required because TokenRouter's `_feeRecipientAndAmount`
236
295
  * is non-virtual and hardcodes `ITokenFee`. Delegating through
237
- * `transferRemoteTo` keeps both transfer paths on `IMultiCollateralFee`.
296
+ * `transferRemoteTo` keeps both transfer paths on `ICrossCollateralFee`.
238
297
  */
239
298
  function transferRemote(
240
299
  uint32 _destination,
@@ -263,13 +322,11 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
263
322
  uint256 _amount,
264
323
  bytes32 _targetRouter
265
324
  ) public payable returns (bytes32 messageId) {
266
- require(
267
- _isRemoteRouter(_destination, _targetRouter) ||
268
- _enrolledRouters[_destination].contains(_targetRouter),
269
- "MC: unauthorized router"
270
- );
325
+ _requireAuthorizedRouter(_destination, _targetRouter);
271
326
  if (_destination == localDomain) {
272
- require(msg.value == 0, "MC: local transfer no msg.value");
327
+ // Local transfers call handle() directly without mailbox dispatch,
328
+ // so any msg.value would be stuck in this contract permanently.
329
+ require(msg.value == 0, "CCR: local transfer no msg.value");
273
330
  }
274
331
 
275
332
  (, uint256 remainingValue) = _calculateFeesAndChargeForRouter(
@@ -286,8 +343,8 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
286
343
  if (_destination == localDomain) {
287
344
  // Same-domain: call target router's handle directly
288
345
  address target = _targetRouter.bytes32ToAddress();
289
- require(target.code.length > 0, "MC: target router not contract");
290
- MultiCollateral(target).handle(
346
+ require(target.code.length > 0, "CCR: target router not contract");
347
+ CrossCollateralRouter(target).handle(
291
348
  localDomain,
292
349
  TypeCasts.addressToBytes32(address(this)),
293
350
  tokenMsg
@@ -310,24 +367,33 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
310
367
  // Differences: (1) router-aware fee lookup, (2) same-domain returns 0 gas
311
368
  // since handle() is called directly without mailbox dispatch.
312
369
 
313
- /// @inheritdoc IMultiCollateralFee
370
+ /// @inheritdoc ICrossCollateralFee
314
371
  function quoteTransferRemoteTo(
315
372
  uint32 _destination,
316
373
  bytes32 _recipient,
317
374
  uint256 _amount,
318
375
  bytes32 _targetRouter
319
- ) external view override returns (Quote[] memory quotes) {
376
+ ) public view override returns (Quote[] memory quotes) {
377
+ _requireAuthorizedRouter(_destination, _targetRouter);
378
+ if (_destination == localDomain) {
379
+ require(
380
+ _targetRouter.bytes32ToAddress().code.length > 0,
381
+ "CCR: target router not contract"
382
+ );
383
+ }
384
+
320
385
  quotes = new Quote[](3);
321
386
 
322
387
  // Same-domain: handle() called directly, no interchain gas
323
388
  uint256 gasQuote = 0;
324
389
  address _feeToken = feeToken();
325
390
  if (_destination != localDomain) {
326
- gasQuote = _quoteGasPayment(
391
+ gasQuote = _quoteGasPaymentTo(
327
392
  _destination,
328
393
  _recipient,
329
394
  _outboundAmount(_amount),
330
- _feeToken
395
+ _feeToken,
396
+ _targetRouter
331
397
  );
332
398
  }
333
399
  quotes[0] = Quote({token: _feeToken, amount: gasQuote});
@@ -346,4 +412,23 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
346
412
  amount: _externalFeeAmount(_destination, _recipient, _amount)
347
413
  });
348
414
  }
415
+
416
+ /// @dev Target-router-aware gas quote helper. Avoids Router._mustHaveRemoteRouter().
417
+ /// Caller must validate `_targetRouter` is authorized for `_destination`.
418
+ function _quoteGasPaymentTo(
419
+ uint32 _destination,
420
+ bytes32 _recipient,
421
+ uint256 _amount,
422
+ address _feeToken,
423
+ bytes32 _targetRouter
424
+ ) internal view returns (uint256) {
425
+ return
426
+ mailbox.quoteDispatch(
427
+ _destination,
428
+ _targetRouter,
429
+ TokenMessage.format(_recipient, _amount),
430
+ _generateHookMetadata(_destination, _feeToken),
431
+ IPostDispatchHook(address(hook))
432
+ );
433
+ }
349
434
  }
@@ -1,18 +1,21 @@
1
1
  // SPDX-License-Identifier: MIT OR Apache-2.0
2
2
  pragma solidity >=0.8.0;
3
3
 
4
- import {IMultiCollateralFee} from "./interfaces/IMultiCollateralFee.sol";
4
+ import {ICrossCollateralFee} from "./interfaces/ICrossCollateralFee.sol";
5
5
  import {ITokenFee, Quote} from "@hyperlane-xyz/core/interfaces/ITokenBridge.sol";
6
6
  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
7
7
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8
8
  import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
9
9
 
10
10
  /**
11
- * @title MultiCollateralRoutingFee
11
+ * @title CrossCollateralRoutingFee
12
12
  * @notice Routes fee lookups by destination + target router. Delegates to
13
13
  * existing ITokenFee (3-param) fee contracts (LinearFee, ProgressiveFee, etc.).
14
+ * @dev This contract assumes standard ERC20 behavior for fee balances and
15
+ * transfers. Rebasing, fee-on-transfer, and ERC777 tokens are not supported
16
+ * for deterministic fee accounting.
14
17
  */
15
- contract MultiCollateralRoutingFee is IMultiCollateralFee, ITokenFee, Ownable {
18
+ contract CrossCollateralRoutingFee is ICrossCollateralFee, ITokenFee, Ownable {
16
19
  using SafeERC20 for IERC20;
17
20
 
18
21
  /// @notice Sentinel key for destination-level default fee contracts.
@@ -34,7 +37,7 @@ contract MultiCollateralRoutingFee is IMultiCollateralFee, ITokenFee, Ownable {
34
37
  _transferOwnership(_owner);
35
38
  }
36
39
 
37
- function setRouterFeeContracts(
40
+ function setCrossCollateralRouterFeeContracts(
38
41
  uint32[] calldata destinations,
39
42
  bytes32[] calldata targetRouters,
40
43
  address[] calldata _feeContracts
@@ -42,7 +45,7 @@ contract MultiCollateralRoutingFee is IMultiCollateralFee, ITokenFee, Ownable {
42
45
  require(
43
46
  destinations.length == targetRouters.length &&
44
47
  destinations.length == _feeContracts.length,
45
- "MCF: length mismatch"
48
+ "CCRF: length mismatch"
46
49
  );
47
50
 
48
51
  for (uint256 i = 0; i < destinations.length; i++) {
@@ -79,7 +82,7 @@ contract MultiCollateralRoutingFee is IMultiCollateralFee, ITokenFee, Ownable {
79
82
  }
80
83
 
81
84
  /**
82
- * @inheritdoc IMultiCollateralFee
85
+ * @inheritdoc ICrossCollateralFee
83
86
  * @dev Routes: specific router → destination default (DEFAULT_ROUTER).
84
87
  */
85
88
  function quoteTransferRemoteTo(