diff --git a/libdevcore/LevelDB.h b/libdevcore/LevelDB.h index 9674cb9ec..5aa1f7df0 100644 --- a/libdevcore/LevelDB.h +++ b/libdevcore/LevelDB.h @@ -71,6 +71,7 @@ class LevelDB : public DatabaseFace { static std::atomic< uint64_t > g_keyDeletesStats; // count of the keys that are scheduled to be deleted but are not yet deleted static std::atomic< uint64_t > g_keysToBeDeletedStats; + static uint64_t getCurrentTimeMs(); private: std::unique_ptr< leveldb::DB > m_db; @@ -123,7 +124,6 @@ class LevelDB : public DatabaseFace { } }; void openDBInstanceUnsafe(); - uint64_t getCurrentTimeMs(); void reopenDataBaseIfNeeded(); }; diff --git a/libethereum/Client.cpp b/libethereum/Client.cpp index 1cd97c9f3..faf04ec7d 100644 --- a/libethereum/Client.cpp +++ b/libethereum/Client.cpp @@ -674,10 +674,13 @@ size_t Client::syncTransactions( // Tell network about the new transactions. m_skaleHost->noteNewTransactions(); - ctrace << cc::debug( "Processed " ) << cc::size10( newPendingReceipts.size() ) - << cc::debug( " transactions in " ) << cc::size10( timer.elapsed() * 1000 ) - << cc::debug( "(" ) << ( bool ) m_syncTransactionQueue << cc::debug( ")" ); + ctrace << "Processed " << newPendingReceipts.size() << " transactions in " + << timer.elapsed() * 1000 << "(" << ( bool ) m_syncTransactionQueue << ")"; +#ifdef HISTORIC_STATE + LOG( m_logger ) << "HSCT: " + << m_working.mutableState().mutableHistoricState().getAndResetBlockCommitTime(); +#endif return goodReceipts; } diff --git a/libethereum/SkaleHost.cpp b/libethereum/SkaleHost.cpp index a2f9fcd9e..47714ebfe 100644 --- a/libethereum/SkaleHost.cpp +++ b/libethereum/SkaleHost.cpp @@ -731,6 +731,7 @@ void SkaleHost::createBlock( const ConsensusExtFace::transactions_vector& _appro skaledTimeFinish - skaledTimeStart ) .count(); } + latestBlockTime = skaledTimeFinish; LOG( m_debugLogger ) << "Successfully imported " << n_succeeded << " of " << out_txns.size() << " transactions"; diff --git a/libevm/LegacyVM.cpp b/libevm/LegacyVM.cpp index f0128a9f2..a6d64254d 100644 --- a/libevm/LegacyVM.cpp +++ b/libevm/LegacyVM.cpp @@ -231,25 +231,29 @@ void LegacyVM::interpretCases() { // CASE( CREATE2 ) { - // for CREATE and CREATE2 we call ON_OP in caseCreate, since it calculates - // correct gas cost - // ON_OP(); - if ( !m_schedule->haveCreate2 ) + if ( !m_schedule->haveCreate2 ) { + ON_OP(); throwBadInstruction(); - if ( m_ext->staticCall ) + } + if ( m_ext->staticCall ) { + ON_OP(); throwDisallowedStateChange(); + } + // for CREATE and CREATE2 we call ON_OP in caseCreate, since it calculates + // correct gas cost m_bounce = &LegacyVM::caseCreate; } BREAK CASE( CREATE ) { - // for CREATE and CREATE2 we call ON_OP in caseCreate, since it calculates - // correct gas cost - // ON_OP(); - if ( m_ext->staticCall ) + if ( m_ext->staticCall ) { + ON_OP(); throwDisallowedStateChange(); + } + // for CREATE and CREATE2 we call ON_OP in caseCreate, since it calculates + // correct gas cost m_bounce = &LegacyVM::caseCreate; } BREAK diff --git a/libhistoric/AlethExecutive.cpp b/libhistoric/AlethExecutive.cpp index fbe3de73e..b5c6cc372 100644 --- a/libhistoric/AlethExecutive.cpp +++ b/libhistoric/AlethExecutive.cpp @@ -362,8 +362,7 @@ bool AlethExecutive::go( OnOpFunc const& _onOp ) { throw; #ifdef HISTORIC_STATE } catch ( VMTracingError const& _e ) { - cwarn << "Tracing error: " << *boost::get_error_info< errinfo_evmcStatusCode >( _e ) - << ")"; + cerr << "VM tracing error:" << _e.message; revert(); throw; #endif diff --git a/libhistoric/AlethStandardTrace.cpp b/libhistoric/AlethStandardTrace.cpp index affcdf988..bc840eab8 100644 --- a/libhistoric/AlethStandardTrace.cpp +++ b/libhistoric/AlethStandardTrace.cpp @@ -207,7 +207,8 @@ void AlethStandardTrace::setTopFunctionCall( void AlethStandardTrace::recordFunctionReturned( evmc_status_code _status, const vector< uint8_t >& _returnData, uint64_t _gasUsed ) { - STATE_CHECK( getLastOpRecord()->m_gasRemaining >= getLastOpRecord()->m_opGas ) + // note that m_gas remaining can be less than m_opGas. This happens in case + // of out of gas revert STATE_CHECK( m_currentlyExecutingFunctionCall ) STATE_CHECK( !m_isFinalized ) @@ -348,7 +349,6 @@ shared_ptr< OpExecutionRecord > AlethStandardTrace::createOpExecutionRecord( uin opName = "KECCAK256"; } - auto executionRecord = std::make_shared< OpExecutionRecord >( ext.depth, _inst, ( uint64_t ) _gasRemaining, ( uint64_t ) _gasOpGas, _pc, ext.sub.refunds, opName ); diff --git a/libhistoric/CallTracePrinter.cpp b/libhistoric/CallTracePrinter.cpp index ea7a46ab9..ed2136a03 100644 --- a/libhistoric/CallTracePrinter.cpp +++ b/libhistoric/CallTracePrinter.cpp @@ -52,6 +52,13 @@ void CallTracePrinter::printContractTransactionTrace( // call trace on the top Solidity function call in the stack // this will also recursively call printTrace on nested calls if exist topFunctionCall->printTrace( _jsonTrace, _statePost, 0, m_trace.getOptions() ); + + // for the top function the gas limit that geth prints is the transaction gas limit + // and not the gas limit given to the top level function, which is smaller because of 21000 + // transaction cost and potentially eth transfer cost + _jsonTrace["gas"] = + AlethStandardTrace::toGethCompatibleCompactHexPrefixed( m_trace.getGasLimit() ); + // handle the case of a transaction that deploys a contract // in this case geth prints transaction input data as input // end prints to as newly created contract address diff --git a/libhistoric/HistoricState.cpp b/libhistoric/HistoricState.cpp index 6d5ef615c..9f3938a71 100644 --- a/libhistoric/HistoricState.cpp +++ b/libhistoric/HistoricState.cpp @@ -44,7 +44,8 @@ HistoricState::HistoricState( HistoricState const& _s ) m_unchangedCacheEntries( _s.m_unchangedCacheEntries ), m_nonExistingAccountsCache( _s.m_nonExistingAccountsCache ), m_unrevertablyTouched( _s.m_unrevertablyTouched ), - m_accountStartNonce( _s.m_accountStartNonce ) {} + m_accountStartNonce( _s.m_accountStartNonce ), + m_totalTimeSpentInStateCommitsPerBlock( _s.m_totalTimeSpentInStateCommitsPerBlock ) {} OverlayDB HistoricState::openDB( fs::path const& _basePath, h256 const& _genesisHash, WithExisting _we ) { @@ -137,6 +138,7 @@ HistoricState& HistoricState::operator=( HistoricState const& _s ) { m_nonExistingAccountsCache = _s.m_nonExistingAccountsCache; m_unrevertablyTouched = _s.m_unrevertablyTouched; m_accountStartNonce = _s.m_accountStartNonce; + m_totalTimeSpentInStateCommitsPerBlock = _s.m_totalTimeSpentInStateCommitsPerBlock; return *this; } @@ -194,11 +196,20 @@ void HistoricState::clearCacheIfTooLarge() const { } void HistoricState::commitExternalChanges( AccountMap const& _accountMap ) { + auto historicStateStart = dev::db::LevelDB::getCurrentTimeMs(); commitExternalChangesIntoTrieDB( _accountMap, m_state ); m_state.db()->commit(); m_changeLog.clear(); m_cache.clear(); m_unchangedCacheEntries.clear(); + auto historicStateFinish = dev::db::LevelDB::getCurrentTimeMs(); + m_totalTimeSpentInStateCommitsPerBlock += historicStateFinish - historicStateStart; +} + +uint64_t HistoricState::getAndResetBlockCommitTime() { + uint64_t retVal = m_totalTimeSpentInStateCommitsPerBlock; + m_totalTimeSpentInStateCommitsPerBlock = 0; + return retVal; } diff --git a/libhistoric/HistoricState.h b/libhistoric/HistoricState.h index 201e214d3..8c3a8cb4a 100644 --- a/libhistoric/HistoricState.h +++ b/libhistoric/HistoricState.h @@ -296,6 +296,8 @@ class HistoricState { void setRootFromDB(); + uint64_t getAndResetBlockCommitTime(); + private: /// Turns all "touched" empty accounts into non-alive accounts. void removeEmptyAccounts(); @@ -345,6 +347,8 @@ class HistoricState { AddressHash commitExternalChangesIntoTrieDB( AccountMap const& _cache, SecureTrieDB< Address, OverlayDB >& _state ); + + uint64_t m_totalTimeSpentInStateCommitsPerBlock = 0; }; std::ostream& operator<<( std::ostream& _out, HistoricState const& _s ); diff --git a/test/historicstate/hardhat/contracts/Erc20Custom.sol b/test/historicstate/hardhat/contracts/Erc20Custom.sol new file mode 100644 index 000000000..fe4a6a36f --- /dev/null +++ b/test/historicstate/hardhat/contracts/Erc20Custom.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +/** + * ERC20Custom.sol - SKALE Test tokens + * Copyright (C) 2022-Present SKALE Labs + * @author Artem Payvin + * + * SKALE IMA is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SKALE IMA is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with SKALE IMA. If not, see . + */ + +pragma solidity ^0.8.20; + +contract ERC20Custom { + + mapping (address => uint256) private _balances; + + mapping (address => mapping (address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + uint8 private _decimals; + + event Approval(address owner, address spender, uint256 amount); + + event Transfer(address from, address to, uint256 amount); + + constructor(string memory tokenName, string memory tokenSymbol) { + _name = tokenName; + _symbol = tokenSymbol; + _decimals = 18; + + _mint(tx.origin, 10); + } + + function mint(address account, uint256 amount) external returns (bool) { + _mint(account, amount); + return true; + } + + /** + * @dev burn - destroys token on msg sender + * + * NEED TO HAVE THIS FUNCTION ON SKALE-CHAIN + * + * @param amount - amount of tokens + */ + function burn(uint256 amount) external { + _burn(msg.sender, amount); + } + + function name() public view returns (string memory) { + return _name; + } + + function symbol() public view returns (string memory) { + return _symbol; + } + + function decimals() public view returns (uint8) { + return _decimals; + } + + function totalSupply() public view returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) public view returns (uint256) { + return _balances[account]; + } + + function transfer(address recipient, uint256 amount) public virtual returns (bool) { + this._transfer1(); + return true; + } + + function allowance(address owner, address spender) public view virtual returns (uint256) { + return _allowances[owner][spender]; + } + + function approve(address spender, uint256 amount) public virtual returns (bool) { + _approve(msg.sender, spender, amount); + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) public virtual returns (bool) { + _transfer(sender, recipient, amount); + _approve( + sender, + msg.sender, + _allowances[sender][msg.sender] - amount + ); + return true; + } + + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + _approve(msg.sender, spender, _allowances[msg.sender][spender] + addedValue); + return true; + } + + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + _approve( + msg.sender, + spender, + _allowances[msg.sender][spender] - subtractedValue + ); + return true; + } + + function _transfer1() public { + } + + function _transfer(address sender, address recipient, uint256 amount) internal virtual { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _balances[sender] = _balances[sender] - amount; + _balances[recipient] = _balances[recipient] + amount; + emit Transfer(sender, recipient, amount); + } + + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _totalSupply = _totalSupply + amount; + _balances[account] = _balances[account] + amount; + emit Transfer(address(0), account, amount); + } + + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _balances[account] = _balances[account] - amount; + _totalSupply = _totalSupply - amount; + emit Transfer(account, address(0), amount); + } + + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } +} \ No newline at end of file diff --git a/test/historicstate/hardhat/run_geth.py b/test/historicstate/hardhat/run_geth.py index 22d576487..f1dd69709 100755 --- a/test/historicstate/hardhat/run_geth.py +++ b/test/historicstate/hardhat/run_geth.py @@ -50,7 +50,7 @@ def add_ether_to_account(address, amount): return # Unlock the default account (coinbase) - coinbase = w3.eth.coinbase + coinbase = w3.eth.accounts[0] w3.geth.personal.unlock_account(coinbase, '', 0) # Convert Ether to Wei diff --git a/test/historicstate/hardhat/scripts/geth_traces/ERC20Custom.transfer.callTracer.json b/test/historicstate/hardhat/scripts/geth_traces/ERC20Custom.transfer.callTracer.json new file mode 100644 index 000000000..9cc83b2b1 --- /dev/null +++ b/test/historicstate/hardhat/scripts/geth_traces/ERC20Custom.transfer.callTracer.json @@ -0,0 +1,21 @@ +{ + "calls": [ + { + "error": "out of gas", + "from": "ERC20Custom.address", + "gas": "0x51", + "gasUsed": "0x30d", + "input": "0x53bb9b8a", + "to": "ERC20Custom.address", + "type": "CALL" + } + ], + "error": "out of gas", + "from": "OWNER.address", + "gas": "0x6239", + "gasUsed": "0x6239", + "input": "0xa9059cbb000000000000000000000000ce5c7ca85f8cb94fa284a303348ef42add23f5e70000000000000000000000000000000000000000000000000000000000000001", + "to": "ERC20Custom.address", + "type": "CALL", + "value": "0x0" +} \ No newline at end of file diff --git a/test/historicstate/hardhat/scripts/token_revert.ts b/test/historicstate/hardhat/scripts/token_revert.ts new file mode 100644 index 000000000..dcbf3a5bf --- /dev/null +++ b/test/historicstate/hardhat/scripts/token_revert.ts @@ -0,0 +1,402 @@ +import {mkdir, readdir, writeFileSync, readFile, unlink} from "fs"; + +const fs = require('fs'); +import {existsSync} from "fs"; +import deepDiff, {diff} from 'deep-diff'; +import {expect} from "chai"; +import * as path from 'path'; +import {int, string} from "hardhat/internal/core/params/argumentTypes"; +import internal from "node:stream"; +import exp from "node:constants"; + +const OWNER_ADDRESS: string = "0x907cd0881E50d359bb9Fd120B1A5A143b1C97De6"; +const CALL_ADDRESS: string = "0xCe5c7ca85F8cB94FA284a303348ef42ADD23f5e7"; + +const ZERO_ADDRESS: string = '0x0000000000000000000000000000000000000000'; + +const SKALE_TRACES_DIR = "/tmp/skale_traces/" +const GETH_TRACES_DIR = "scripts/geth_traces/" + +const DEFAULT_TRACER = "defaultTracer"; +const CALL_TRACER = "callTracer"; +const PRESTATE_TRACER = "prestateTracer"; +const PRESTATEDIFF_TRACER = "prestateDiffTracer"; +const FOURBYTE_TRACER = "4byteTracer"; +const REPLAY_TRACER = "replayTracer"; + +var DEPLOYED_CONTRACT_ADDRESS_LOWER_CASE: string = ""; +var globalCallCount = 0; + + +async function replaceAddressesWithSymbolicNames(_traceFileName: string) { + + let callAddressLowerCase = CALL_ADDRESS.toLowerCase(); + + await replaceStringInFile(SKALE_TRACES_DIR + _traceFileName, + callAddressLowerCase, "CALL.address"); + + let ownerAddressLowerCase = OWNER_ADDRESS.toLowerCase(); + + await replaceStringInFile(SKALE_TRACES_DIR + _traceFileName, + ownerAddressLowerCase, "OWNER.address"); + + // if the contract has been deployed, also replace contract address + + if (DEPLOYED_CONTRACT_ADDRESS_LOWER_CASE.length > 0) { + await replaceStringInFile(SKALE_TRACES_DIR + _traceFileName, + DEPLOYED_CONTRACT_ADDRESS_LOWER_CASE, TEST_CONTRACT_NAME + ".address"); + + } + + +} + +async function getTraceJsonOptions(_tracer: string): Promise { + if (_tracer == DEFAULT_TRACER) { + return {disableStack: true}; + } + + if (_tracer == PRESTATEDIFF_TRACER) { + return {"tracer": PRESTATE_TRACER, "tracerConfig": {diffMode: true}}; + } + + return {"tracer": _tracer} +} + + +async function deleteAndRecreateDirectory(dirPath: string): Promise { + try { + // Remove the directory and its contents + await deleteDirectory(dirPath); + } catch (error) { + } + try { + + // Recreate the directory + + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath); + } + console.log(`Directory recreated: ${dirPath}`); + } catch (error) { + console.error('An error occurred:', error); + } +} + +async function deleteDirectory(dirPath: string): Promise { + if (!fs.existsSync(dirPath)) + return; + const entries = fs.readdirSync(dirPath); + + // Iterate over directory contents + for (const entry of entries) { + const entryPath = path.join(dirPath, entry.name); + + if (entry.isDirectory()) { + // Recursive call for nested directories + await deleteDirectory(entryPath); + } else { + // Delete file + await fs.unlink(entryPath); + } + } + + // Delete the now-empty directory + await fs.rmdir(dirPath); +} + + +/** + * Replaces a string in a file. + * @param {string} _filePath - The path to the file. + * @param {string} _str1 - The string to be replaced. + * @param {string} _str2 - The string to replace with. + */ +async function replaceStringInFile(_filePath, _str1, _str2) { + // Read the file + + let data = fs.readFileSync(_filePath, 'utf8'); + + // Replace the string + const transformedFile = data.replace(new RegExp(_str1, 'g'), _str2); + + // Write the file + fs.writeFileSync(_filePath, transformedFile, 'utf8'); + +} + + +async function waitUntilNextBlock() { + + const current = await hre.ethers.provider.getBlockNumber(); + let newBlock = current; + console.log(`BLOCK_NUMBER ${current}`); + + while (newBlock == current) { + newBlock = await hre.ethers.provider.getBlockNumber(); + } + + console.log(`BLOCK_NUMBER ${newBlock}`); + + return current; + +} + + + +const TEST_CONTRACT_NAME = "ERC20Custom"; + +async function deployTestContract(): Promise { + + console.log(`Deploying ` + TEST_CONTRACT_NAME); + + const factory = await ethers.getContractFactory(TEST_CONTRACT_NAME); + const testContractName = await factory.deploy( + "RevertTest", "RevertTest", { + gasLimit: 2100000, // this is just an example value; you'll need to set an appropriate gas limit for your specific function call + }); + const deployedTestContract = await testContractName.deployed(); + + const deployReceipt = await ethers.provider.getTransactionReceipt(deployedTestContract.deployTransaction.hash) + const deployBlockNumber: number = deployReceipt.blockNumber; + + const hash = deployedTestContract.deployTransaction.hash; + console.log(`Contract deployed to ${deployedTestContract.address} at block ${deployBlockNumber.toString(16)} tx hash ${hash}`); + + return deployedTestContract; + +} + + +function generateNewWallet() { + const wallet = hre.ethers.Wallet.createRandom(); + console.log("Address:", wallet.address); + return wallet; +} + +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + + +async function writeTraceFileReplacingAddressesWithSymbolicNames(_traceFileName: string, traceResult: string) { + writeFileSync(SKALE_TRACES_DIR + _traceFileName, traceResult); + await replaceAddressesWithSymbolicNames(_traceFileName); +} + +async function getBlockTrace(blockNumber: number): Promise { + + const blockStr = "0x" + blockNumber.toString(16); + const params : any = [blockStr, await getTraceJsonOptions(DEFAULT_TRACER)]; + console.log(params); + const trace : any = await ethers.provider.send('debug_traceBlockByNumber', params); + + return trace; +} + +async function getAndPrintCommittedTransactionTrace(hash: string, _tracer: string, _skaleFileName: string): Promise { + globalCallCount++; + + let traceOptions = await getTraceJsonOptions(_tracer); + + + + + console.log("Calling debug_traceTransaction to generate " + _skaleFileName); + + let trace; + + + if (_tracer == DEFAULT_TRACER) { + // test both empty tracer and now tracer + if (globalCallCount % 2 === 0) { + trace = await ethers.provider.send('debug_traceTransaction', [hash]); + } else { + trace = await ethers.provider.send('debug_traceTransaction', [hash, traceOptions]); + } + } else { + trace = await ethers.provider.send('debug_traceTransaction', [hash, traceOptions]); + } + + const result = JSON.stringify(trace, null, 4); + + await writeTraceFileReplacingAddressesWithSymbolicNames(_skaleFileName, result); + + return trace; +} + +async function callDebugTraceCall(_deployedContract: any, _tracer: string, _traceFileName: string): Promise { + + // first call function using eth_call + + const currentBlock = await hre.ethers.provider.getBlockNumber(); + + const transaction = { + from: CALL_ADDRESS, + to: _deployedContract.address, + data: _deployedContract.interface.encodeFunctionData("getBalance", []) + }; + + const returnData = await ethers.provider.call(transaction, currentBlock - 1); + + const result = _deployedContract.interface.decodeFunctionResult("getBalance", returnData); + + + console.log("Calling debug_traceCall to generate " + _traceFileName); + + let traceOptions = await getTraceJsonOptions(_tracer); + + const trace = await ethers.provider.send('debug_traceCall', [transaction, "latest", traceOptions]); + + const traceResult = JSON.stringify(trace, null, 4); + await writeTraceFileReplacingAddressesWithSymbolicNames(_traceFileName, traceResult); +} + +async function readJSONFile(fileName: string): Promise { + return new Promise((resolve, reject) => { + readFile(fileName, 'utf8', (err, data) => { + if (err) { + reject(`An error occurred while reading the file: ${err.message}`); + return; + } + try { + const obj: object = JSON.parse(data); + resolve(obj); + } catch (parseError: any) { + reject(`Error parsing JSON: ${parseError.message}`); + } + }); + }); +} + +const EXECUTE_FUNCTION_NAME = "transfer"; + +const TEST_CONTRACT_EXECUTE_CALLTRACER_FILE_NAME = TEST_CONTRACT_NAME + "." + EXECUTE_FUNCTION_NAME + ".callTracer.json"; + + +async function sendERCTransferWithoutConfirmation(deployedContract: any): Promise { + // Generate a new wallet + const newWallet = generateNewWallet(); + + await sleep(3000); // Sleep for 1000 milliseconds (1 second) + + + const currentNonce = await signer.getTransactionCount(); + + // Define the transaction + const tx = { + to: deployedContract.address, + gasLimit: 2100000, + nonce: currentNonce, + data: deployedContract.interface.encodeFunctionData(EXECUTE_FUNCTION_NAME, [CALL_ADDRESS, 10]) + }; + + // Send the transaction and wait until it is submitted ot the queue + const txResponse = signer.sendTransaction(tx); + + if (hre.network.name == "geth") { + await txResponse; + } + + console.log(`Submitted a tx to send 0.1 ETH to ${newWallet.address}`); + + return currentNonce; +} + + + +async function executeERC20Transfer(deployedContract: any): Promise { + + console.log("Doing transfer revert") + + // Get the first signer from Hardhat's local network + const [signer] = await hre.ethers.getSigners(); + + const currentNonce = await signer.getTransactionCount(); + + const receipt = await deployedContract[EXECUTE_FUNCTION_NAME]( CALL_ADDRESS, 1, { + gasLimit: 0x6239, // this is just an example value; you'll need to set an appropriate gas limit for your specific function call + nonce: currentNonce + }); + + expect(receipt.blockNumber).not.to.be.null; + + + const trace: string = await getBlockTrace(receipt.blockNumber); + + expect(Array.isArray(trace)); + + expect(trace.length).equal(1); + + console.log("Trace:" + JSON.stringify(trace[0])); + + return receipt.hash!; + +} + +async function verifyCallTraceAgainstGethTrace(_fileName: string) { + + console.log("Verifying " + _fileName); + + const _expectedResultFileName = GETH_TRACES_DIR + _fileName; + const _actualResultFileName = SKALE_TRACES_DIR + _fileName; + + let expectedResult = await readJSONFile(_expectedResultFileName) + let actualResult = await readJSONFile(_actualResultFileName) + + const differences = deepDiff(expectedResult, actualResult)!; + + let foundDiffs = false; + + + if (differences) { + differences.forEach((difference, index) => { + // do not print differences related to total gas in the account + + + if (difference.kind == "E" && difference.path!.length == 1) { + let key = difference.path![0]; + if (key == "to" || key == "gas" || key == "gasUsed") + return; + } + + if (difference.kind == "E" && difference.path!.length == 3 && difference.path![0] == "calls") { + let key = difference.path![2]; + if (key == "to" || key == "gas" || key == "gasUsed") + return; + } + + foundDiffs = true; + + console.log(`Found difference (lhs is expected value) ${index + 1} at path:`, difference.path); + console.log(`Difference ${index + 1}:`, difference); + }); + } + + + await expect(foundDiffs).to.be.eq(false) +} + +async function main(): Promise { + + await deleteAndRecreateDirectory(SKALE_TRACES_DIR); + + let deployedContract = await deployTestContract(); + const deployHash = deployedContract.deployTransaction.hash; + DEPLOYED_CONTRACT_ADDRESS_LOWER_CASE = deployedContract.address.toString().toLowerCase(); + + sleep(10000); + + console.log("Balance before:" + await deployedContract.balanceOf(OWNER_ADDRESS)); + const failedTransferHash: string = await executeERC20Transfer(deployedContract); + await getAndPrintCommittedTransactionTrace(failedTransferHash, CALL_TRACER, TEST_CONTRACT_EXECUTE_CALLTRACER_FILE_NAME); + console.log("Balance after:" + await deployedContract.balanceOf(OWNER_ADDRESS)); + await verifyCallTraceAgainstGethTrace(TEST_CONTRACT_EXECUTE_CALLTRACER_FILE_NAME); + +} + + +main().catch((error: any) => { + console.error(error); + process.exitCode = 1; +});