-
Couldn't load subscription status.
- Fork 35
allow user claim; allow claim and call #152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,12 @@ import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/crypt | |
| import {ECDSAUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; | ||
|
|
||
| import {IL1ERC20Gateway} from "../L1/gateways/IL1ERC20Gateway.sol"; | ||
| import {IL1ScrollMessenger} from "../L1/IL1ScrollMessenger.sol"; | ||
| import {IScrollChain} from "../L1/rollup/IScrollChain.sol"; | ||
| import {IWETH} from "../interfaces/IWETH.sol"; | ||
| import {IScrollGatewayCallback} from "../libraries/callbacks/IScrollGatewayCallback.sol"; | ||
| import {IScrollGateway} from "../libraries/gateway/IScrollGateway.sol"; | ||
| import {WithdrawTrieVerifier} from "../libraries/verifier/WithdrawTrieVerifier.sol"; | ||
|
|
||
| /// @title FastWithdrawVault | ||
| /// @notice The vault for fast withdrawals from L2 to L1. | ||
|
|
@@ -24,7 +29,12 @@ import {IWETH} from "../interfaces/IWETH.sol"; | |
| /// also sending the proper amount of tokens. | ||
| /// 2. The sequencer signs the withdraw request and sends it to the vault. | ||
| /// 3. The vault verifies the signature and the message hash, and then withdraws the tokens from L2 to L1. | ||
| contract FastWithdrawVault is AccessControlUpgradeable, ReentrancyGuardUpgradeable, EIP712Upgradeable { | ||
| contract FastWithdrawVault is | ||
| AccessControlUpgradeable, | ||
| ReentrancyGuardUpgradeable, | ||
| EIP712Upgradeable, | ||
| IScrollGatewayCallback | ||
| { | ||
| using SafeERC20Upgradeable for IERC20Upgradeable; | ||
|
|
||
| /********** | ||
|
|
@@ -46,6 +56,15 @@ contract FastWithdrawVault is AccessControlUpgradeable, ReentrancyGuardUpgradeab | |
| /// @dev Thrown when the given withdraw message has already been processed. | ||
| error ErrorWithdrawAlreadyProcessed(); | ||
|
|
||
| /// @dev Thrown when the given message is not valid. | ||
| error ErrorInvalidMessage(); | ||
|
|
||
| /// @dev Thrown when the given batch is not finalized. | ||
| error ErrorBatchNotFinalized(); | ||
|
|
||
| /// @dev Thrown when the given proof is invalid. | ||
| error ErrorInvalidProof(); | ||
|
|
||
| /************* | ||
| * Constants * | ||
| *************/ | ||
|
|
@@ -68,6 +87,27 @@ contract FastWithdrawVault is AccessControlUpgradeable, ReentrancyGuardUpgradeab | |
| /// @notice The address of the `L1ERC20Gateway` contract. | ||
| address public immutable gateway; | ||
|
|
||
| /// @notice The address of the `ScrollChain` contract. | ||
| address public immutable rollup; | ||
|
|
||
| /*********** | ||
| * Structs * | ||
| ***********/ | ||
|
|
||
| /// @notice The struct of the validium cross domain message. | ||
| /// @param from The address of the sender of the message. | ||
| /// @param to The address of the recipient of the message. | ||
| /// @param value The msg.value passed to the message call. | ||
| /// @param nonce The nonce of the message to avoid replay attack. | ||
| /// @param message The content of the message. | ||
| struct ValidiumCrossDomainMessage { | ||
| address from; | ||
| address to; | ||
| uint256 value; | ||
| uint256 nonce; | ||
| bytes message; | ||
| } | ||
|
|
||
| /********************* | ||
| * Storage Variables * | ||
| *********************/ | ||
|
|
@@ -108,6 +148,52 @@ contract FastWithdrawVault is AccessControlUpgradeable, ReentrancyGuardUpgradeab | |
|
|
||
| receive() external payable {} | ||
|
|
||
| /// @notice Fast withdraw some tokens from L2 to L1 with proof from sequencer. | ||
| /// @param message The content of the message. | ||
| /// @param proof The proof used to verify the correctness of the transaction. | ||
| function claimWithProof(ValidiumCrossDomainMessage calldata message, IL1ScrollMessenger.L2MessageProof memory proof) | ||
| external | ||
| nonReentrant | ||
| { | ||
| if (message.to != gateway) revert ErrorInvalidMessage(); | ||
| if (message.from != IScrollGateway(gateway).counterpart()) revert ErrorInvalidMessage(); | ||
|
|
||
| bytes32 messageHash = keccak256( | ||
| _encodeXDomainCalldata(message.from, message.to, message.value, message.nonce, message.message) | ||
| ); | ||
| if (isWithdrawn[messageHash]) revert ErrorWithdrawAlreadyProcessed(); | ||
| isWithdrawn[messageHash] = true; | ||
|
|
||
| // verify proof | ||
| { | ||
| if (!IScrollChain(rollup).isBatchFinalized(proof.batchIndex)) { | ||
| revert ErrorBatchNotFinalized(); | ||
| } | ||
| bytes32 _messageRoot = IScrollChain(rollup).withdrawRoots(proof.batchIndex); | ||
| if (!WithdrawTrieVerifier.verifyMerkleProof(_messageRoot, messageHash, message.nonce, proof.merkleProof)) { | ||
| revert ErrorInvalidProof(); | ||
| } | ||
| } | ||
|
|
||
| // decode actual validium sender from message. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently I encode the recipient into |
||
| (address l1Token, address l2Token, address sender, address receiver, uint256 amount, ) = abi.decode( | ||
| message.message[4:], | ||
| (address, address, address, address, uint256, bytes) | ||
| ); | ||
| if (IL1ERC20Gateway(gateway).getL2ERC20Address(l1Token) != l2Token) revert ErrorInvalidMessage(); | ||
| if (receiver != address(this)) revert ErrorInvalidMessage(); | ||
|
|
||
| // transfer tokens to sender | ||
| if (l1Token == weth) { | ||
| IWETH(weth).withdraw(amount); | ||
| AddressUpgradeable.sendValue(payable(sender), amount); | ||
| } else { | ||
| IERC20Upgradeable(l1Token).safeTransfer(sender, amount); | ||
| } | ||
|
Comment on lines
+187
to
+192
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My thinking was a bit different: If the message is never relayed to L2, the user can claim from |
||
|
|
||
| emit Withdraw(l1Token, l2Token, sender, amount, messageHash); | ||
| } | ||
|
|
||
| /// @notice Fast withdraw some tokens from L2 to L1 with signature from sequencer. | ||
| /// @param l1Token The address of the L1 token. | ||
| /// @param to The address of the recipient. | ||
|
|
@@ -121,23 +207,34 @@ contract FastWithdrawVault is AccessControlUpgradeable, ReentrancyGuardUpgradeab | |
| bytes32 messageHash, | ||
| bytes memory signature | ||
| ) external nonReentrant { | ||
| address l2Token = IL1ERC20Gateway(gateway).getL2ERC20Address(l1Token); | ||
| bytes32 structHash = keccak256(abi.encode(_WITHDRAW_TYPEHASH, l1Token, l2Token, to, amount, messageHash)); | ||
| if (isWithdrawn[structHash]) revert ErrorWithdrawAlreadyProcessed(); | ||
| isWithdrawn[structHash] = true; | ||
| _claim(l1Token, to, amount, messageHash, signature); | ||
| } | ||
|
|
||
| bytes32 hash = _hashTypedDataV4(structHash); | ||
| address signer = ECDSAUpgradeable.recover(hash, signature); | ||
| _checkRole(SEQUENCER_ROLE, signer); | ||
| /// @notice Fast withdraw some tokens from L2 to L1 with signature from sequencer and call the target contract. | ||
| /// @param l1Token The address of the L1 token. | ||
| /// @param to The address of the recipient. | ||
| /// @param amount The amount of tokens to withdraw. | ||
| /// @param messageHash The hash of the message, which is the corresponding withdraw message hash in L2. | ||
| /// @param signature The signature of the message from sequencer. | ||
| /// @param data The data to call the target contract. | ||
| function claimAndCall( | ||
| address l1Token, | ||
| address to, | ||
| uint256 amount, | ||
| bytes32 messageHash, | ||
| bytes memory signature, | ||
| address callbackTo, | ||
| bytes memory data | ||
| ) external payable nonReentrant { | ||
| _claim(l1Token, to, amount, messageHash, signature); | ||
|
|
||
| if (l1Token == weth) { | ||
| IWETH(weth).withdraw(amount); | ||
| AddressUpgradeable.sendValue(payable(to), amount); | ||
| } else { | ||
| IERC20Upgradeable(l1Token).safeTransfer(to, amount); | ||
| } | ||
| // @note callbackTo is the address of the target contract to call. | ||
| IScrollGatewayCallback(callbackTo).onScrollGatewayCallback(data); | ||
|
Comment on lines
+231
to
+232
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So for example to support swaps, we'd need to deploy a "forwarder contract" that implements But how does it work with tokens, when tokens are transferred to |
||
| } | ||
|
|
||
| emit Withdraw(l1Token, l2Token, to, amount, messageHash); | ||
| /// @inheritdoc IScrollGatewayCallback | ||
| function onScrollGatewayCallback(bytes calldata _data) external override { | ||
| // noop | ||
| } | ||
|
|
||
| /************************ | ||
|
|
@@ -155,4 +252,65 @@ contract FastWithdrawVault is AccessControlUpgradeable, ReentrancyGuardUpgradeab | |
| ) external nonReentrant onlyRole(DEFAULT_ADMIN_ROLE) { | ||
| IERC20Upgradeable(token).safeTransfer(recipient, amount); | ||
| } | ||
|
|
||
| /********************** | ||
| * Internal Functions * | ||
| **********************/ | ||
|
|
||
| /// @dev Internal function to claim the fast withdraw. | ||
| /// @param l1Token The address of the L1 token. | ||
| /// @param to The address of the recipient. | ||
| /// @param amount The amount of tokens to withdraw. | ||
| /// @param messageHash The hash of the message, which is the corresponding withdraw message hash in L2. | ||
| /// @param signature The signature of the message from sequencer. | ||
| function _claim( | ||
| address l1Token, | ||
| address to, | ||
| uint256 amount, | ||
| bytes32 messageHash, | ||
| bytes memory signature | ||
| ) internal { | ||
| address l2Token = IL1ERC20Gateway(gateway).getL2ERC20Address(l1Token); | ||
| bytes32 structHash = keccak256(abi.encode(_WITHDRAW_TYPEHASH, l1Token, l2Token, to, amount, messageHash)); | ||
| if (isWithdrawn[structHash]) revert ErrorWithdrawAlreadyProcessed(); | ||
| isWithdrawn[structHash] = true; | ||
|
|
||
| bytes32 hash = _hashTypedDataV4(structHash); | ||
| address signer = ECDSAUpgradeable.recover(hash, signature); | ||
| _checkRole(SEQUENCER_ROLE, signer); | ||
|
|
||
| if (l1Token == weth) { | ||
| IWETH(weth).withdraw(amount); | ||
| AddressUpgradeable.sendValue(payable(to), amount); | ||
| } else { | ||
| IERC20Upgradeable(l1Token).safeTransfer(to, amount); | ||
| } | ||
|
|
||
| emit Withdraw(l1Token, l2Token, to, amount, messageHash); | ||
| } | ||
|
|
||
| /// @dev Internal function to generate the correct cross domain calldata for a message. | ||
| /// @param _sender Message sender address. | ||
| /// @param _target Target contract address. | ||
| /// @param _value The amount of ETH pass to the target. | ||
| /// @param _messageNonce Nonce for the provided message. | ||
| /// @param _message Message to send to the target. | ||
| /// @return ABI encoded cross domain calldata. | ||
| function _encodeXDomainCalldata( | ||
| address _sender, | ||
| address _target, | ||
| uint256 _value, | ||
| uint256 _messageNonce, | ||
| bytes memory _message | ||
| ) internal pure returns (bytes memory) { | ||
| return | ||
| abi.encodeWithSignature( | ||
| "relayMessage(address,address,uint256,uint256,bytes)", | ||
| _sender, | ||
| _target, | ||
| _value, | ||
| _messageNonce, | ||
| _message | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to init in constructor.