diff --git a/contracts/contracts/dev-contracts/PendingBalanceWithdrawer.sol b/contracts/contracts/dev-contracts/PendingBalanceWithdrawer.sol new file mode 100644 index 0000000000..5f80a5ff3f --- /dev/null +++ b/contracts/contracts/dev-contracts/PendingBalanceWithdrawer.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.7.0; + +pragma experimental ABIEncoderV2; + +import "../ZkSync.sol"; + +contract PendingBalanceWithdrawer { + ZkSync immutable zkSync; + + struct RequestWithdrawFT { + address payable owner; + address token; + uint256 gas; + } + + struct RequestWithdrawNFT { + uint32 tokenId; + uint256 gas; + } + + constructor(address _zkSync) { + zkSync = ZkSync(_zkSync); + } + + function withdrawPendingBalances( + RequestWithdrawFT[] calldata _FTRequests, + RequestWithdrawNFT[] calldata _NFTRequests + ) external { + for (uint256 i = 0; i < _FTRequests.length; ++i) { + try + zkSync.withdrawPendingBalance{gas: _FTRequests[i].gas}( + _FTRequests[i].owner, + _FTRequests[i].token, + type(uint128).max + ) + {} catch {} + } + + for (uint256 i = 0; i < _NFTRequests.length; ++i) { + try zkSync.withdrawPendingNFTBalance{gas: _NFTRequests[i].gas}(_NFTRequests[i].tokenId) {} catch {} + } + } +} diff --git a/contracts/scripts/deploy-testkit.ts b/contracts/scripts/deploy-testkit.ts index dc4fc2abc3..c9d98d62a0 100644 --- a/contracts/scripts/deploy-testkit.ts +++ b/contracts/scripts/deploy-testkit.ts @@ -58,6 +58,13 @@ async function main() { const testWallet = Wallet.fromMnemonic(ethTestConfig.test_mnemonic, "m/44'/60'/0'/0/" + i).connect(provider); await (await erc20.mint(testWallet.address, '0x4B3B4CA85A86C47A098A224000000000')).wait(); } + const pendingWithdrawer = await deployContract( + deployWallet, + readContractCode('dev-contracts/PendingBalanceWithdrawer'), + [deployer.addresses.ZkSync], + { gasLimit: 5000000 } + ); + console.log(`CONTRACTS_PENDING_BALANCE_WITHDRAWER=${pendingWithdrawer.address}`); } main() diff --git a/core/tests/testkit/src/bin/gas_price_test.rs b/core/tests/testkit/src/bin/gas_price_test.rs index e4b7f6d570..72c9b27a00 100644 --- a/core/tests/testkit/src/bin/gas_price_test.rs +++ b/core/tests/testkit/src/bin/gas_price_test.rs @@ -47,6 +47,8 @@ struct CostsSample { verify_cost: BigInt, /// Operator withdrawal gas cost withdrawals_cost: BigInt, + /// Pending withdrawer withdrawal gas cost + pending_withdrawals_cost: BigInt, } impl CostsSample { @@ -64,6 +66,14 @@ impl CostsSample { .gas_used .map(u256_to_bigint) .expect("verify gas used"), + pending_withdrawals_cost: block_result + .pending_withdrawals_result + .map(|rec| { + rec.gas_used + .map(u256_to_bigint) + .expect("pending withdrawals gas used") + }) + .unwrap_or_default(), withdrawals_cost: block_result .withdrawals_result .gas_used @@ -80,13 +90,15 @@ impl CostsSample { let commit_cost = (&self.commit_cost - &base_cost.base_commit_cost) / samples; let verify_cost = (&self.verify_cost - &base_cost.base_verify_cost) / samples; let withdraw_cost = (&self.withdrawals_cost - &base_cost.base_withdraw_cost) / samples; - let total = &commit_cost + &verify_cost + &withdraw_cost; + let pending_withdraw_cost = &self.pending_withdrawals_cost / samples; + let total = &commit_cost + &verify_cost + &withdraw_cost + &pending_withdraw_cost; CostPerOperation { user_gas_cost, commit_cost, verify_cost, withdraw_cost, + pending_withdraw_cost, total, } } @@ -110,6 +122,7 @@ struct CostPerOperation { commit_cost: BigInt, verify_cost: BigInt, withdraw_cost: BigInt, + pending_withdraw_cost: BigInt, total: BigInt, } @@ -139,12 +152,13 @@ impl CostPerOperation { String::new() }; println!( - "Gas cost of {}:\nuser_gas_cost: {}\ncommit: {}\nprove: {}\nexecute: {}\ntotal: {}{}", + "Gas cost of {}:\nuser_gas_cost: {}\ncommit: {}\nprove: {}\nexecute: {}\npending_withdraw: {}\ntotal: {}{}", description, self.user_gas_cost, self.commit_cost, self.verify_cost, self.withdraw_cost, + self.pending_withdraw_cost, self.total, grief_info ); diff --git a/core/tests/testkit/src/eth_account.rs b/core/tests/testkit/src/eth_account.rs index d6988892fc..41e765dcc4 100644 --- a/core/tests/testkit/src/eth_account.rs +++ b/core/tests/testkit/src/eth_account.rs @@ -1,7 +1,8 @@ use crate::external_commands::js_revert_reason; +use std::collections::HashMap; use anyhow::{bail, ensure, format_err}; -use ethabi::Token; +use ethabi::{Contract, Token, Uint}; use num::{BigUint, ToPrimitive}; use std::convert::TryFrom; use std::str::FromStr; @@ -18,7 +19,7 @@ use zksync_types::aggregated_operations::{ stored_block_info, BlocksCommitOperation, BlocksExecuteOperation, BlocksProofOperation, }; use zksync_types::block::Block; -use zksync_types::{AccountId, Address, Nonce, PriorityOp, PubKeyHash, TokenId}; +use zksync_types::{AccountId, Address, Nonce, PriorityOp, PubKeyHash, TokenId, ZkSyncTx}; pub fn parse_ether(eth_value: &str) -> Result { let split = eth_value.split('.').collect::>(); @@ -429,6 +430,67 @@ impl EthereumAccount { Ok(ETHExecResult::new(receipt, &self.main_contract_eth_client).await) } + // Completes pending withdrawals. + pub async fn execute_pending_withdrawals( + &self, + execute_operation: &BlocksExecuteOperation, + tokens: &HashMap, + pending_withdrawer_contract: &(Contract, Address), + ) -> Result, anyhow::Error> { + let ex_ops: Vec<_> = execute_operation + .blocks + .iter() + .flat_map(|b| { + b.block_transactions.iter().filter(|a| { + a.is_successful() + && (a.variance_name() == "Withdraw" || a.variance_name() == "WithdrawNFT") + }) + }) + .collect(); + let mut ft_balances = vec![]; + let mut nft_balances = vec![]; + for ex_op in ex_ops { + let ex_op = ex_op.get_executed_tx().unwrap().signed_tx.clone().tx; + match ex_op { + ZkSyncTx::Withdraw(tx) => ft_balances.push(Token::Tuple(vec![ + Token::Address(tx.to), + Token::Address(*tokens.get(&tx.token).unwrap()), + Token::Uint(Uint::from(200_000)), + ])), + ZkSyncTx::WithdrawNFT(tx) => nft_balances.push(Token::Tuple(vec![ + Token::Uint(Uint::from_str(&tx.token.0.to_string()).unwrap()), + Token::Uint(Uint::from(300_000)), + ])), + _ => unreachable!(), + }; + } + + if ft_balances.is_empty() && nft_balances.is_empty() { + return Ok(None); + } + + let f = pending_withdrawer_contract + .0 + .function("withdrawPendingBalances") + .expect("failed to get function parameters"); + + let tokens = vec![Token::Array(ft_balances), Token::Array(nft_balances)]; + let data = f + .encode_input(&tokens) + .expect("failed to encode parameters"); + let signed_tx = self + .main_contract_eth_client + .sign_prepared_tx_for_addr(data, pending_withdrawer_contract.1, Options::default()) + .await + .map_err(|e| format_err!("Complete withdrawals send err: {}", e))?; + + let receipt = + send_raw_tx_wait_confirmation(&self.main_contract_eth_client, signed_tx.raw_tx).await?; + Ok(Some( + ETHExecResult::new(receipt, &self.main_contract_eth_client).await, + )) + } + pub async fn revert_blocks(&self, blocks: &[Block]) -> Result { let tx_arg = Token::Array(blocks.iter().map(stored_block_info).collect()); diff --git a/core/tests/testkit/src/external_commands.rs b/core/tests/testkit/src/external_commands.rs index 0e857063cb..ee3d9bbd0e 100644 --- a/core/tests/testkit/src/external_commands.rs +++ b/core/tests/testkit/src/external_commands.rs @@ -2,11 +2,13 @@ //! `zk` script should be in path. //! use std::collections::HashMap; +use std::fs::read; use std::process::Command; use std::str::FromStr; use web3::types::{Address, H256}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use zksync_crypto::convert::FeConvert; use zksync_crypto::Fr; @@ -17,6 +19,7 @@ pub struct Contracts { pub contract: Address, pub upgrade_gatekeeper: Address, pub test_erc20_address: Address, + pub pending_withdrawer: (ethabi::Contract, Address), } fn get_contract_address(deploy_script_out: &str) -> Option<(String, Address)> { @@ -42,6 +45,13 @@ fn get_contract_address(deploy_script_out: &str) -> Option<(String, Address)> { String::from("CONTRACTS_UPGRADE_GATEKEEPER_ADDR"), Address::from_str(output).expect("can't parse contract address"), )) + } else if let Some(output) = + deploy_script_out.strip_prefix("CONTRACTS_PENDING_BALANCE_WITHDRAWER=0x") + { + Some(( + String::from("CONTRACTS_PENDING_BALANCE_WITHDRAWER"), + Address::from_str(output).expect("can't parse contract address"), + )) } else { deploy_script_out .strip_prefix("CONTRACTS_TEST_ERC20=0x") @@ -54,6 +64,13 @@ fn get_contract_address(deploy_script_out: &str) -> Option<(String, Address)> { } } +fn pending_withdrawer_contract() -> ethabi::Contract { + let path = "contracts/artifacts/cache/solpp-generated-contracts/dev-contracts/PendingBalanceWithdrawer.sol/PendingBalanceWithdrawer.json"; + let pending_withdrawer_abi: Value = + serde_json::from_slice(read(path).unwrap().as_slice()).unwrap(); + serde_json::from_value(pending_withdrawer_abi.get("abi").unwrap().clone()).unwrap() +} + /// Runs external command and returns stdout output fn run_external_command(command: &str, args: &[&str]) -> String { let result = Command::new(command) @@ -120,6 +137,12 @@ pub fn deploy_contracts(use_prod_contracts: bool, genesis_root: Fr) -> Contracts test_erc20_address: contracts .remove("CONTRACTS_TEST_ERC20") .expect("TEST_ERC20 missing"), + pending_withdrawer: ( + pending_withdrawer_contract(), + contracts + .remove("CONTRACTS_PENDING_BALANCE_WITHDRAWER") + .expect("CONTRACTS_PENDING_BALANCE_WITHDRAWER missing"), + ), } } diff --git a/core/tests/testkit/src/test_setup.rs b/core/tests/testkit/src/test_setup.rs index e996144607..dd4f58e427 100644 --- a/core/tests/testkit/src/test_setup.rs +++ b/core/tests/testkit/src/test_setup.rs @@ -48,6 +48,7 @@ pub struct TestSetup { pub accounts: AccountSet, pub tokens: HashMap, + pub deployed_contracts: Contracts, pub expected_changes_for_current_block: ExpectedAccountState, @@ -94,6 +95,7 @@ impl TestSetup { processed_tx_events_receiver: sk_channels.queued_txs_events, accounts, tokens, + deployed_contracts: deployed_contracts.clone(), expected_changes_for_current_block: ExpectedAccountState::default(), commit_account, current_state_root: Some(initial_root), @@ -996,6 +998,7 @@ impl TestSetup { .await .expect("block commit send tx") .expect_success(); + self.last_committed_block = new_block.clone(); new_block @@ -1081,7 +1084,6 @@ impl TestSetup { .send(StateKeeperTestkitRequest::SealBlock) .await .expect("sk receiver dropped"); - let new_block = self.await_for_block_commit().await; self.current_state_root = Some(new_block.new_root_hash); @@ -1119,6 +1121,16 @@ impl TestSetup { .await .expect("execute block tx") .expect_success(); + let pending_withdrawals_result = self + .commit_account + .execute_pending_withdrawals( + &block_execute_op, + &self.tokens, + &self.deployed_contracts.pending_withdrawer, + ) + .await + .expect("execute block tx") + .map(|a| a.expect_success()); self.last_committed_block = new_block.clone(); @@ -1167,6 +1179,7 @@ impl TestSetup { commit_result, verify_result, withdrawals_result, + pending_withdrawals_result, block_chunks, )) } diff --git a/core/tests/testkit/src/types.rs b/core/tests/testkit/src/types.rs index 31c60d75d8..7cb3a0f278 100644 --- a/core/tests/testkit/src/types.rs +++ b/core/tests/testkit/src/types.rs @@ -43,6 +43,7 @@ pub struct BlockExecutionResult { pub commit_result: TransactionReceipt, pub verify_result: TransactionReceipt, pub withdrawals_result: TransactionReceipt, + pub pending_withdrawals_result: Option, pub block_size_chunks: usize, } @@ -52,6 +53,7 @@ impl BlockExecutionResult { commit_result: TransactionReceipt, verify_result: TransactionReceipt, withdrawals_result: TransactionReceipt, + pending_withdrawals_result: Option, block_size_chunks: usize, ) -> Self { Self { @@ -59,6 +61,7 @@ impl BlockExecutionResult { commit_result, verify_result, withdrawals_result, + pending_withdrawals_result, block_size_chunks, } }