@@ -43,6 +43,7 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
4343 using TypeCasts for bytes32 ;
4444 using SafeERC20 for IERC20 ;
4545 using EnumerableSet for EnumerableSet.Bytes32Set;
46+ using EnumerableSet for EnumerableSet.UintSet;
4647
4748 // ============ Events ============
4849
@@ -55,6 +56,10 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
5556 /// enrolled remote router). Local routers use localDomain as key.
5657 mapping (uint32 => EnumerableSet.Bytes32Set) private _enrolledRouters;
5758
59+ /// @notice Tracks which domains have at least one MC-enrolled router,
60+ /// enabling on-chain enumeration for the SDK reader.
61+ EnumerableSet.UintSet private _enrolledDomains;
62+
5863 // ============ Constructor ============
5964
6065 constructor (
@@ -73,6 +78,7 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
7378 require (_domains.length == _routers.length , "MC: length mismatch " );
7479 for (uint256 i = 0 ; i < _domains.length ; i++ ) {
7580 if (_enrolledRouters[_domains[i]].add (_routers[i])) {
81+ _enrolledDomains.add (uint256 (_domains[i]));
7682 emit RouterEnrolled (_domains[i], _routers[i]);
7783 }
7884 }
@@ -85,6 +91,9 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
8591 require (_domains.length == _routers.length , "MC: length mismatch " );
8692 for (uint256 i = 0 ; i < _domains.length ; i++ ) {
8793 if (_enrolledRouters[_domains[i]].remove (_routers[i])) {
94+ if (_enrolledRouters[_domains[i]].length () == 0 ) {
95+ _enrolledDomains.remove (uint256 (_domains[i]));
96+ }
8897 emit RouterUnenrolled (_domains[i], _routers[i]);
8998 }
9099 }
@@ -105,6 +114,50 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
105114 return _enrolledRouters[_domain].values ();
106115 }
107116
117+ /// @notice Returns all domains that have at least one MC-enrolled router.
118+ function getEnrolledDomains ()
119+ external
120+ view
121+ returns (uint32 [] memory domains )
122+ {
123+ uint256 len = _enrolledDomains.length ();
124+ domains = new uint32 [](len);
125+ for (uint256 i = 0 ; i < len; i++ ) {
126+ domains[i] = uint32 (_enrolledDomains.at (i));
127+ }
128+ }
129+
130+ // ============ Destination Gas Override ============
131+
132+ /// @dev Overrides GasRouter._setDestinationGas to also accept MC-enrolled
133+ /// domains (not just default Router._routers). Excludes localDomain since
134+ /// same-chain transfers skip mailbox dispatch.
135+ function _setDestinationGas (uint32 domain , uint256 gas ) internal override {
136+ require (domain != localDomain, "MC: no gas for local domain " );
137+ require (
138+ routers (domain) != bytes32 (0 ) ||
139+ _enrolledRouters[domain].length () > 0 ,
140+ "MC: domain has no routers "
141+ );
142+ destinationGas[domain] = gas;
143+ emit GasSet (domain, gas);
144+ }
145+
146+ // ============ Internal Helpers ============
147+
148+ /// @dev Reverts unless `_router` is enrolled for `_domain` (either via the
149+ /// standard Router._routers map or via the MC-specific _enrolledRouters set).
150+ function _requireAuthorizedRouter (
151+ uint32 _domain ,
152+ bytes32 _router
153+ ) internal view {
154+ require (
155+ _isRemoteRouter (_domain, _router) ||
156+ _enrolledRouters[_domain].contains (_router),
157+ "MC: unauthorized router "
158+ );
159+ }
160+
108161 // ============ Handle Override ============
109162
110163 /// @dev Overrides `Router.handle` from core (`client/Router.sol`) via
@@ -119,11 +172,7 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
119172 ) external payable override {
120173 if (msg .sender == address (mailbox)) {
121174 // Cross-chain via mailbox: sender must be enrolled
122- require (
123- _isRemoteRouter (_origin, _sender) ||
124- _enrolledRouters[_origin].contains (_sender),
125- "MC: unauthorized router "
126- );
175+ _requireAuthorizedRouter (_origin, _sender);
127176 } else {
128177 // Same-chain direct call: caller must be an enrolled router
129178 require (
@@ -197,11 +246,12 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
197246 // Same-domain transferRemoteTo calls handle() directly and does not dispatch
198247 // through mailbox hooks, so do not charge hook fees in that path.
199248 if (_feeHook != address (0 ) && _destination != localDomain) {
200- uint256 hookFee = _quoteGasPayment (
249+ uint256 hookFee = _quoteGasPaymentTo (
201250 _destination,
202251 _recipient,
203- _amount,
204- _token
252+ _outboundAmount (_amount),
253+ _token,
254+ _targetRouter
205255 );
206256 if (hookFee > 0 ) {
207257 if (_token != address (this )) {
@@ -263,12 +313,10 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
263313 uint256 _amount ,
264314 bytes32 _targetRouter
265315 ) public payable returns (bytes32 messageId ) {
266- require (
267- _isRemoteRouter (_destination, _targetRouter) ||
268- _enrolledRouters[_destination].contains (_targetRouter),
269- "MC: unauthorized router "
270- );
316+ _requireAuthorizedRouter (_destination, _targetRouter);
271317 if (_destination == localDomain) {
318+ // Local transfers call handle() directly without mailbox dispatch,
319+ // so any msg.value would be stuck in this contract permanently.
272320 require (msg .value == 0 , "MC: local transfer no msg.value " );
273321 }
274322
@@ -317,17 +365,26 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
317365 uint256 _amount ,
318366 bytes32 _targetRouter
319367 ) external view override returns (Quote[] memory quotes ) {
368+ _requireAuthorizedRouter (_destination, _targetRouter);
369+ if (_destination == localDomain) {
370+ require (
371+ _targetRouter.bytes32ToAddress ().code.length > 0 ,
372+ "MC: target router not contract "
373+ );
374+ }
375+
320376 quotes = new Quote [](3 );
321377
322378 // Same-domain: handle() called directly, no interchain gas
323379 uint256 gasQuote = 0 ;
324380 address _feeToken = feeToken ();
325381 if (_destination != localDomain) {
326- gasQuote = _quoteGasPayment (
382+ gasQuote = _quoteGasPaymentTo (
327383 _destination,
328384 _recipient,
329385 _outboundAmount (_amount),
330- _feeToken
386+ _feeToken,
387+ _targetRouter
331388 );
332389 }
333390 quotes[0 ] = Quote ({token: _feeToken, amount: gasQuote});
@@ -346,4 +403,23 @@ contract MultiCollateral is HypERC20Collateral, IMultiCollateralFee {
346403 amount: _externalFeeAmount (_destination, _recipient, _amount)
347404 });
348405 }
406+
407+ /// @dev Target-router-aware gas quote helper. Avoids Router._mustHaveRemoteRouter().
408+ /// Caller must validate `_targetRouter` is authorized for `_destination`.
409+ function _quoteGasPaymentTo (
410+ uint32 _destination ,
411+ bytes32 _recipient ,
412+ uint256 _amount ,
413+ address _feeToken ,
414+ bytes32 _targetRouter
415+ ) internal view returns (uint256 ) {
416+ return
417+ mailbox.quoteDispatch (
418+ _destination,
419+ _targetRouter,
420+ TokenMessage.format (_recipient, _amount),
421+ _generateHookMetadata (_destination, _feeToken),
422+ IPostDispatchHook (address (hook))
423+ );
424+ }
349425}
0 commit comments