Skip to content

Commit 6bd4fd1

Browse files
feat: predicate cross collateral wrapper (#8513)
Co-authored-by: Yorke Rhodes IV <yorke@hyperlane.xyz>
1 parent e1f35a7 commit 6bd4fd1

10 files changed

Lines changed: 1647 additions & 342 deletions

.changeset/shiny-hounds-smoke.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+
feat: added PredicateCrossCollateralRouterWrapper.sol and extracted shared predicate wrapper logic into an abstract class
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity >=0.8.0;
3+
4+
interface IPredicateWrapper {
5+
error PredicateRouterWrapper__UnauthorizedTransfer();
6+
error PredicateRouterWrapper__InvalidWarpRoute();
7+
error PredicateRouterWrapper__NativeTokenUnsupported();
8+
error PredicateRouterWrapper__InvalidRegistry();
9+
error PredicateRouterWrapper__InvalidPolicy();
10+
error PredicateRouterWrapper__WithdrawFailed();
11+
error PredicateRouterWrapper__AttestationInvalid();
12+
error PredicateRouterWrapper__InsufficientValue();
13+
error PredicateRouterWrapper__PostDispatchNotExecuted();
14+
error PredicateRouterWrapper__RefundFailed();
15+
error PredicateRouterWrapper__ReentryDetected();
16+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity >=0.8.0;
3+
4+
/*@@@@@@@ @@@@@@@@@
5+
@@@@@@@@@ @@@@@@@@@
6+
@@@@@@@@@ @@@@@@@@@
7+
@@@@@@@@@ @@@@@@@@@
8+
@@@@@@@@@@@@@@@@@@@@@@@@@
9+
@@@@@ HYPERLANE @@@@@@@
10+
@@@@@@@@@@@@@@@@@@@@@@@@@
11+
@@@@@@@@@ @@@@@@@@@
12+
@@@@@@@@@ @@@@@@@@@
13+
@@@@@@@@@ @@@@@@@@@
14+
@@@@@@@@@ @@@@@@@@*/
15+
16+
// ============ Internal Imports ============
17+
import {Quote} from "../../interfaces/ITokenBridge.sol";
18+
import {IPredicateWrapper} from "../../interfaces/IPredicateWrapper.sol";
19+
import {AbstractPredicateWrapper} from "../libs/AbstractPredicateWrapper.sol";
20+
21+
// ============ Predicate Imports ============
22+
import {Attestation} from "@predicate/interfaces/IPredicateRegistry.sol";
23+
24+
// ============ Local Imports ============
25+
import {CrossCollateralRouter} from "../CrossCollateralRouter.sol";
26+
import {ICrossCollateralFee} from "../interfaces/ICrossCollateralFee.sol";
27+
28+
/**
29+
* @title PredicateCrossCollateralRouterWrapper
30+
* @author Abacus Works
31+
* @notice Wraps an existing CrossCollateralRouter with Predicate attestation validation.
32+
* Acts as BOTH a user entry point AND a post-dispatch hook.
33+
* @dev Security model:
34+
* 1. User calls transferRemoteWithAttestation() or transferRemoteToWithAttestation()
35+
* 2. Wrapper validates attestation via PredicateClient, sets pendingAttestation = true
36+
* 3. Wrapper calls router.transferRemote() or transferRemoteTo()
37+
* 4. For cross-domain: CrossCollateralRouter dispatches message, mailbox calls postDispatch()
38+
* 5. For same-domain: CrossCollateralRouter calls handle() directly, no postDispatch
39+
* 6. postDispatch() verifies pendingAttestation == true (cross-domain only), then clears it
40+
*
41+
* If someone bypasses wrapper and calls the router directly, postDispatch()
42+
* will revert because pendingAttestation will be false.
43+
*
44+
* Usage:
45+
* 1. Deploy PredicateCrossCollateralRouterWrapper pointing to existing CrossCollateralRouter
46+
* 2. Set PredicateCrossCollateralRouterWrapper as the hook: router.setHook(predicateWrapper)
47+
* 3. Optionally aggregate with default hook for IGP using StaticAggregationHook
48+
* 4. Users call wrapper.transferRemoteWithAttestation() or transferRemoteToWithAttestation()
49+
*
50+
* @custom:oz-version 4.9.x (uses Ownable without constructor argument)
51+
*/
52+
contract PredicateCrossCollateralRouterWrapper is
53+
AbstractPredicateWrapper,
54+
ICrossCollateralFee
55+
{
56+
// ============ Events ============
57+
58+
/// @notice Emitted when a transfer is authorized via attestation
59+
event TransferAuthorized(
60+
address indexed sender,
61+
uint32 indexed destination,
62+
bytes32 indexed recipient,
63+
uint256 amount,
64+
bytes32 targetRouter,
65+
string uuid
66+
);
67+
68+
// ============ Constructor ============
69+
70+
constructor(
71+
address _crossCollateralRouter,
72+
address _registry,
73+
string memory _policyID
74+
) AbstractPredicateWrapper(_crossCollateralRouter, _registry, _policyID) {
75+
// CrossCollateralRouter always has a non-zero token (native not supported)
76+
if (address(token) == address(0))
77+
revert IPredicateWrapper
78+
.PredicateRouterWrapper__NativeTokenUnsupported();
79+
}
80+
81+
// ============ External Functions ============
82+
83+
/**
84+
* @notice Transfer tokens to specific target router with Predicate attestation validation
85+
* @param _attestation The Predicate attestation proving compliance
86+
* @param _destination The destination chain domain
87+
* @param _recipient The recipient address on destination (as bytes32)
88+
* @param _amount The amount of tokens to transfer
89+
* @param _targetRouter The enrolled router to receive the message on destination
90+
* @return messageId The Hyperlane message ID (0 for same-domain transfers)
91+
*/
92+
function transferRemoteToWithAttestation(
93+
Attestation calldata _attestation,
94+
uint32 _destination,
95+
bytes32 _recipient,
96+
uint256 _amount,
97+
bytes32 _targetRouter
98+
) external payable returns (bytes32 messageId) {
99+
CrossCollateralRouter ccr = CrossCollateralRouter(address(warpRoute));
100+
101+
bytes memory encodedSigAndArgs = abi.encodeWithSelector(
102+
CrossCollateralRouter.transferRemoteTo.selector,
103+
_destination,
104+
_recipient,
105+
_amount,
106+
_targetRouter
107+
);
108+
109+
Quote[] memory quotes = ccr.quoteTransferRemoteTo(
110+
_destination,
111+
_recipient,
112+
_amount,
113+
_targetRouter
114+
);
115+
116+
emit TransferAuthorized(
117+
msg.sender,
118+
_destination,
119+
_recipient,
120+
_amount,
121+
_targetRouter,
122+
_attestation.uuid
123+
);
124+
125+
return
126+
_executeAttested(
127+
_attestation,
128+
encodedSigAndArgs,
129+
quotes,
130+
_destination
131+
);
132+
}
133+
134+
// ========== ICrossCollateralFee Implementation ==========
135+
136+
/**
137+
* @notice Quotes the fees to a specific target router by delegating to the underlying
138+
* cross collateral route
139+
*/
140+
function quoteTransferRemoteTo(
141+
uint32 _destination,
142+
bytes32 _recipient,
143+
uint256 _amount,
144+
bytes32 _targetRouter
145+
) external view override returns (Quote[] memory quotes) {
146+
return
147+
CrossCollateralRouter(address(warpRoute)).quoteTransferRemoteTo(
148+
_destination,
149+
_recipient,
150+
_amount,
151+
_targetRouter
152+
);
153+
}
154+
155+
// ============ Internal Overrides ============
156+
157+
function _emitTransferAuthorized(
158+
address sender,
159+
uint32 destination,
160+
bytes32 recipient,
161+
uint256 amount,
162+
string calldata uuid
163+
) internal override {
164+
emit TransferAuthorized(
165+
sender,
166+
destination,
167+
recipient,
168+
amount,
169+
bytes32(0),
170+
uuid
171+
);
172+
}
173+
}

0 commit comments

Comments
 (0)