Skip to content
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

add data hash for withdraw #141

Merged
merged 2 commits into from
Jan 20, 2024
Merged
Show file tree
Hide file tree
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
11 changes: 11 additions & 0 deletions contracts/Storage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ contract Storage is ZkLinkAcceptor, Config {
/// @dev Oracle verifier
IOracleVerifier public oracleVerifier;

/// @dev Store withdraw data hash that need to be called
/// @dev The key is the withdraw data hash
/// @dev The value is a flag to indicating whether withdraw exists
mapping(bytes32 => bool) public pendingWithdrawWithCalls;

// #if CHAIN_ID == MASTER_CHAIN_ID
/// @notice block stored data
/// @dev `blockNumber`,`timestamp`,`stateHash`,`commitment` are the same on all chains
Expand Down Expand Up @@ -233,6 +238,12 @@ contract Storage is ZkLinkAcceptor, Config {
return _amount / SafeCast.toUint128(10**(TOKEN_DECIMALS_OF_LAYER2 - _decimals));
}

/// @dev Return withdraw hash with call data
/// @dev (_accountIdOfNonce, _subAccountIdOfNonce, _nonce) ensures the uniqueness of withdraw hash
function getWithdrawWithDataHash(address _owner, address _tokenAddress, uint128 _amount, bytes32 _dataHash, uint32 _accountIdOfNonce, uint8 _subAccountIdOfNonce, uint32 _nonce) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(_owner, _tokenAddress, _amount, _dataHash, _accountIdOfNonce, _subAccountIdOfNonce, _nonce));
}

/// @notice Performs a delegatecall to the contract implementation
/// @dev Fallback function allowing to perform a delegatecall to the given implementation
/// This function will return whatever the implementation call returns
Expand Down
17 changes: 12 additions & 5 deletions contracts/ZkLink.sol
Original file line number Diff line number Diff line change
Expand Up @@ -780,12 +780,12 @@ contract ZkLink is ReentrancyGuard, Storage, Events, UpgradeableMaster {
if (opType == Operations.OpType.Withdraw) {
Operations.Withdraw memory op = Operations.readWithdrawPubdata(pubData);
// account request fast withdraw and sub account supply nonce
_executeWithdraw(op.accountId, op.subAccountId, op.nonce, op.owner, op.tokenId, op.amount, op.fastWithdrawFeeRate, op.withdrawToL1);
_executeWithdraw(op.accountId, op.subAccountId, op.nonce, op.owner, op.tokenId, op.amount, op.dataHash, op.fastWithdrawFeeRate, op.withdrawToL1);
} else if (opType == Operations.OpType.ForcedExit) {
Operations.ForcedExit memory op = Operations.readForcedExitPubdata(pubData);
// request forced exit for target account but initiator sub account supply nonce
// forced exit require fast withdraw default and take no fee for fast withdraw
_executeWithdraw(op.initiatorAccountId, op.initiatorSubAccountId, op.initiatorNonce, op.target, op.tokenId, op.amount, 0, op.withdrawToL1);
// forced exit take no fee for fast withdraw
_executeWithdraw(op.initiatorAccountId, op.initiatorSubAccountId, op.initiatorNonce, op.target, op.tokenId, op.amount, bytes32(0), 0, op.withdrawToL1);
} else if (opType == Operations.OpType.FullExit) {
Operations.FullExit memory op = Operations.readFullExitPubdata(pubData);
increasePendingBalance(op.tokenId, op.owner, op.amount);
Expand All @@ -798,7 +798,7 @@ contract ZkLink is ReentrancyGuard, Storage, Events, UpgradeableMaster {
}

/// @dev The circuit will check whether there is dust in the amount
function _executeWithdraw(uint32 accountIdOfNonce, uint8 subAccountIdOfNonce, uint32 nonce, address owner, uint16 tokenId, uint128 amount, uint16 fastWithdrawFeeRate, uint8 withdrawToL1) internal {
function _executeWithdraw(uint32 accountIdOfNonce, uint8 subAccountIdOfNonce, uint32 nonce, address owner, uint16 tokenId, uint128 amount, bytes32 dataHash, uint16 fastWithdrawFeeRate, uint8 withdrawToL1) internal {
// token MUST be registered
RegisteredToken storage rt = tokens[tokenId];
require(rt.registered, "o0");
Expand All @@ -815,7 +815,14 @@ contract ZkLink is ReentrancyGuard, Storage, Events, UpgradeableMaster {
if (acceptor == address(0)) {
// receiver act as an acceptor
accepts[withdrawHash] = owner;
increasePendingBalance(tokenId, owner, amount);
if (dataHash != bytes32(0)) {
// record token ownership to pending withdraw with calls and waiting relayer to call `withdrawPendingBalanceWithCall`
bytes32 withdrawWithDataHash = getWithdrawWithDataHash(owner, rt.tokenAddress, recoverAmount, dataHash, accountIdOfNonce, subAccountIdOfNonce, nonce);
pendingWithdrawWithCalls[withdrawWithDataHash] = true;
} else {
// record token ownership to pending balances and waiting relayer to call `withdrawPendingBalance`
increasePendingBalance(tokenId, owner, amount);
}
} else {
increasePendingBalance(tokenId, acceptor, amount);
}
Expand Down
72 changes: 51 additions & 21 deletions contracts/ZkLinkPeriphery.sol
Original file line number Diff line number Diff line change
Expand Up @@ -120,39 +120,69 @@ contract ZkLinkPeriphery is ReentrancyGuard, Storage, Events {
/// @notice Withdraws tokens from zkLink contract to the owner
/// @param _owner Address of the tokens owner
/// @param _tokenId Token id
/// @param _amount Amount to withdraw to request.
/// @return The actual withdrawn amount
/// @dev NOTE: We will call ERC20.transfer(.., _amount), but if according to internal logic of ERC20 token zkLink contract
/// balance will be decreased by value more then _amount we will try to subtract this value from user pending balance
function withdrawPendingBalance(address payable _owner, uint16 _tokenId, uint128 _amount) external nonReentrant returns (uint128) {
/// @param _amount The actual withdrawn amount
function withdrawPendingBalance(address payable _owner, uint16 _tokenId, uint128 _amount) external nonReentrant {
// ===Checks===
// token MUST be registered to ZkLink
RegisteredToken storage rt = tokens[_tokenId];
require(rt.registered, "b0");

// ===Effects===
// Set the available amount to withdraw
// balance need to be recovery decimals
bytes32 owner = extendAddress(_owner);
uint128 balance = pendingBalances[owner][_tokenId];
uint128 withdrawBalance = recoveryDecimals(balance, rt.decimals);
uint128 amount = Utils.minU128(withdrawBalance, _amount);
require(amount > 0, "b1");
bytes32 l2Owner = extendAddress(_owner);
uint128 l2Balance = pendingBalances[l2Owner][_tokenId];
uint128 l2Amount = improveDecimals(_amount, rt.decimals);
pendingBalances[l2Owner][_tokenId] = l2Balance - l2Amount;

// ===Interactions===
address tokenAddress = rt.tokenAddress;
if (tokenAddress == ETH_ADDRESS) {
// solhint-disable-next-line avoid-low-level-calls
(bool success, ) = _owner.call{value: amount}("");
require(success, "b2");
_withdrawTo(_owner, rt.tokenAddress, _amount, new bytes(0));
emit Withdrawal(_tokenId, _amount);
}

/// @notice Withdraws tokens from zkLink contract to the target contract
/// @param _owner Address of the tokens owner
/// @param _tokenAddress Token address
/// @param _amount The actual withdrawn amount
/// @param _data The target contract address and call data
/// @param _accountIdOfNonce Account that supply nonce
/// @param _subAccountIdOfNonce SubAccount that supply nonce
/// @param _nonce SubAccount nonce
/// @param _callTarget True when call target or withdraw pending balance to owner
function withdrawPendingBalanceWithCall(address payable _owner, address _tokenAddress, uint128 _amount, bytes memory _data, uint32 _accountIdOfNonce, uint8 _subAccountIdOfNonce, uint32 _nonce, bool _callTarget) external nonReentrant {
// ===Checks===
// pending withdraw record MUST be exist
bytes32 dataHash = keccak256(_data);
bytes32 withdrawWithDataHash = getWithdrawWithDataHash(_owner, _tokenAddress, _amount, dataHash, _accountIdOfNonce, _subAccountIdOfNonce, _nonce);
require(pendingWithdrawWithCalls[withdrawWithDataHash], "z0");

// ===Effects===
pendingWithdrawWithCalls[withdrawWithDataHash] = false;

if (_callTarget) {
// decode data
(address targetContract, bytes memory callData) = abi.decode(_data, (address, bytes));
_withdrawTo(payable(targetContract), _tokenAddress, _amount, callData);
} else {
IERC20(tokenAddress).safeTransfer(_owner, amount);
_withdrawTo(_owner, _tokenAddress, _amount, new bytes(0));
}
emit WithdrawalCall(withdrawWithDataHash);
}

// improve withdrawn amount decimals
pendingBalances[owner][_tokenId] = balance - improveDecimals(amount, rt.decimals);
emit Withdrawal(_tokenId, amount);

return amount;
function _withdrawTo(address payable _to, address _tokenAddress, uint128 _amount, bytes memory _callData) internal {
// ===Interactions===
if (_tokenAddress == ETH_ADDRESS) {
// solhint-disable-next-line avoid-low-level-calls
(bool success, ) = _to.call{value: _amount}(_callData);
require(success, "b1");
} else {
IERC20(_tokenAddress).safeTransfer(_to, _amount);
if (_callData.length > 0) {
// solhint-disable-next-line avoid-low-level-calls
(bool success, ) = _to.call(_callData);
require(success, "b2");
}
}
}

/// @notice Returns amount of tokens that can be withdrawn by `address` from zkLink contract
Expand Down
2 changes: 1 addition & 1 deletion contracts/dev-contracts/ZkLinkTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ contract ZkLinkTest is ZkLink {
// #endif

function testExecuteWithdraw(Operations.Withdraw memory op) external {
_executeWithdraw(op.accountId, op.subAccountId, op.nonce, op.owner, op.tokenId, op.amount, op.fastWithdrawFeeRate, op.withdrawToL1);
_executeWithdraw(op.accountId, op.subAccountId, op.nonce, op.owner, op.tokenId, op.amount, op.dataHash, op.fastWithdrawFeeRate, op.withdrawToL1);
}

function testVerifyChangePubkey(bytes memory _ethWitness, Operations.ChangePubKey memory _changePk) external pure returns (bool) {
Expand Down
2 changes: 1 addition & 1 deletion contracts/zksync/Config.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ contract Config {
/// @dev Operation chunks
uint256 internal constant DEPOSIT_BYTES = 3 * CHUNK_BYTES;
uint256 internal constant FULL_EXIT_BYTES = 3 * CHUNK_BYTES;
uint256 internal constant WITHDRAW_BYTES = 3 * CHUNK_BYTES;
uint256 internal constant WITHDRAW_BYTES = 5 * CHUNK_BYTES;
uint256 internal constant FORCED_EXIT_BYTES = 3 * CHUNK_BYTES;
uint256 internal constant CHANGE_PUBKEY_BYTES = 3 * CHUNK_BYTES;

Expand Down
3 changes: 3 additions & 0 deletions contracts/zksync/Events.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ interface Events {
/// @notice Event emitted when user funds are withdrawn from the zkLink state but not from contract
event WithdrawalPending(uint16 indexed tokenId, bytes32 indexed recepient, uint128 amount);

/// @notice Event emitted when user funds are withdrawn from the zkLink state to a target contract
event WithdrawalCall(bytes32 indexed withdrawHash);

/// @notice Event emitted when user funds are withdrawn from the zkLink state to L1 and contract
event WithdrawalL1(bytes32 indexed withdrawHash);

Expand Down
4 changes: 3 additions & 1 deletion contracts/zksync/Operations.sol
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,14 @@ library Operations {
uint16 tokenId; // the token that to withdraw
//uint16 srcTokenId; -- the token that decreased in L2, present in pubdata, ignored at serialization
uint128 amount; // the token amount to withdraw
bytes32 dataHash; // the call data for withdraw
//uint16 fee; -- present in pubdata, ignored at serialization
//bytes12 addressPrefixZero; -- address bytes length in L2 is 32
address owner; // the address to receive token
uint32 nonce; // the sub account nonce
uint16 fastWithdrawFeeRate; // fast withdraw fee rate taken by acceptor
uint8 withdrawToL1; // when this flag is 1, it means withdraw token to L1
} // 68 bytes
} // 100 bytes

function readWithdrawPubdata(bytes memory _data) internal pure returns (Withdraw memory parsed) {
// NOTE: there is no check that variable sizes are same as constants (i.e. TOKEN_BYTES), fix if possible.
Expand All @@ -183,6 +184,7 @@ library Operations {
(offset, parsed.tokenId) = Bytes.readUInt16(_data, offset);
offset += TOKEN_BYTES;
(offset, parsed.amount) = Bytes.readUInt128(_data, offset);
(offset, parsed.dataHash) = Bytes.readBytes32(_data, offset);
offset += FEE_BYTES;
offset += ADDRESS_PREFIX_ZERO_BYTES;
(offset, parsed.owner) = Bytes.readAddress(_data, offset);
Expand Down