@@ -12,7 +12,12 @@ import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/crypt
1212import {ECDSAUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol " ;
1313
1414import {IL1ERC20Gateway} from "../L1/gateways/IL1ERC20Gateway.sol " ;
15+ import {IL1ScrollMessenger} from "../L1/IL1ScrollMessenger.sol " ;
16+ import {IScrollChain} from "../L1/rollup/IScrollChain.sol " ;
1517import {IWETH} from "../interfaces/IWETH.sol " ;
18+ import {IScrollGatewayCallback} from "../libraries/callbacks/IScrollGatewayCallback.sol " ;
19+ import {IScrollGateway} from "../libraries/gateway/IScrollGateway.sol " ;
20+ import {WithdrawTrieVerifier} from "../libraries/verifier/WithdrawTrieVerifier.sol " ;
1621
1722/// @title FastWithdrawVault
1823/// @notice The vault for fast withdrawals from L2 to L1.
@@ -24,7 +29,12 @@ import {IWETH} from "../interfaces/IWETH.sol";
2429/// also sending the proper amount of tokens.
2530/// 2. The sequencer signs the withdraw request and sends it to the vault.
2631/// 3. The vault verifies the signature and the message hash, and then withdraws the tokens from L2 to L1.
27- contract FastWithdrawVault is AccessControlUpgradeable , ReentrancyGuardUpgradeable , EIP712Upgradeable {
32+ contract FastWithdrawVault is
33+ AccessControlUpgradeable ,
34+ ReentrancyGuardUpgradeable ,
35+ EIP712Upgradeable ,
36+ IScrollGatewayCallback
37+ {
2838 using SafeERC20Upgradeable for IERC20Upgradeable ;
2939
3040 /**********
@@ -46,6 +56,15 @@ contract FastWithdrawVault is AccessControlUpgradeable, ReentrancyGuardUpgradeab
4656 /// @dev Thrown when the given withdraw message has already been processed.
4757 error ErrorWithdrawAlreadyProcessed ();
4858
59+ /// @dev Thrown when the given message is not valid.
60+ error ErrorInvalidMessage ();
61+
62+ /// @dev Thrown when the given batch is not finalized.
63+ error ErrorBatchNotFinalized ();
64+
65+ /// @dev Thrown when the given proof is invalid.
66+ error ErrorInvalidProof ();
67+
4968 /*************
5069 * Constants *
5170 *************/
@@ -68,6 +87,27 @@ contract FastWithdrawVault is AccessControlUpgradeable, ReentrancyGuardUpgradeab
6887 /// @notice The address of the `L1ERC20Gateway` contract.
6988 address public immutable gateway;
7089
90+ /// @notice The address of the `ScrollChain` contract.
91+ address public immutable rollup;
92+
93+ /***********
94+ * Structs *
95+ ***********/
96+
97+ /// @notice The struct of the validium cross domain message.
98+ /// @param from The address of the sender of the message.
99+ /// @param to The address of the recipient of the message.
100+ /// @param value The msg.value passed to the message call.
101+ /// @param nonce The nonce of the message to avoid replay attack.
102+ /// @param message The content of the message.
103+ struct ValidiumCrossDomainMessage {
104+ address from;
105+ address to;
106+ uint256 value;
107+ uint256 nonce;
108+ bytes message;
109+ }
110+
71111 /*********************
72112 * Storage Variables *
73113 *********************/
@@ -108,6 +148,52 @@ contract FastWithdrawVault is AccessControlUpgradeable, ReentrancyGuardUpgradeab
108148
109149 receive () external payable {}
110150
151+ /// @notice Fast withdraw some tokens from L2 to L1 with proof from sequencer.
152+ /// @param message The content of the message.
153+ /// @param proof The proof used to verify the correctness of the transaction.
154+ function claimWithProof (ValidiumCrossDomainMessage calldata message , IL1ScrollMessenger.L2MessageProof memory proof )
155+ external
156+ nonReentrant
157+ {
158+ if (message.to != gateway) revert ErrorInvalidMessage ();
159+ if (message.from != IScrollGateway (gateway).counterpart ()) revert ErrorInvalidMessage ();
160+
161+ bytes32 messageHash = keccak256 (
162+ _encodeXDomainCalldata (message.from, message.to, message.value, message.nonce, message.message)
163+ );
164+ if (isWithdrawn[messageHash]) revert ErrorWithdrawAlreadyProcessed ();
165+ isWithdrawn[messageHash] = true ;
166+
167+ // verify proof
168+ {
169+ if (! IScrollChain (rollup).isBatchFinalized (proof.batchIndex)) {
170+ revert ErrorBatchNotFinalized ();
171+ }
172+ bytes32 _messageRoot = IScrollChain (rollup).withdrawRoots (proof.batchIndex);
173+ if (! WithdrawTrieVerifier.verifyMerkleProof (_messageRoot, messageHash, message.nonce, proof.merkleProof)) {
174+ revert ErrorInvalidProof ();
175+ }
176+ }
177+
178+ // decode actual validium sender from message.
179+ (address l1Token , address l2Token , address sender , address receiver , uint256 amount , ) = abi.decode (
180+ message.message[4 :],
181+ (address , address , address , address , uint256 , bytes )
182+ );
183+ if (IL1ERC20Gateway (gateway).getL2ERC20Address (l1Token) != l2Token) revert ErrorInvalidMessage ();
184+ if (receiver != address (this )) revert ErrorInvalidMessage ();
185+
186+ // transfer tokens to sender
187+ if (l1Token == weth) {
188+ IWETH (weth).withdraw (amount);
189+ AddressUpgradeable.sendValue (payable (sender), amount);
190+ } else {
191+ IERC20Upgradeable (l1Token).safeTransfer (sender, amount);
192+ }
193+
194+ emit Withdraw (l1Token, l2Token, sender, amount, messageHash);
195+ }
196+
111197 /// @notice Fast withdraw some tokens from L2 to L1 with signature from sequencer.
112198 /// @param l1Token The address of the L1 token.
113199 /// @param to The address of the recipient.
@@ -121,23 +207,34 @@ contract FastWithdrawVault is AccessControlUpgradeable, ReentrancyGuardUpgradeab
121207 bytes32 messageHash ,
122208 bytes memory signature
123209 ) external nonReentrant {
124- address l2Token = IL1ERC20Gateway (gateway).getL2ERC20Address (l1Token);
125- bytes32 structHash = keccak256 (abi.encode (_WITHDRAW_TYPEHASH, l1Token, l2Token, to, amount, messageHash));
126- if (isWithdrawn[structHash]) revert ErrorWithdrawAlreadyProcessed ();
127- isWithdrawn[structHash] = true ;
210+ _claim (l1Token, to, amount, messageHash, signature);
211+ }
128212
129- bytes32 hash = _hashTypedDataV4 (structHash);
130- address signer = ECDSAUpgradeable.recover (hash, signature);
131- _checkRole (SEQUENCER_ROLE, signer);
213+ /// @notice Fast withdraw some tokens from L2 to L1 with signature from sequencer and call the target contract.
214+ /// @param l1Token The address of the L1 token.
215+ /// @param to The address of the recipient.
216+ /// @param amount The amount of tokens to withdraw.
217+ /// @param messageHash The hash of the message, which is the corresponding withdraw message hash in L2.
218+ /// @param signature The signature of the message from sequencer.
219+ /// @param data The data to call the target contract.
220+ function claimAndCall (
221+ address l1Token ,
222+ address to ,
223+ uint256 amount ,
224+ bytes32 messageHash ,
225+ bytes memory signature ,
226+ address callbackTo ,
227+ bytes memory data
228+ ) external payable nonReentrant {
229+ _claim (l1Token, to, amount, messageHash, signature);
132230
133- if (l1Token == weth) {
134- IWETH (weth).withdraw (amount);
135- AddressUpgradeable.sendValue (payable (to), amount);
136- } else {
137- IERC20Upgradeable (l1Token).safeTransfer (to, amount);
138- }
231+ // @note callbackTo is the address of the target contract to call.
232+ IScrollGatewayCallback (callbackTo).onScrollGatewayCallback (data);
233+ }
139234
140- emit Withdraw (l1Token, l2Token, to, amount, messageHash);
235+ /// @inheritdoc IScrollGatewayCallback
236+ function onScrollGatewayCallback (bytes calldata _data ) external override {
237+ // noop
141238 }
142239
143240 /************************
@@ -155,4 +252,65 @@ contract FastWithdrawVault is AccessControlUpgradeable, ReentrancyGuardUpgradeab
155252 ) external nonReentrant onlyRole (DEFAULT_ADMIN_ROLE) {
156253 IERC20Upgradeable (token).safeTransfer (recipient, amount);
157254 }
255+
256+ /**********************
257+ * Internal Functions *
258+ **********************/
259+
260+ /// @dev Internal function to claim the fast withdraw.
261+ /// @param l1Token The address of the L1 token.
262+ /// @param to The address of the recipient.
263+ /// @param amount The amount of tokens to withdraw.
264+ /// @param messageHash The hash of the message, which is the corresponding withdraw message hash in L2.
265+ /// @param signature The signature of the message from sequencer.
266+ function _claim (
267+ address l1Token ,
268+ address to ,
269+ uint256 amount ,
270+ bytes32 messageHash ,
271+ bytes memory signature
272+ ) internal {
273+ address l2Token = IL1ERC20Gateway (gateway).getL2ERC20Address (l1Token);
274+ bytes32 structHash = keccak256 (abi.encode (_WITHDRAW_TYPEHASH, l1Token, l2Token, to, amount, messageHash));
275+ if (isWithdrawn[structHash]) revert ErrorWithdrawAlreadyProcessed ();
276+ isWithdrawn[structHash] = true ;
277+
278+ bytes32 hash = _hashTypedDataV4 (structHash);
279+ address signer = ECDSAUpgradeable.recover (hash, signature);
280+ _checkRole (SEQUENCER_ROLE, signer);
281+
282+ if (l1Token == weth) {
283+ IWETH (weth).withdraw (amount);
284+ AddressUpgradeable.sendValue (payable (to), amount);
285+ } else {
286+ IERC20Upgradeable (l1Token).safeTransfer (to, amount);
287+ }
288+
289+ emit Withdraw (l1Token, l2Token, to, amount, messageHash);
290+ }
291+
292+ /// @dev Internal function to generate the correct cross domain calldata for a message.
293+ /// @param _sender Message sender address.
294+ /// @param _target Target contract address.
295+ /// @param _value The amount of ETH pass to the target.
296+ /// @param _messageNonce Nonce for the provided message.
297+ /// @param _message Message to send to the target.
298+ /// @return ABI encoded cross domain calldata.
299+ function _encodeXDomainCalldata (
300+ address _sender ,
301+ address _target ,
302+ uint256 _value ,
303+ uint256 _messageNonce ,
304+ bytes memory _message
305+ ) internal pure returns (bytes memory ) {
306+ return
307+ abi.encodeWithSignature (
308+ "relayMessage(address,address,uint256,uint256,bytes) " ,
309+ _sender,
310+ _target,
311+ _value,
312+ _messageNonce,
313+ _message
314+ );
315+ }
158316}
0 commit comments