Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 173 additions & 15 deletions src/validium/FastWithdrawVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;

/**********
Expand All @@ -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 *
*************/
Expand All @@ -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;
Comment on lines +90 to +91
Copy link
Contributor

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.


/***********
* 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 *
*********************/
Expand Down Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently I encode the recipient into data instead of always using the L3 sender as the withdraw recipient. Should we do that here?

(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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 L1ScrollMessenger, and not from the liquidity pool here (which might be empty). Is that doable?


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.
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 onScrollGatewayCallback, decodes the data, and makes the swap?

But how does it work with tokens, when tokens are transferred to to instead of callbackTo? Shouldn't we transfer tokens to callbackTo in this case?

}

emit Withdraw(l1Token, l2Token, to, amount, messageHash);
/// @inheritdoc IScrollGatewayCallback
function onScrollGatewayCallback(bytes calldata _data) external override {
// noop
}

/************************
Expand All @@ -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
);
}
}
Loading