Skip to content

Commit

Permalink
add data hash for withdraw (#141)
Browse files Browse the repository at this point in the history
* add data hash for withdraw

* update
  • Loading branch information
zkbenny authored Jan 20, 2024
1 parent c12ec34 commit 675358c
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 29 deletions.
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

0 comments on commit 675358c

Please sign in to comment.