@across-protocol/contracts 5.0.16 → 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.
- package/contracts/interfaces/ICounterfactualBeacon.sol +91 -0
- package/contracts/interfaces/ICounterfactualDeposit.sol +21 -0
- package/contracts/libraries/TronClones.sol +13 -0
- package/contracts/periphery/counterfactual/CounterfactualBeacon.sol +115 -0
- package/contracts/periphery/counterfactual/CounterfactualBeaconBase.sol +121 -0
- package/contracts/periphery/counterfactual/CounterfactualBeaconBootstrap.sol +39 -0
- package/contracts/periphery/counterfactual/CounterfactualDeposit.sol +109 -28
- package/contracts/periphery/counterfactual/CounterfactualDepositCCTP.sol +102 -57
- package/contracts/periphery/counterfactual/CounterfactualDepositFactory.sol +61 -83
- package/contracts/periphery/counterfactual/CounterfactualDepositFactoryTron.sol +14 -24
- package/contracts/periphery/counterfactual/CounterfactualDepositOFT.sol +119 -58
- package/contracts/periphery/counterfactual/CounterfactualDepositSpokePool.sol +122 -90
- package/contracts/periphery/counterfactual/CounterfactualDepositSpokePoolTr.sol +10 -21
- package/contracts/periphery/counterfactual/CounterfactualDepositVanillaCCTP.sol +170 -0
- package/contracts/periphery/counterfactual/CounterfactualImplementationBase.sol +51 -0
- package/dist/broadcast/deployed-addresses.json +34 -10
- package/dist/evm/artifacts/AdminWithdrawManager.sol/AdminWithdrawManager.json +1 -1
- package/dist/evm/artifacts/CounterfactualBeacon.sol/CounterfactualBeacon.json +1 -0
- package/dist/evm/artifacts/CounterfactualBeaconBase.sol/CounterfactualBeaconBase.json +1 -0
- package/dist/evm/artifacts/CounterfactualBeaconBase.sol/IBeaconTarget.json +1 -0
- package/dist/evm/artifacts/CounterfactualBeaconBootstrap.sol/CounterfactualBeaconBootstrap.json +1 -0
- package/dist/evm/artifacts/CounterfactualDeposit.sol/CounterfactualDeposit.json +1 -1
- package/dist/evm/artifacts/CounterfactualDepositCCTP.sol/CounterfactualDepositCCTP.json +1 -1
- package/dist/evm/artifacts/CounterfactualDepositFactory.sol/CounterfactualDepositFactory.json +1 -1
- package/dist/evm/artifacts/CounterfactualDepositFactoryTron.sol/CounterfactualDepositFactoryTron.json +1 -1
- package/dist/evm/artifacts/CounterfactualDepositOFT.sol/CounterfactualDepositOFT.json +1 -1
- package/dist/evm/artifacts/CounterfactualDepositOFT.sol/ISponsoredOFTSrcPeriphery.json +1 -1
- package/dist/evm/artifacts/CounterfactualDepositSpokePool.sol/CounterfactualDepositSpokePool.json +1 -1
- package/dist/evm/artifacts/CounterfactualDepositSpokePoolTr.sol/CounterfactualDepositSpokePoolTr.json +1 -1
- package/dist/evm/artifacts/CounterfactualDepositVanillaCCTP.sol/CounterfactualDepositVanillaCCTP.json +1 -0
- package/dist/evm/artifacts/CounterfactualImplementationBase.sol/CounterfactualImplementationBase.json +1 -0
- package/dist/evm/artifacts/CounterfactualTestBase.sol/CounterfactualTestBase.json +1 -0
- package/dist/evm/artifacts/ICounterfactualBeacon.sol/ICounterfactualBeacon.json +1 -0
- package/dist/evm/artifacts/ICounterfactualDeposit.sol/ICounterfactualDeposit.json +1 -1
- package/dist/evm/artifacts/Ownable2StepUpgradeable.sol/Ownable2StepUpgradeable.json +1 -0
- package/dist/evm/artifacts/OwnableUpgradeable.sol/OwnableUpgradeable.json +1 -1
- package/dist/evm/artifacts/UUPSUpgradeable.sol/UUPSUpgradeable.json +1 -1
- package/dist/src/consts.js +4 -0
- package/package.json +2 -2
- 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
|
|
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
|
-
|
|
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
|
-
|
|
56
|
+
uint256 executionFee;
|
|
57
|
+
uint32 signatureDeadline;
|
|
58
|
+
bytes peripherySignature;
|
|
59
|
+
bytes counterfactualSignature;
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
/**
|
|
51
63
|
* @title CounterfactualDepositOFT
|
|
52
|
-
* @notice
|
|
53
|
-
* @dev
|
|
54
|
-
*
|
|
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
|
|
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(
|
|
82
|
+
event OFTDepositExecuted(
|
|
83
|
+
uint256 amount,
|
|
84
|
+
address indexed executionFeeRecipient,
|
|
85
|
+
bytes32 nonce,
|
|
86
|
+
uint256 oftDeadline,
|
|
87
|
+
uint256 executionFee
|
|
88
|
+
);
|
|
68
89
|
|
|
69
|
-
|
|
70
|
-
|
|
90
|
+
error InvalidSignature();
|
|
91
|
+
error SignatureExpired();
|
|
92
|
+
error MaxExecutionFee();
|
|
71
93
|
|
|
72
|
-
/// @notice
|
|
73
|
-
|
|
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(
|
|
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). `
|
|
83
|
-
* `
|
|
84
|
-
* ERC-20 only
|
|
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
|
|
87
|
-
|
|
88
|
-
OFTSubmitterData memory
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
+
uint256 depositAmount = submitterData.amount - submitterData.executionFee;
|
|
95
127
|
|
|
96
|
-
|
|
128
|
+
IERC20(inputToken).forceApprove(oftSrcPeriphery, depositAmount);
|
|
97
129
|
|
|
98
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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:
|
|
112
|
-
dstEid:
|
|
113
|
-
destinationHandler:
|
|
170
|
+
srcEid: _beacon().oftSrcEid(),
|
|
171
|
+
dstEid: routeParams.dstEid,
|
|
172
|
+
destinationHandler: routeParams.destinationHandler,
|
|
114
173
|
amountLD: depositAmount,
|
|
115
|
-
nonce:
|
|
116
|
-
deadline:
|
|
117
|
-
maxBpsToSponsor:
|
|
118
|
-
maxUserSlippageBps:
|
|
119
|
-
finalRecipient:
|
|
120
|
-
finalToken:
|
|
121
|
-
destinationDex:
|
|
122
|
-
lzReceiveGasLimit:
|
|
123
|
-
lzComposeGasLimit:
|
|
124
|
-
maxOftFeeBps:
|
|
125
|
-
accountCreationMode:
|
|
126
|
-
executionMode:
|
|
127
|
-
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({
|
|
188
|
+
unsignedParams: SponsoredOFTInterface.UnsignedQuoteParams({
|
|
189
|
+
refundRecipient: routeParams.refundRecipient
|
|
190
|
+
})
|
|
130
191
|
}),
|
|
131
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
46
|
-
* @dev
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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
|
-
*
|
|
51
|
-
* `
|
|
52
|
-
*
|
|
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
|
|
56
|
-
//
|
|
57
|
-
//
|
|
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
|
-
|
|
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
|
|
118
|
-
*
|
|
119
|
-
*
|
|
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
|
|
123
|
-
|
|
124
|
-
SpokePoolSubmitterData memory
|
|
125
|
-
|
|
126
|
-
if (block.timestamp >
|
|
127
|
-
_verifySignature(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
159
|
+
routeParams.recipient,
|
|
141
160
|
spokePoolInputToken,
|
|
142
|
-
|
|
161
|
+
routeParams.outputToken,
|
|
143
162
|
depositAmount,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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 (
|
|
173
|
+
if (submitterData.executionFee > 0) {
|
|
155
174
|
if (isNative) {
|
|
156
|
-
(bool success, ) =
|
|
175
|
+
(bool success, ) = submitterData.executionFeeRecipient.call{ value: submitterData.executionFee }("");
|
|
157
176
|
if (!success) revert NativeTransferFailed();
|
|
158
177
|
} else {
|
|
159
|
-
_safeTransfer(inputToken,
|
|
178
|
+
_safeTransfer(inputToken, submitterData.executionFeeRecipient, submitterData.executionFee);
|
|
160
179
|
}
|
|
161
180
|
}
|
|
162
181
|
|
|
163
182
|
emit SpokePoolDepositExecuted(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
uint256
|
|
184
|
-
|
|
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
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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),
|
|
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
|
|
10
|
-
*
|
|
11
|
-
* @dev Inherits
|
|
12
|
-
* `_safeTransfer`
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
|
|
25
|
-
|
|
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
|
}
|