@across-protocol/contracts 5.0.18 → 5.0.19

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 (39) hide show
  1. package/contracts/interfaces/ICounterfactualBeacon.sol +91 -0
  2. package/contracts/interfaces/ICounterfactualDeposit.sol +21 -0
  3. package/contracts/libraries/TronClones.sol +13 -0
  4. package/contracts/periphery/counterfactual/CounterfactualBeacon.sol +115 -0
  5. package/contracts/periphery/counterfactual/CounterfactualBeaconBase.sol +121 -0
  6. package/contracts/periphery/counterfactual/CounterfactualBeaconBootstrap.sol +39 -0
  7. package/contracts/periphery/counterfactual/CounterfactualDeposit.sol +109 -28
  8. package/contracts/periphery/counterfactual/CounterfactualDepositCCTP.sol +102 -57
  9. package/contracts/periphery/counterfactual/CounterfactualDepositFactory.sol +61 -83
  10. package/contracts/periphery/counterfactual/CounterfactualDepositFactoryTron.sol +14 -24
  11. package/contracts/periphery/counterfactual/CounterfactualDepositOFT.sol +119 -58
  12. package/contracts/periphery/counterfactual/CounterfactualDepositSpokePool.sol +122 -90
  13. package/contracts/periphery/counterfactual/CounterfactualDepositSpokePoolTr.sol +10 -21
  14. package/contracts/periphery/counterfactual/CounterfactualDepositVanillaCCTP.sol +170 -0
  15. package/contracts/periphery/counterfactual/CounterfactualImplementationBase.sol +51 -0
  16. package/dist/broadcast/deployed-addresses.json +6 -6
  17. package/dist/evm/artifacts/AdminWithdrawManager.sol/AdminWithdrawManager.json +1 -1
  18. package/dist/evm/artifacts/CounterfactualBeacon.sol/CounterfactualBeacon.json +1 -0
  19. package/dist/evm/artifacts/CounterfactualBeaconBase.sol/CounterfactualBeaconBase.json +1 -0
  20. package/dist/evm/artifacts/CounterfactualBeaconBase.sol/IBeaconTarget.json +1 -0
  21. package/dist/evm/artifacts/CounterfactualBeaconBootstrap.sol/CounterfactualBeaconBootstrap.json +1 -0
  22. package/dist/evm/artifacts/CounterfactualDeposit.sol/CounterfactualDeposit.json +1 -1
  23. package/dist/evm/artifacts/CounterfactualDepositCCTP.sol/CounterfactualDepositCCTP.json +1 -1
  24. package/dist/evm/artifacts/CounterfactualDepositFactory.sol/CounterfactualDepositFactory.json +1 -1
  25. package/dist/evm/artifacts/CounterfactualDepositFactoryTron.sol/CounterfactualDepositFactoryTron.json +1 -1
  26. package/dist/evm/artifacts/CounterfactualDepositOFT.sol/CounterfactualDepositOFT.json +1 -1
  27. package/dist/evm/artifacts/CounterfactualDepositOFT.sol/ISponsoredOFTSrcPeriphery.json +1 -1
  28. package/dist/evm/artifacts/CounterfactualDepositSpokePool.sol/CounterfactualDepositSpokePool.json +1 -1
  29. package/dist/evm/artifacts/CounterfactualDepositSpokePoolTr.sol/CounterfactualDepositSpokePoolTr.json +1 -1
  30. package/dist/evm/artifacts/CounterfactualDepositVanillaCCTP.sol/CounterfactualDepositVanillaCCTP.json +1 -0
  31. package/dist/evm/artifacts/CounterfactualImplementationBase.sol/CounterfactualImplementationBase.json +1 -0
  32. package/dist/evm/artifacts/CounterfactualTestBase.sol/CounterfactualTestBase.json +1 -0
  33. package/dist/evm/artifacts/ICounterfactualBeacon.sol/ICounterfactualBeacon.json +1 -0
  34. package/dist/evm/artifacts/ICounterfactualDeposit.sol/ICounterfactualDeposit.json +1 -1
  35. package/dist/evm/artifacts/Ownable2StepUpgradeable.sol/Ownable2StepUpgradeable.json +1 -0
  36. package/dist/evm/artifacts/OwnableUpgradeable.sol/OwnableUpgradeable.json +1 -1
  37. package/dist/evm/artifacts/UUPSUpgradeable.sol/UUPSUpgradeable.json +1 -1
  38. package/package.json +1 -1
  39. package/dist/evm/artifacts/Clones.sol/Clones.json +0 -1
@@ -3,24 +3,31 @@ pragma solidity ^0.8.0;
3
3
 
4
4
  import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
5
  import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6
+ import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
7
+ import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
6
8
  import { SponsoredOFTInterface } from "../../interfaces/SponsoredOFTInterface.sol";
7
9
  import { ICounterfactualImplementation } from "../../interfaces/ICounterfactualImplementation.sol";
10
+ import { CounterfactualImplementationBase } from "./CounterfactualImplementationBase.sol";
8
11
 
9
- /**
10
- * @notice Minimal interface for calling deposit on SponsoredOFTSrcPeriphery
11
- * @custom:security-contact bugs@across.to
12
- */
12
+ /// @notice Minimal interface for SponsoredOFTSrcPeriphery: `deposit` plus its immutable `TOKEN`.
13
13
  interface ISponsoredOFTSrcPeriphery {
14
14
  function deposit(SponsoredOFTInterface.Quote calldata quote, bytes calldata signature) external payable;
15
+
16
+ /// @notice The single ERC-20 this periphery deposits (pulled from `msg.sender`).
17
+ function TOKEN() external view returns (address);
15
18
  }
16
19
 
17
20
  /**
18
- * @notice Route parameters committed to in the merkle leaf.
21
+ * @notice Route parameters committed to in the merkle leaf (chain-agnostic: no source chain, no token).
22
+ * @dev `peripheryGetter` is the selector of the beacon getter for the SponsoredOFTSrcPeriphery to use
23
+ * (e.g. `beacon.oftSrcPeriphery.selector`). Each OFT periphery is single-token (immutable `TOKEN`), so
24
+ * naming the periphery selects the input token — supporting many OFT tokens with one leaf shape. The
25
+ * source EID comes from `beacon.oftSrcEid()`.
19
26
  */
20
- struct OFTDepositParams {
27
+ struct OFTRouteParams {
28
+ bytes4 peripheryGetter;
21
29
  uint32 dstEid;
22
30
  bytes32 destinationHandler;
23
- address token;
24
31
  uint256 maxOftFeeBps;
25
32
  uint256 lzReceiveGasLimit;
26
33
  uint256 lzComposeGasLimit;
@@ -33,7 +40,9 @@ struct OFTDepositParams {
33
40
  uint8 executionMode;
34
41
  address refundRecipient;
35
42
  bytes actionData;
36
- uint256 executionFee;
43
+ /// @dev Selector of the beacon getter for this route's per-chain execution-fee cap (e.g.
44
+ /// `beacon.usdtOftMaxExecutionFee.selector`).
45
+ bytes4 maxExecutionFeeGetter;
37
46
  }
38
47
 
39
48
  /**
@@ -44,17 +53,22 @@ struct OFTSubmitterData {
44
53
  address executionFeeRecipient;
45
54
  bytes32 nonce;
46
55
  uint256 oftDeadline;
47
- bytes signature;
56
+ uint256 executionFee;
57
+ uint32 signatureDeadline;
58
+ bytes peripherySignature;
59
+ bytes counterfactualSignature;
48
60
  }
49
61
 
50
62
  /**
51
63
  * @title CounterfactualDepositOFT
52
- * @notice Implementation contract for counterfactual deposits via SponsoredOFT.
53
- * @dev Called via delegatecall from the CounterfactualDeposit dispatcher.
54
- * msg.value covers LayerZero native messaging fees.
64
+ * @notice Counterfactual deposit via SponsoredOFT (LayerZero).
65
+ * @dev Delegatecalled by the dispatcher. Source EID and fee signer come from the beacon; the periphery from
66
+ * the beacon getter the leaf's `peripheryGetter` names, and the input token from that periphery's
67
+ * immutable `TOKEN` — so the impl is token-agnostic, holds no chain-specific values, and has one
68
+ * address per chain. `msg.value` covers LayerZero messaging fees.
55
69
  * @custom:security-contact bugs@across.to
56
70
  */
57
- contract CounterfactualDepositOFT is ICounterfactualImplementation {
71
+ contract CounterfactualDepositOFT is CounterfactualImplementationBase, EIP712 {
58
72
  using SafeERC20 for IERC20;
59
73
 
60
74
  /**
@@ -63,72 +77,119 @@ contract CounterfactualDepositOFT is ICounterfactualImplementation {
63
77
  * @param executionFeeRecipient Address that received the execution fee.
64
78
  * @param nonce OFT nonce used for the deposit.
65
79
  * @param oftDeadline Deadline timestamp for the OFT quote.
80
+ * @param executionFee Execution fee paid to the executor (in input token).
66
81
  */
67
- event OFTDepositExecuted(uint256 amount, address indexed executionFeeRecipient, bytes32 nonce, uint256 oftDeadline);
82
+ event OFTDepositExecuted(
83
+ uint256 amount,
84
+ address indexed executionFeeRecipient,
85
+ bytes32 nonce,
86
+ uint256 oftDeadline,
87
+ uint256 executionFee
88
+ );
68
89
 
69
- /// @notice SponsoredOFTSrcPeriphery contract
70
- address public immutable oftSrcPeriphery;
90
+ error InvalidSignature();
91
+ error SignatureExpired();
92
+ error MaxExecutionFee();
71
93
 
72
- /// @notice OFT source endpoint ID for this chain
73
- uint32 public immutable srcEid;
94
+ /// @notice EIP-712 typehash binding the local fee signature to the route, nonce, runtime fee, and deadline.
95
+ bytes32 public constant EXECUTE_OFT_TYPEHASH =
96
+ keccak256("ExecuteOFT(bytes32 routeParamsHash,bytes32 nonce,uint256 executionFee,uint32 signatureDeadline)");
74
97
 
75
- constructor(address _oftSrcPeriphery, uint32 _srcEid) {
76
- oftSrcPeriphery = _oftSrcPeriphery;
77
- srcEid = _srcEid;
78
- }
98
+ constructor() EIP712("CounterfactualDepositOFT", "v2.0.0") {}
79
99
 
80
100
  /**
81
101
  * @inheritdoc ICounterfactualImplementation
82
- * @dev Bridges tokens via SponsoredOFT (LayerZero). `params` is ABI-encoded as `OFTDepositParams`;
83
- * `submitterData` as `OFTSubmitterData` (includes a signature forwarded to the OFT periphery).
84
- * ERC-20 only. Forwards `msg.value` for LayerZero messaging fees. No local signature verification.
102
+ * @dev Bridges tokens via SponsoredOFT (LayerZero). `routeParamsEncoded` is ABI-encoded as `OFTRouteParams`;
103
+ * `submitterDataEncoded` as `OFTSubmitterData` (includes a signature forwarded to the OFT periphery).
104
+ * ERC-20 only — the token is the periphery's immutable `TOKEN`. Forwards `msg.value` for LayerZero
105
+ * messaging fees. The local fee signature binds the route (`routeParamsHash`, which includes the
106
+ * `peripheryGetter`); `amount` is bound transitively via the periphery quote signature.
85
107
  */
86
- function execute(bytes calldata params, bytes calldata submitterData) external payable {
87
- OFTDepositParams memory dp = abi.decode(params, (OFTDepositParams));
88
- OFTSubmitterData memory sd = abi.decode(submitterData, (OFTSubmitterData));
108
+ function execute(bytes calldata routeParamsEncoded, bytes calldata submitterDataEncoded) external payable {
109
+ OFTRouteParams memory routeParams = abi.decode(routeParamsEncoded, (OFTRouteParams));
110
+ OFTSubmitterData memory submitterData = abi.decode(submitterDataEncoded, (OFTSubmitterData));
111
+
112
+ _verifySignature(keccak256(routeParamsEncoded), submitterData);
113
+ if (submitterData.executionFee > _resolveBeaconUint(routeParams.maxExecutionFeeGetter))
114
+ revert MaxExecutionFee();
89
115
 
90
- if (dp.executionFee > 0) IERC20(dp.token).safeTransfer(sd.executionFeeRecipient, dp.executionFee);
116
+ // Periphery chosen by the leaf's selector; input token is that periphery's immutable `TOKEN`.
117
+ address oftSrcPeriphery = _requireConfigured(_resolveBeaconAddress(routeParams.peripheryGetter));
118
+ address inputToken = ISponsoredOFTSrcPeriphery(oftSrcPeriphery).TOKEN();
91
119
 
92
- uint256 depositAmount = sd.amount - dp.executionFee;
120
+ // Fee paid before the periphery call (load-bearing): the local signature binds the route and
121
+ // (nonce, fee, deadline) but not `amount`, so amount-replay protection is the periphery's nonce
122
+ // check — a replayed fee reverts at `deposit` and rolls back this transfer.
123
+ if (submitterData.executionFee > 0)
124
+ IERC20(inputToken).safeTransfer(submitterData.executionFeeRecipient, submitterData.executionFee);
93
125
 
94
- IERC20(dp.token).forceApprove(oftSrcPeriphery, depositAmount);
126
+ uint256 depositAmount = submitterData.amount - submitterData.executionFee;
95
127
 
96
- _deposit(dp, sd, depositAmount);
128
+ IERC20(inputToken).forceApprove(oftSrcPeriphery, depositAmount);
97
129
 
98
- emit OFTDepositExecuted(sd.amount, sd.executionFeeRecipient, sd.nonce, sd.oftDeadline);
130
+ _deposit(oftSrcPeriphery, routeParams, submitterData, depositAmount);
131
+
132
+ emit OFTDepositExecuted(
133
+ submitterData.amount,
134
+ submitterData.executionFeeRecipient,
135
+ submitterData.nonce,
136
+ submitterData.oftDeadline,
137
+ submitterData.executionFee
138
+ );
99
139
  }
100
140
 
101
- /**
102
- * @notice Calls deposit on the SponsoredOFTSrcPeriphery with the constructed quote.
103
- * @param dp Route parameters from the merkle leaf.
104
- * @param sd Submitter-provided execution data.
105
- * @param depositAmount Amount to deposit after deducting the execution fee.
106
- */
107
- function _deposit(OFTDepositParams memory dp, OFTSubmitterData memory sd, uint256 depositAmount) private {
141
+ function _verifySignature(bytes32 routeParamsHash, OFTSubmitterData memory submitterData) private view {
142
+ if (block.timestamp > submitterData.signatureDeadline) revert SignatureExpired();
143
+ bytes32 structHash = keccak256(
144
+ abi.encode(
145
+ EXECUTE_OFT_TYPEHASH,
146
+ routeParamsHash,
147
+ submitterData.nonce,
148
+ submitterData.executionFee,
149
+ submitterData.signatureDeadline
150
+ )
151
+ );
152
+ if (ECDSA.recover(_hashTypedDataV4(structHash), submitterData.counterfactualSignature) != _beacon().signer())
153
+ revert InvalidSignature();
154
+ }
155
+
156
+ /// @notice Calls `deposit` on the SponsoredOFTSrcPeriphery with the constructed quote.
157
+ /// @param oftSrcPeriphery The OFT periphery (resolved from the leaf's selector).
158
+ /// @param routeParams Route parameters from the merkle leaf.
159
+ /// @param submitterData Submitter-provided execution data.
160
+ /// @param depositAmount Amount to deposit after deducting the execution fee.
161
+ function _deposit(
162
+ address oftSrcPeriphery,
163
+ OFTRouteParams memory routeParams,
164
+ OFTSubmitterData memory submitterData,
165
+ uint256 depositAmount
166
+ ) private {
108
167
  ISponsoredOFTSrcPeriphery(oftSrcPeriphery).deposit{ value: msg.value }(
109
168
  SponsoredOFTInterface.Quote({
110
169
  signedParams: SponsoredOFTInterface.SignedQuoteParams({
111
- srcEid: srcEid,
112
- dstEid: dp.dstEid,
113
- destinationHandler: dp.destinationHandler,
170
+ srcEid: _beacon().oftSrcEid(),
171
+ dstEid: routeParams.dstEid,
172
+ destinationHandler: routeParams.destinationHandler,
114
173
  amountLD: depositAmount,
115
- nonce: sd.nonce,
116
- deadline: sd.oftDeadline,
117
- maxBpsToSponsor: dp.maxBpsToSponsor,
118
- maxUserSlippageBps: dp.maxUserSlippageBps,
119
- finalRecipient: dp.finalRecipient,
120
- finalToken: dp.finalToken,
121
- destinationDex: dp.destinationDex,
122
- lzReceiveGasLimit: dp.lzReceiveGasLimit,
123
- lzComposeGasLimit: dp.lzComposeGasLimit,
124
- maxOftFeeBps: dp.maxOftFeeBps,
125
- accountCreationMode: dp.accountCreationMode,
126
- executionMode: dp.executionMode,
127
- actionData: dp.actionData
174
+ nonce: submitterData.nonce,
175
+ deadline: submitterData.oftDeadline,
176
+ maxBpsToSponsor: routeParams.maxBpsToSponsor,
177
+ maxUserSlippageBps: routeParams.maxUserSlippageBps,
178
+ finalRecipient: routeParams.finalRecipient,
179
+ finalToken: routeParams.finalToken,
180
+ destinationDex: routeParams.destinationDex,
181
+ lzReceiveGasLimit: routeParams.lzReceiveGasLimit,
182
+ lzComposeGasLimit: routeParams.lzComposeGasLimit,
183
+ maxOftFeeBps: routeParams.maxOftFeeBps,
184
+ accountCreationMode: routeParams.accountCreationMode,
185
+ executionMode: routeParams.executionMode,
186
+ actionData: routeParams.actionData
128
187
  }),
129
- unsignedParams: SponsoredOFTInterface.UnsignedQuoteParams({ refundRecipient: dp.refundRecipient })
188
+ unsignedParams: SponsoredOFTInterface.UnsignedQuoteParams({
189
+ refundRecipient: routeParams.refundRecipient
190
+ })
130
191
  }),
131
- sd.signature
192
+ submitterData.peripherySignature
132
193
  );
133
194
  }
134
195
  }
@@ -6,23 +6,32 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s
6
6
  import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
7
7
  import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
8
8
  import { V3SpokePoolInterface } from "../../interfaces/V3SpokePoolInterface.sol";
9
+ import { ICounterfactualBeacon } from "../../interfaces/ICounterfactualBeacon.sol";
9
10
  import { ICounterfactualImplementation } from "../../interfaces/ICounterfactualImplementation.sol";
10
- import { NATIVE_ASSET, BPS_SCALAR } from "./CounterfactualConstants.sol";
11
+ import { CounterfactualImplementationBase } from "./CounterfactualImplementationBase.sol";
12
+ import { BPS_SCALAR } from "./CounterfactualConstants.sol";
11
13
  import { SafeTransferERC20 } from "../../libraries/SafeTransferERC20.sol";
12
14
 
13
15
  /**
14
- * @notice Route parameters committed to in the merkle leaf.
16
+ * @notice Route parameters committed to in the merkle leaf (chain-agnostic: no source chain, no token).
17
+ * @dev `inputTokenGetter` is the selector of the beacon getter resolving the per-chain input token (e.g.
18
+ * `beacon.usdc.selector`). Native isn't a special selector: the resolved value signals it — the
19
+ * sentinel (`0xEeee…EEeE`) ⇒ native, any other address ⇒ ERC-20 — so e.g. a `beacon.nativeToken`
20
+ * leaf is native where the beacon returns the sentinel and ERC-20 where it returns a token.
21
+ * `destinationChainId`, `outputToken`, `recipient` are the (chain-invariant) destination identity.
15
22
  */
16
- struct SpokePoolDepositParams {
23
+ struct SpokePoolRouteParams {
24
+ bytes4 inputTokenGetter;
17
25
  uint256 destinationChainId;
18
- bytes32 inputToken;
19
26
  bytes32 outputToken;
20
27
  bytes32 recipient;
21
28
  bytes message;
29
+ bool checkStableExchangeRate;
22
30
  uint256 stableExchangeRate;
23
- uint256 maxFeeFixed;
31
+ /// @dev Selector of the beacon getter for this route's per-chain fixed fee cap (e.g.
32
+ /// `beacon.usdcSpokePoolMaxExecutionFee.selector`); added to the `maxFeeBps` term below.
33
+ bytes4 maxExecutionFeeGetter;
24
34
  uint256 maxFeeBps;
25
- uint256 executionFee;
26
35
  }
27
36
 
28
37
  /**
@@ -37,29 +46,36 @@ struct SpokePoolSubmitterData {
37
46
  uint32 quoteTimestamp;
38
47
  uint32 fillDeadline;
39
48
  uint32 signatureDeadline;
49
+ uint256 executionFee;
40
50
  bytes signature;
41
51
  }
42
52
 
43
53
  /**
44
54
  * @title CounterfactualDepositSpokePool
45
- * @notice Implementation contract for counterfactual deposits via Across SpokePool.
46
- * @dev Called via delegatecall from the CounterfactualDeposit dispatcher. EIP-712 domain separator uses
47
- * `address(this)` (the clone address) to prevent cross-clone replay attacks. No nonce is needed:
48
- * token balance is consumed on execution (natural replay protection), and short deadlines bound the window.
55
+ * @notice Counterfactual deposit via Across SpokePool, agnostic to the input token.
56
+ * @dev Delegatecalled by the dispatcher. SpokePool, wrapped native token and fee signer come from the
57
+ * beacon; the input token from the beacon getter the leaf's `inputTokenGetter` names. Native vs ERC-20
58
+ * is the resolved value (`NATIVE_SENTINEL` msg.value path, input is `beacon.wrappedNativeToken()`;
59
+ * else ⇒ ERC-20). Holds no chain-specific values; one address per chain.
49
60
  *
50
- * Depositor-driven speed-ups are not supported: the `depositor` passed to `SpokePool.deposit()` is
51
- * `address(this)` (the clone), which has no private key and does not implement EIP-1271, and therefore
52
- * cannot sign `speedUpV3Deposit` messages.
61
+ * No per-token variants or per-variant EIP-712 names: `inputTokenGetter` is in `params`
62
+ * `routeParamsHash`, which the fee signature binds, so a signature for one token can't validate for
63
+ * another. Cross-chain replay is prevented by the domain `chainId`, cross-clone by `verifyingContract`.
64
+ * No nonce needed (balance is consumed on execution; short deadlines bound the window). Depositor
65
+ * speed-ups are unsupported: `depositor` is `address(this)` (the clone), which can't sign.
53
66
  * @custom:security-contact bugs@across.to
54
67
  */
55
- contract CounterfactualDepositSpokePool is ICounterfactualImplementation, EIP712, SafeTransferERC20 {
56
- // Restrict the `using` attachment to `forceApprove` only. All `safeTransfer` calls must go
57
- // through the `_safeTransfer` hook (inherited from `SafeTransferERC20`) so chain-specific
58
- // variants can override transfer semantics in one place.
68
+ contract CounterfactualDepositSpokePool is CounterfactualImplementationBase, EIP712, SafeTransferERC20 {
69
+ // `using` is restricted to `forceApprove`; `safeTransfer` goes through the `_safeTransfer` hook so
70
+ // chain-specific variants (Tron) can override transfer semantics in one place.
59
71
  using { SafeERC20.forceApprove } for IERC20;
60
72
 
61
73
  uint256 internal constant EXCHANGE_RATE_SCALAR = 1e18;
62
74
 
75
+ /// @notice Sentinel returned by `inputTokenGetter` to signal a native route (the Aave/Compound-style
76
+ /// native-asset placeholder).
77
+ address public constant NATIVE_SENTINEL = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
78
+
63
79
  /**
64
80
  * @notice Emitted after a SpokePool deposit is successfully executed.
65
81
  * @param inputAmount Total input amount (including execution fee).
@@ -70,6 +86,7 @@ contract CounterfactualDepositSpokePool is ICounterfactualImplementation, EIP712
70
86
  * @param quoteTimestamp Timestamp of the deposit quote.
71
87
  * @param fillDeadline Deadline by which the deposit must be filled.
72
88
  * @param signatureDeadline Deadline after which the authorizing signature expires.
89
+ * @param executionFee Execution fee paid to the executor (in input token).
73
90
  */
74
91
  event SpokePoolDepositExecuted(
75
92
  uint256 inputAmount,
@@ -79,7 +96,8 @@ contract CounterfactualDepositSpokePool is ICounterfactualImplementation, EIP712
79
96
  address indexed executionFeeRecipient,
80
97
  uint32 quoteTimestamp,
81
98
  uint32 fillDeadline,
82
- uint32 signatureDeadline
99
+ uint32 signatureDeadline,
100
+ uint256 executionFee
83
101
  );
84
102
 
85
103
  error MaxFee();
@@ -90,114 +108,128 @@ contract CounterfactualDepositSpokePool is ICounterfactualImplementation, EIP712
90
108
  /// @notice EIP-712 typehash for execute deposit signature verification.
91
109
  bytes32 public constant EXECUTE_DEPOSIT_TYPEHASH =
92
110
  keccak256(
93
- "ExecuteDeposit(uint256 inputAmount,uint256 outputAmount,bytes32 exclusiveRelayer,uint32 exclusivityDeadline,uint32 quoteTimestamp,uint32 fillDeadline,uint32 signatureDeadline)"
111
+ "ExecuteDeposit(address clone,bytes32 routeParamsHash,uint256 inputAmount,uint256 outputAmount,bytes32 exclusiveRelayer,uint32 exclusivityDeadline,uint32 quoteTimestamp,uint32 fillDeadline,uint32 signatureDeadline,uint256 executionFee)"
94
112
  );
95
113
 
96
- /// @notice Across SpokePool contract
97
- address public immutable spokePool;
98
-
99
- /// @notice Signer that authorizes execution parameters
100
- address public immutable signer;
101
-
102
- /// @notice Wrapped native token address (e.g. WETH) passed to SpokePool for native deposits.
103
- address public immutable wrappedNativeToken;
104
-
105
- constructor(
106
- address _spokePool,
107
- address _signer,
108
- address _wrappedNativeToken
109
- ) EIP712("CounterfactualDepositSpokePool", "v1.0.0") {
110
- spokePool = _spokePool;
111
- signer = _signer;
112
- wrappedNativeToken = _wrappedNativeToken;
113
- }
114
+ constructor() EIP712("CounterfactualDepositSpokePool", "v2.0.0") {} // solhint-disable-line no-empty-blocks
114
115
 
115
116
  /**
116
117
  * @inheritdoc ICounterfactualImplementation
117
- * @dev Deposits into the Across SpokePool. `params` is ABI-encoded as `SpokePoolDepositParams`;
118
- * `submitterData` as `SpokePoolSubmitterData` (includes an EIP-712 signature from `signer`).
119
- * Supports native-token deposits. Reverts: `SignatureExpired`, `InvalidSignature`, `MaxFee`,
120
- * `NativeTransferFailed`.
118
+ * @dev `routeParamsEncoded`/`submitterDataEncoded` decode to `SpokePoolRouteParams`/`SpokePoolSubmitterData`
119
+ * (the latter carries the beacon `signer`'s EIP-712 signature). The value resolved from
120
+ * `inputTokenGetter` decides native (`NATIVE_SENTINEL`, msg.value) vs ERC-20. Reverts:
121
+ * `SignatureExpired`, `InvalidSignature`, `MaxFee`, `NativeTransferFailed`, `RouteNotConfigured`.
121
122
  */
122
- function execute(bytes calldata params, bytes calldata submitterData) external payable {
123
- SpokePoolDepositParams memory dp = abi.decode(params, (SpokePoolDepositParams));
124
- SpokePoolSubmitterData memory sd = abi.decode(submitterData, (SpokePoolSubmitterData));
125
-
126
- if (block.timestamp > sd.signatureDeadline) revert SignatureExpired();
127
- _verifySignature(sd);
128
-
129
- address inputToken = address(uint160(uint256(dp.inputToken)));
130
- uint256 depositAmount = sd.inputAmount - dp.executionFee;
131
-
132
- _checkFee(dp, sd.inputAmount, sd.outputAmount, depositAmount);
123
+ function execute(bytes calldata routeParamsEncoded, bytes calldata submitterDataEncoded) external payable {
124
+ SpokePoolRouteParams memory routeParams = abi.decode(routeParamsEncoded, (SpokePoolRouteParams));
125
+ SpokePoolSubmitterData memory submitterData = abi.decode(submitterDataEncoded, (SpokePoolSubmitterData));
126
+
127
+ if (block.timestamp > submitterData.signatureDeadline) revert SignatureExpired();
128
+ _verifySignature(keccak256(routeParamsEncoded), submitterData);
129
+
130
+ uint256 depositAmount = submitterData.inputAmount - submitterData.executionFee;
131
+ _checkFee(
132
+ routeParams,
133
+ submitterData.inputAmount,
134
+ submitterData.outputAmount,
135
+ depositAmount,
136
+ submitterData.executionFee,
137
+ _resolveBeaconUint(routeParams.maxExecutionFeeGetter)
138
+ );
133
139
 
134
- bool isNative = inputToken == NATIVE_ASSET;
135
- if (!isNative) IERC20(inputToken).forceApprove(spokePool, depositAmount);
140
+ ICounterfactualBeacon beacon = _beacon();
141
+ address spokePool = _requireConfigured(beacon.spokePool());
142
+
143
+ // The leaf names a beacon getter; its resolved value decides native (`NATIVE_SENTINEL`) vs ERC-20.
144
+ // Branching on the value, not the selector, lets one leaf serve both.
145
+ address resolved = _requireConfigured(_resolveBeaconAddress(routeParams.inputTokenGetter));
146
+ bool isNative = resolved == NATIVE_SENTINEL;
147
+ address inputToken; // ERC-20 to approve/sweep (unused for native)
148
+ bytes32 spokePoolInputToken;
149
+ if (isNative) {
150
+ spokePoolInputToken = bytes32(uint256(uint160(_requireConfigured(beacon.wrappedNativeToken()))));
151
+ } else {
152
+ inputToken = resolved;
153
+ IERC20(inputToken).forceApprove(spokePool, depositAmount);
154
+ spokePoolInputToken = bytes32(uint256(uint160(inputToken)));
155
+ }
136
156
 
137
- bytes32 spokePoolInputToken = isNative ? bytes32(uint256(uint160(wrappedNativeToken))) : dp.inputToken;
138
157
  V3SpokePoolInterface(spokePool).deposit{ value: isNative ? depositAmount : 0 }(
139
158
  bytes32(uint256(uint160(address(this)))),
140
- dp.recipient,
159
+ routeParams.recipient,
141
160
  spokePoolInputToken,
142
- dp.outputToken,
161
+ routeParams.outputToken,
143
162
  depositAmount,
144
- sd.outputAmount,
145
- dp.destinationChainId,
146
- sd.exclusiveRelayer,
147
- sd.quoteTimestamp,
148
- sd.fillDeadline,
149
- sd.exclusivityDeadline,
150
- dp.message
163
+ submitterData.outputAmount,
164
+ routeParams.destinationChainId,
165
+ submitterData.exclusiveRelayer,
166
+ submitterData.quoteTimestamp,
167
+ submitterData.fillDeadline,
168
+ submitterData.exclusivityDeadline,
169
+ routeParams.message
151
170
  );
152
171
 
153
172
  // Pay execution fee
154
- if (dp.executionFee > 0) {
173
+ if (submitterData.executionFee > 0) {
155
174
  if (isNative) {
156
- (bool success, ) = sd.executionFeeRecipient.call{ value: dp.executionFee }("");
175
+ (bool success, ) = submitterData.executionFeeRecipient.call{ value: submitterData.executionFee }("");
157
176
  if (!success) revert NativeTransferFailed();
158
177
  } else {
159
- _safeTransfer(inputToken, sd.executionFeeRecipient, dp.executionFee);
178
+ _safeTransfer(inputToken, submitterData.executionFeeRecipient, submitterData.executionFee);
160
179
  }
161
180
  }
162
181
 
163
182
  emit SpokePoolDepositExecuted(
164
- sd.inputAmount,
165
- sd.outputAmount,
166
- sd.exclusiveRelayer,
167
- sd.exclusivityDeadline,
168
- sd.executionFeeRecipient,
169
- sd.quoteTimestamp,
170
- sd.fillDeadline,
171
- sd.signatureDeadline
183
+ submitterData.inputAmount,
184
+ submitterData.outputAmount,
185
+ submitterData.exclusiveRelayer,
186
+ submitterData.exclusivityDeadline,
187
+ submitterData.executionFeeRecipient,
188
+ submitterData.quoteTimestamp,
189
+ submitterData.fillDeadline,
190
+ submitterData.signatureDeadline,
191
+ submitterData.executionFee
172
192
  );
173
193
  }
174
194
 
175
195
  function _checkFee(
176
- SpokePoolDepositParams memory dp,
196
+ SpokePoolRouteParams memory routeParams,
177
197
  uint256 inputAmount,
178
198
  uint256 outputAmount,
179
- uint256 depositAmount
199
+ uint256 depositAmount,
200
+ uint256 executionFee,
201
+ uint256 maxFeeFixed
180
202
  ) private pure {
181
- uint256 outputInInputToken = (outputAmount * dp.stableExchangeRate) / EXCHANGE_RATE_SCALAR;
182
- uint256 relayerFee = depositAmount > outputInInputToken ? depositAmount - outputInInputToken : 0;
183
- uint256 totalFee = relayerFee + dp.executionFee;
184
- uint256 maxFee = dp.maxFeeFixed + (dp.maxFeeBps * inputAmount) / BPS_SCALAR;
203
+ // With `checkStableExchangeRate` false (non-stable pairs), the rate-derived relayer fee isn't
204
+ // enforced (`outputAmount` is trusted via the signature); `executionFee` is still bounded by `maxFee`.
205
+ uint256 relayerFee;
206
+ if (routeParams.checkStableExchangeRate) {
207
+ uint256 outputInInputToken = (outputAmount * routeParams.stableExchangeRate) / EXCHANGE_RATE_SCALAR;
208
+ relayerFee = depositAmount > outputInInputToken ? depositAmount - outputInInputToken : 0;
209
+ }
210
+ uint256 totalFee = relayerFee + executionFee;
211
+ // `maxFeeFixed` is the per-chain fixed cap resolved from the beacon; `maxFeeBps` stays in the leaf.
212
+ uint256 maxFee = maxFeeFixed + (routeParams.maxFeeBps * inputAmount) / BPS_SCALAR;
185
213
  if (totalFee > maxFee) revert MaxFee();
186
214
  }
187
215
 
188
- function _verifySignature(SpokePoolSubmitterData memory sd) private view {
216
+ function _verifySignature(bytes32 routeParamsHash, SpokePoolSubmitterData memory submitterData) private view {
189
217
  bytes32 structHash = keccak256(
190
218
  abi.encode(
191
219
  EXECUTE_DEPOSIT_TYPEHASH,
192
- sd.inputAmount,
193
- sd.outputAmount,
194
- sd.exclusiveRelayer,
195
- sd.exclusivityDeadline,
196
- sd.quoteTimestamp,
197
- sd.fillDeadline,
198
- sd.signatureDeadline
220
+ address(this),
221
+ routeParamsHash,
222
+ submitterData.inputAmount,
223
+ submitterData.outputAmount,
224
+ submitterData.exclusiveRelayer,
225
+ submitterData.exclusivityDeadline,
226
+ submitterData.quoteTimestamp,
227
+ submitterData.fillDeadline,
228
+ submitterData.signatureDeadline,
229
+ submitterData.executionFee
199
230
  )
200
231
  );
201
- if (ECDSA.recover(_hashTypedDataV4(structHash), sd.signature) != signer) revert InvalidSignature();
232
+ if (ECDSA.recover(_hashTypedDataV4(structHash), submitterData.signature) != _beacon().signer())
233
+ revert InvalidSignature();
202
234
  }
203
235
  }
@@ -6,30 +6,19 @@ import { TronTransferLib } from "../../libraries/TronTransferLib.sol";
6
6
 
7
7
  /**
8
8
  * @title CounterfactualDepositSpokePoolTr
9
- * @notice Tron-specific variant of `CounterfactualDepositSpokePool` for chains where the
10
- * input token may be Tron USDT (whose `transfer` returns false on success).
11
- * @dev Inherits everything from the mainline implementation and overrides the
12
- * `_safeTransfer` hook to use a balance-delta success check that tolerates
13
- * Tron USDT's non-standard return value. `forceApprove` is unaffected `approve`
14
- * returns true correctly on Tron USDT.
15
- *
16
- * The EIP-712 domain name is inherited from the parent (`CounterfactualDepositSpokePool`).
17
- * Cross-implementation signature replay is already prevented by the `verifyingContract`
18
- * field of the EIP-712 domain: each clone's address is derived via CREATE2 from its
19
- * implementation address, so a signature for a mainline clone does not verify against
20
- * a Tron-variant clone.
9
+ * @notice Tron variant of `CounterfactualDepositSpokePool` for input tokens like Tron USDT (whose
10
+ * `transfer` returns false on success).
11
+ * @dev Inherits the mainline impl (including input-token-agnostic resolution) and overrides only
12
+ * `_safeTransfer` to use a balance-delta success check tolerating Tron USDT's non-standard return;
13
+ * `forceApprove` is unaffected. The mainline EIP-712 name is inherited, which is safe: a fee signature
14
+ * commits `chainId` and the full route (incl. token selector) via `routeParamsHash`, and only one
15
+ * SpokePool impl is deployed per chain — so it can't cross chains or tokens, and the variant changes
16
+ * only transfer semantics, not the signed outcome.
21
17
  * @custom:security-contact bugs@across.to
22
18
  */
23
19
  contract CounterfactualDepositSpokePoolTr is CounterfactualDepositSpokePool {
24
- constructor(
25
- address _spokePool,
26
- address _signer,
27
- address _wrappedNativeToken
28
- ) CounterfactualDepositSpokePool(_spokePool, _signer, _wrappedNativeToken) {} // solhint-disable-line no-empty-blocks
29
-
30
- /// @dev TRON OVERRIDE: was `IERC20(token).safeTransfer(to, amount)` in the parent.
31
- /// `TronTransferLib._safeTransferBalanceCheck` uses a balance-delta success check so it
32
- /// tolerates Tron USDT's non-standard `transfer` return value.
20
+ /// @dev TRON OVERRIDE of `_safeTransfer`: a balance-delta success check that tolerates Tron USDT's
21
+ /// non-standard `transfer` return value.
33
22
  function _safeTransfer(address token, address to, uint256 amount) internal override {
34
23
  TronTransferLib._safeTransferBalanceCheck(token, to, amount);
35
24
  }