From 98e366c70b50f8abc35c385c0fe98f430d96536e Mon Sep 17 00:00:00 2001 From: emilevgenievgeorgiev Date: Tue, 23 Jul 2024 10:42:57 +0300 Subject: [PATCH] test: extended coverage for debug_traceTransaction with opcode logger tests Signed-off-by: emilevgenievgeorgiev --- .../OpcodeLogger.sol/OpcodeLogger.json | 136 +++++ .../TokenCreateTest.sol/TokenCreateTest.json | 265 ++++++++++ .../solidity/opcode-logger/OpcodeLogger.sol | 46 ++ .../opcode-logger/TokenCreateTest.sol | 112 +++++ test/constants.js | 1 + test/solidity/opcode-logger/OpcodeLogger.js | 475 +++++++++++++++++- test/utils.js | 9 + 7 files changed, 1041 insertions(+), 3 deletions(-) create mode 100644 contracts-abi/contracts/solidity/opcode-logger/TokenCreateTest.sol/TokenCreateTest.json create mode 100644 contracts/solidity/opcode-logger/TokenCreateTest.sol diff --git a/contracts-abi/contracts/solidity/opcode-logger/OpcodeLogger.sol/OpcodeLogger.json b/contracts-abi/contracts/solidity/opcode-logger/OpcodeLogger.sol/OpcodeLogger.json index 27c6e6e74..c43e7a36e 100644 --- a/contracts-abi/contracts/solidity/opcode-logger/OpcodeLogger.sol/OpcodeLogger.json +++ b/contracts-abi/contracts/solidity/opcode-logger/OpcodeLogger.sol/OpcodeLogger.json @@ -100,6 +100,142 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "int64[]", + "name": "amounts", + "type": "int64[]" + }, + { + "internalType": "bytes[]", + "name": "metadata", + "type": "bytes[]" + } + ], + "name": "executeHtsMintTokenRevertingCalls", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "int64[]", + "name": "amounts", + "type": "int64[]" + }, + { + "internalType": "bytes[]", + "name": "metadata", + "type": "bytes[]" + } + ], + "name": "executeHtsMintTokenRevertingCallsAndFailToAssociate", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "int64", + "name": "amount", + "type": "int64" + }, + { + "internalType": "bytes[]", + "name": "metadata", + "type": "bytes[]" + } + ], + "name": "mintTokenExternalCall", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "int64[]", + "name": "amounts", + "type": "int64[]" + }, + { + "internalType": "bytes[]", + "name": "metadata", + "type": "bytes[]" + } + ], + "name": "nestEverySecondHtsMintTokenCall", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "owner", diff --git a/contracts-abi/contracts/solidity/opcode-logger/TokenCreateTest.sol/TokenCreateTest.json b/contracts-abi/contracts/solidity/opcode-logger/TokenCreateTest.sol/TokenCreateTest.json new file mode 100644 index 000000000..e3b4abe88 --- /dev/null +++ b/contracts-abi/contracts/solidity/opcode-logger/TokenCreateTest.sol/TokenCreateTest.json @@ -0,0 +1,265 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "", + "type": "bool" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "CallResponseEvent", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "tokenAddress", + "type": "address" + } + ], + "name": "CreatedToken", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "kycGranted", + "type": "bool" + } + ], + "name": "KycGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "int64", + "name": "newTotalSupply", + "type": "int64" + }, + { + "indexed": false, + "internalType": "int64[]", + "name": "serialNumbers", + "type": "int64[]" + } + ], + "name": "MintedToken", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "int256", + "name": "responseCode", + "type": "int256" + } + ], + "name": "ResponseCode", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "associateTokenPublic", + "outputs": [ + { + "internalType": "int256", + "name": "responseCode", + "type": "int256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "treasury", + "type": "address" + } + ], + "name": "createFungibleTokenPublic", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "treasury", + "type": "address" + } + ], + "name": "createNonFungibleTokenPublic", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "int64", + "name": "amount", + "type": "int64" + }, + { + "internalType": "bytes[]", + "name": "metadata", + "type": "bytes[]" + } + ], + "name": "mintTokenPublic", + "outputs": [ + { + "internalType": "int256", + "name": "responseCode", + "type": "int256" + }, + { + "internalType": "int64", + "name": "newTotalSupply", + "type": "int64" + }, + { + "internalType": "int64[]", + "name": "serialNumbers", + "type": "int64[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "bytes", + "name": "encodedFunctionSelector", + "type": "bytes" + } + ], + "name": "redirectForToken", + "outputs": [ + { + "internalType": "int256", + "name": "responseCode", + "type": "int256" + }, + { + "internalType": "bytes", + "name": "response", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "int64", + "name": "responseCode", + "type": "int64" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "serialNumber", + "type": "uint256" + } + ], + "name": "transferFromNFT", + "outputs": [ + { + "internalType": "int64", + "name": "responseCode", + "type": "int64" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/contracts/solidity/opcode-logger/OpcodeLogger.sol b/contracts/solidity/opcode-logger/OpcodeLogger.sol index 6ca1ab3a5..f4c1d0b39 100644 --- a/contracts/solidity/opcode-logger/OpcodeLogger.sol +++ b/contracts/solidity/opcode-logger/OpcodeLogger.sol @@ -74,4 +74,50 @@ contract OpcodeLogger { return isSuccess; } + + function executeHtsMintTokenRevertingCalls(address contractAddress, address tokenAddress, int64[] memory amounts, bytes[] memory metadata) external returns (bool success) { + for (uint i = 0; i < amounts.length; i++) { + if (amounts[i] < 0){ + //reverts with 'Minting reveted with INVALID_TOKEN_ID' + (success,) = address(contractAddress).call(abiEncodeMintTokenPublic(contractAddress, 0, metadata)); + } else { + //reverts with 'Minting tokens reveted with TOKEN_MAX_SUPPLY_REACHED' + (success,) = address(contractAddress).call(abiEncodeMintTokenPublic(tokenAddress, amounts[i], metadata)); + } + } + } + + function executeHtsMintTokenRevertingCallsAndFailToAssociate(address contractAddress, address tokenAddress, int64[] memory amounts, bytes[] memory metadata) external returns (bool success) { + for (uint i = 0; i < amounts.length; i++) { + if (amounts[i] < 0){ + (success,) = address(contractAddress).call(abiEncodeMintTokenPublic(contractAddress, 0, metadata)); + } else { + (success,) = address(contractAddress).call(abiEncodeMintTokenPublic(tokenAddress, amounts[i], metadata)); + } + } + //reverts with 'Association reveted with TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT' + (success,) = address(contractAddress).call(abiEncodeAssociateTokenPublic(contractAddress, tokenAddress)); + } + + function nestEverySecondHtsMintTokenCall(address contractAddress, address token, int64[] memory amounts, bytes[] memory metadata) external returns (bool success) { + for (uint i = 0; i < amounts.length; i++) { + if (i % 2 == 0){ + (success,) = address(contractAddress).call(abiEncodeMintTokenPublic(token, amounts[i], metadata)); + } else { + this.mintTokenExternalCall(contractAddress, token, amounts[i], metadata); + } + } + } + + function mintTokenExternalCall(address contractAddress, address token, int64 amount, bytes[] memory metadata) external returns (bool success) { + (success,) = address(contractAddress).call(abiEncodeMintTokenPublic(token, amount, metadata)); + } + + function abiEncodeMintTokenPublic(address token, int64 amount, bytes[] memory metadata) internal pure returns (bytes memory abiEncodedData) { + return abi.encodeWithSignature("mintTokenPublic(address,int64,bytes[])", token, amount, metadata); + } + + function abiEncodeAssociateTokenPublic(address account, address token) internal pure returns (bytes memory abiEncodedData) { + return abi.encodeWithSignature("associateTokenPublic(address,address)", account, token); + } } diff --git a/contracts/solidity/opcode-logger/TokenCreateTest.sol b/contracts/solidity/opcode-logger/TokenCreateTest.sol new file mode 100644 index 000000000..a5ec170ba --- /dev/null +++ b/contracts/solidity/opcode-logger/TokenCreateTest.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.9.0; +pragma experimental ABIEncoderV2; + +import "contracts/system-contracts/hedera-token-service/HederaTokenService.sol"; +import "contracts/system-contracts/hedera-token-service/ExpiryHelper.sol"; +import "contracts/system-contracts/hedera-token-service/KeyHelper.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + +contract TokenCreateTest is HederaTokenService, ExpiryHelper, KeyHelper { + + string name = "tokenName"; + string symbol = "tokenSymbol"; + string memo = "memo"; + int64 initialTotalSupply = 10000; + int64 maxSupply = 10000; + int32 decimals = 8; + bool freezeDefaultStatus = false; + + event ResponseCode(int responseCode); + event CreatedToken(address tokenAddress); + event MintedToken(int64 newTotalSupply, int64[] serialNumbers); + event KycGranted(bool kycGranted); + + function createFungibleTokenPublic( + address treasury + ) public payable { + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](5); + keys[0] = getSingleKey(KeyType.ADMIN, KeyType.PAUSE, KeyValueType.INHERIT_ACCOUNT_KEY, bytes("")); + keys[1] = getSingleKey(KeyType.KYC, KeyValueType.INHERIT_ACCOUNT_KEY, bytes("")); + keys[2] = getSingleKey(KeyType.FREEZE, KeyValueType.INHERIT_ACCOUNT_KEY, bytes("")); + keys[3] = getSingleKey(KeyType.WIPE, KeyValueType.INHERIT_ACCOUNT_KEY, bytes("")); + keys[4] = getSingleKey(KeyType.SUPPLY, KeyValueType.INHERIT_ACCOUNT_KEY, bytes("")); + + IHederaTokenService.Expiry memory expiry = IHederaTokenService.Expiry( + 0, treasury, 8000000 + ); + + IHederaTokenService.HederaToken memory token = IHederaTokenService.HederaToken( + name, symbol, treasury, memo, true, maxSupply, freezeDefaultStatus, keys, expiry + ); + + (int responseCode, address tokenAddress) = + HederaTokenService.createFungibleToken(token, initialTotalSupply, decimals); + + if (responseCode != HederaResponseCodes.SUCCESS) { + revert (); + } + + emit CreatedToken(tokenAddress); + } + + + function createNonFungibleTokenPublic( + address treasury + ) public payable { + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](5); + keys[0] = getSingleKey(KeyType.ADMIN, KeyType.PAUSE, KeyValueType.INHERIT_ACCOUNT_KEY, bytes("")); + keys[1] = getSingleKey(KeyType.KYC, KeyValueType.INHERIT_ACCOUNT_KEY, bytes("")); + keys[2] = getSingleKey(KeyType.FREEZE, KeyValueType.INHERIT_ACCOUNT_KEY, bytes("")); + keys[3] = getSingleKey(KeyType.SUPPLY, KeyValueType.INHERIT_ACCOUNT_KEY, bytes("")); + keys[4] = getSingleKey(KeyType.WIPE, KeyValueType.INHERIT_ACCOUNT_KEY, bytes("")); + + IHederaTokenService.Expiry memory expiry = IHederaTokenService.Expiry( + 0, treasury, 8000000 + ); + + IHederaTokenService.HederaToken memory token = IHederaTokenService.HederaToken( + name, symbol, treasury, memo, true, maxSupply, freezeDefaultStatus, keys, expiry + ); + + (int responseCode, address tokenAddress) = + HederaTokenService.createNonFungibleToken(token); + + if (responseCode != HederaResponseCodes.SUCCESS) { + revert (); + } + + emit CreatedToken(tokenAddress); + } + + function mintTokenPublic(address token, int64 amount, bytes[] memory metadata) public + returns (int responseCode, int64 newTotalSupply, int64[] memory serialNumbers) { + // Services -> INVALID_TOKEN_ID + // MirrorNode -> OpcodeloggerDebugtraceTransaction -> UNKNOWN + (responseCode, newTotalSupply, serialNumbers) = HederaTokenService.mintToken(token, amount, metadata); + emit ResponseCode(responseCode); + + if (responseCode != HederaResponseCodes.SUCCESS) { + if (responseCode == HederaResponseCodes.INVALID_TOKEN_ID){ + revert(string(abi.encodePacked("Minting reveted with INVALID_TOKEN_ID"))); + } else if (responseCode == HederaResponseCodes.TOKEN_MAX_SUPPLY_REACHED){ + revert(string(abi.encodePacked("Minting ", Strings.toString((uint256(uint64(amount)))), " tokens reveted with TOKEN_MAX_SUPPLY_REACHED"))); + } + + revert (string(abi.encodePacked("Minting was reveted with responseCode: ", Strings.toString(uint256(responseCode))))); + } + emit MintedToken(newTotalSupply, serialNumbers); + } + + function associateTokenPublic(address account, address token) public returns (int responseCode) { + responseCode = HederaTokenService.associateToken(account, token); + emit ResponseCode(responseCode); + + if (responseCode != HederaResponseCodes.SUCCESS) { + if (responseCode == HederaResponseCodes.TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT){ + revert(string(abi.encodePacked("Association reveted with TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT"))); + } + revert ("Default associateTokenPublic() revert reason"); + } + } +} diff --git a/test/constants.js b/test/constants.js index 61f1c600f..a9a681d06 100644 --- a/test/constants.js +++ b/test/constants.js @@ -76,6 +76,7 @@ const Contract = { OZERC20Mock: 'OZERC20Mock', OZERC721Mock: 'OZERC721Mock', TokenCreateContract: 'TokenCreateContract', + TokenCreateTest: 'TokenCreateTest', DiamondCutFacet: 'DiamondCutFacet', Diamond: 'Diamond', DiamondInit: 'DiamondInit', diff --git a/test/solidity/opcode-logger/OpcodeLogger.js b/test/solidity/opcode-logger/OpcodeLogger.js index 4656b75b7..1c6d13467 100644 --- a/test/solidity/opcode-logger/OpcodeLogger.js +++ b/test/solidity/opcode-logger/OpcodeLogger.js @@ -1,8 +1,9 @@ const Constants = require('../../constants'); -const {expect} = require('chai'); +const { expect, assert } = require('chai'); const hre = require('hardhat'); const fs = require('fs'); const {ethers} = hre; +const { hexToASCII } = require('../../utils') const BESU_RESULTS_JSON_PATH = __dirname + '/opcodeLoggerBesuResults.json'; const IS_BESU_NETWORK = hre.network.name === 'besu_local'; @@ -17,7 +18,7 @@ describe('@OpcodeLogger Test Suite', async function () { randomAddress = (ethers.Wallet.createRandom()).address; const factoryOpcodeLogger = await ethers.getContractFactory(Constants.Contract.OpcodeLogger); - opcodeLogger = await factoryOpcodeLogger.deploy(); + opcodeLogger = await factoryOpcodeLogger.deploy({gasLimit: 5_000_000}); await opcodeLogger.waitForDeployment(); }); @@ -599,12 +600,18 @@ describe('@OpcodeLogger Test Suite', async function () { } describe('nested calls', async function () { - let errorsExternal; + let errorsExternal, nestedContractCreateTx; before(async () => { const factoryErrorsExternal = await ethers.getContractFactory(Constants.Contract.ErrorsExternal); errorsExternal = await factoryErrorsExternal.deploy(); await errorsExternal.waitForDeployment(); + + const contractCreatorFactory = await ethers.getContractFactory(Constants.Contract.ContractCreator); + const contractCreator = await contractCreatorFactory.deploy(); + await contractCreator.waitForDeployment(); + const contractByteCode = '0x608060405234801561001057600080fd5b5060405161001d9061005f565b604051809103906000f080158015610039573d6000803e3d6000fd5b50600080546001600160a01b0319166001600160a01b039290921691909117905561006c565b6101a68061058783390190565b61050c8061007b6000396000f3fe608060405234801561001057600080fd5b506004361061007d5760003560e01c80637c833d3a1161005b5780637c833d3a146100de578063b45694dc146100f1578063c6efc9be14610104578063fdf3a4091461011757600080fd5b80634a8fbaa7146100825780635a7fc2fb146100a857806364f0ac05146100cb575b600080fd5b61009561009036600461032e565b61012a565b6040519081526020015b60405180910390f35b6100bb6100b6366004610350565b61017a565b604051901515815260200161009f565b6100bb6100d9366004610372565b610192565b6100956100ec366004610372565b61021f565b6100956100ff366004610372565b61025f565b610095610112366004610372565b61029c565b61009561012536600461032e565b6102db565b6000606483106101555760405162461bcd60e51b815260040161014c9061038b565b60405180910390fd5b60005b8381610163816103cf565b9250101561017357808303610158575b9392505050565b6000811561018a57506001919050565b506000919050565b6000805460405163a9bf563360e01b81526004810184905273ffffffffffffffffffffffffffffffffffffffff9091169063a9bf563390602401600060405180830381865afa92505050801561020a57506040513d6000823e601f3d908101601f19168201604052610207919081019061040c565b60015b61021657506000919050565b50600192915050565b6000606482106102415760405162461bcd60e51b815260040161014c9061038b565b6000805b8381101561025857905060018101610245565b5092915050565b6000606482106102815760405162461bcd60e51b815260040161014c9061038b565b60005b8261028e826103cf565b915081106102845792915050565b6000606482106102be5760405162461bcd60e51b815260040161014c9061038b565b60005b806102cb816103cf565b9150508281106102c15792915050565b6000606483106102fd5760405162461bcd60e51b815260040161014c9061038b565b6000805b848110156103265780841061031e578161031a816103cf565b9250505b600101610301565b509392505050565b6000806040838503121561034157600080fd5b50508035926020909101359150565b60006020828403121561036257600080fd5b8135801515811461017357600080fd5b60006020828403121561038457600080fd5b5035919050565b60208082526024908201527f43616e6e6f742068617665206d6f7265207468616e2031303020697465726174604082015263696f6e7360e01b606082015260800190565b6000600182016103ef57634e487b7160e01b600052601160045260246000fd5b5060010190565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561041f57600080fd5b825167ffffffffffffffff8082111561043757600080fd5b818501915085601f83011261044b57600080fd5b81518181111561045d5761045d6103f6565b604051601f8201601f19908116603f01168101908382118183101715610485576104856103f6565b81604052828152888684870101111561049d57600080fd5b600093505b828410156104bf57848401860151818501870152928501926104a2565b60008684830101528096505050505050509291505056fea26469706673582212207617a513fa5800c056cf704f435835c83521a8d0a4158550f89e441d9238b93364736f6c63430008170033608060405234801561001057600080fd5b50610186806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063a9bf563314610030575b600080fd5b61004361003e3660046100e8565b610059565b6040516100509190610101565b60405180910390f35b6060816000036100af5760405162461bcd60e51b815260206004820152600e60248201527f72657175697265206661696c6564000000000000000000000000000000000000604482015260640160405180910390fd5b505060408051808201909152601281527f6d792066756e63207761732063616c6c65640000000000000000000000000000602082015290565b6000602082840312156100fa57600080fd5b5035919050565b60006020808352835180602085015260005b8181101561012f57858101830151858201604001528201610113565b506000604082860101526040601f19601f830116850101925050509291505056fea2646970667358221220cbab406ae7ac849914cffae16782be71f8529b82b40a5e08b8df57424ad392ec64736f6c63430008170033'; + nestedContractCreateTx = await contractCreator.createNewContract(contractByteCode); }); it('successful NESTED CALL to existing contract with disabledMemory, disabledStack, disabledStorage set to true', async function () { @@ -796,15 +803,69 @@ describe('@OpcodeLogger Test Suite', async function () { expect(sl.stack).to.equal(null); }); }); + + it('successful NESTED Create CALL Deploy a contract which successfully deploys another contract with disableMemory, DisableStack and disableStorage set to true', async function () { + const res = await executeDebugTraceTransaction(nestedContractCreateTx.hash, { + tracer: 'opcodeLogger', + disableStorage: true, + disableMemory: true, + disableStack: true + }); + + expect(res.failed).to.be.false; + expect(res.structLogs.length).to.be.greaterThan(0); + res.structLogs.map(function (sl) { + expect(sl.storage).to.equal(null); + expect(sl.memory).to.equal(null); + expect(sl.stack).to.equal(null); + }); + }); + + it('successful NESTED Create CALL Deploy a contract which successfully deploys another contract with disableMemory, DisableStack and disableStorage set to false', async function () { + const res = await executeDebugTraceTransaction(nestedContractCreateTx.hash, { + tracer: 'opcodeLogger', + disableStorage: false, + disableMemory: false, + disableStack: false + }); + + expect(res.failed).to.be.false; + expect(res.structLogs.length).to.be.greaterThan(0); + res.structLogs.map(function (sl) { + expect(sl.storage).to.not.equal(null); + expect(sl.memory).to.not.equal(null); + expect(sl.stack).to.not.equal(null); + }); + }); }); describe('precompiles', async function () { let precompiles; + let tokenCreateContract; + let tokenCreateTx; + let tokenCreateContractAddress; + let tokenAddress; before(async () => { const factoryPrecompiles = await ethers.getContractFactory(Constants.Contract.Precompiles); precompiles = await factoryPrecompiles.deploy(); await precompiles.waitForDeployment(); + + const tokenCreateFactory = await ethers.getContractFactory(Constants.Contract.TokenCreateTest); + tokenCreateContract = await tokenCreateFactory.deploy(Constants.GAS_LIMIT_1_000_000); + await tokenCreateContract.waitForDeployment(); + tokenCreateTx = await tokenCreateContract.createFungibleTokenPublic( + await tokenCreateContract.getAddress(), + { + value: BigInt('10000000000000000000'), + gasLimit: 1_000_000, + } + ); + const tokenAddressReceipt = await tokenCreateTx.wait(); + tokenAddress = { tokenAddress } = tokenAddressReceipt.logs.filter( + (e) => e.fragment.name === Constants.Events.CreatedToken + )[0].args.tokenAddress; + tokenCreateContractAddress = await tokenCreateContract.getAddress(); }); it('successful ETH precompile call to 0x2 with disabledMemory, disabledStack, disabledStorage set to true', async function () { @@ -886,5 +947,413 @@ describe('@OpcodeLogger Test Suite', async function () { expect(sl.stack).to.not.equal(null); }); }); + + it('successful ETH precompile call to 0x2 with disabledStorage set to false, disabledMemory, disabledStack set to true', async function () { + const tx = await precompiles.modExp(5644, 3, 2); + await tx.wait(); + + const res = await executeDebugTraceTransaction(tx.hash, { + tracer: 'opcodeLogger', + disableStorage: false, + disableMemory: true, + disableStack: true + }); + + expect(res.failed).to.be.false; + expect(res.structLogs.length).to.be.greaterThan(0); + res.structLogs.map(function (sl) { + expect(sl.storage).to.not.equal(null); + expect(sl.memory).to.equal(null); + expect(sl.stack).to.equal(null); + }); + }); + + it('failing ETH precompile call to 0x2 with disabledStorage set to false, disabledMemory, disabledStack set to true', async function () { + const tx = await precompiles.modExp(5644, 3, 2, { gasLimit: 21_496 }); + await expect(tx.wait()).to.be.rejectedWith(Error); + + const res = await executeDebugTraceTransaction(tx.hash, { + tracer: 'opcodeLogger', + disableStorage: false, + disableMemory: true, + disableStack: true + }); + + expect(res.failed).to.be.true; + expect(res.structLogs.length).to.be.greaterThan(0); + res.structLogs.map(function (sl) { + expect(sl.storage).to.not.equal(null); + expect(sl.memory).to.equal(null); + expect(sl.stack).to.equal(null); + }); + }); + + it('successful ETH precompile call to 0x2 with disabledMemory set to false, disabledStorage, disabledStack set to true', async function () { + const tx = await precompiles.modExp(5644, 3, 2); + await tx.wait(); + + const res = await executeDebugTraceTransaction(tx.hash, { + tracer: 'opcodeLogger', + disableStorage: true, + disableMemory: false, + disableStack: true + }); + + expect(res.failed).to.be.false; + expect(res.structLogs.length).to.be.greaterThan(0); + res.structLogs.map(function (sl) { + expect(sl.storage).to.equal(null); + expect(sl.memory).to.not.equal(null); + expect(sl.stack).to.equal(null); + }); + }); + + it('failing ETH precompile call to 0x2 with disabledMemory set to false, disabledStorage, disabledStack set to true', async function () { + const tx = await precompiles.modExp(5644, 3, 2, { gasLimit: 21_496 }); + await expect(tx.wait()).to.be.rejectedWith(Error); + + const res = await executeDebugTraceTransaction(tx.hash, { + tracer: 'opcodeLogger', + disableStorage: true, + disableMemory: false, + disableStack: true + }); + + expect(res.failed).to.be.true; + expect(res.structLogs.length).to.be.greaterThan(0); + res.structLogs.map(function (sl) { + expect(sl.storage).to.equal(null); + expect(sl.memory).to.not.equal(null); + expect(sl.stack).to.equal(null); + }); + }); + + it('successful ETH precompile call to 0x2 with disabledStack set to false, disabledStorage, disabledMemory set to true', async function () { + const tx = await precompiles.modExp(5644, 3, 2); + await tx.wait(); + + const res = await executeDebugTraceTransaction(tx.hash, { + tracer: 'opcodeLogger', + disableStorage: true, + disableMemory: true, + disableStack: false + }); + + expect(res.failed).to.be.false; + expect(res.structLogs.length).to.be.greaterThan(0); + res.structLogs.map(function (sl) { + expect(sl.storage).to.equal(null); + expect(sl.memory).to.equal(null); + expect(sl.stack).to.not.equal(null); + }); + }); + + it('failing ETH precompile call to 0x2 with disabledStack set to false, disabledStorage, disabledMemory set to true', async function () { + const tx = await precompiles.modExp(5644, 3, 2, { gasLimit: 21_496 }); + await expect(tx.wait()).to.be.rejectedWith(Error); + + const res = await executeDebugTraceTransaction(tx.hash, { + tracer: 'opcodeLogger', + disableStorage: true, + disableMemory: true, + disableStack: false + }); + + expect(res.failed).to.be.true; + expect(res.structLogs.length).to.be.greaterThan(0); + res.structLogs.map(function (sl) { + expect(sl.storage).to.equal(null); + expect(sl.memory).to.equal(null); + expect(sl.stack).to.not.equal(null); + }); + }); + + it('successful tokenCreate call with disabledStorage, disabledMemory, disabledStack set to true', async function () { + const res = await executeDebugTraceTransaction(tokenCreateTx.hash, { + tracer: 'opcodeLogger', + disableStorage: true, + disableMemory: true, + disableStack: true + }); + + expect(res.failed).to.be.false; + expect(res.structLogs.length).to.be.greaterThan(0); + res.structLogs.map(function (sl) { + expect(sl.storage).to.equal(null); + expect(sl.memory).to.equal(null); + expect(sl.stack).to.equal(null); + }); + }); + + it('successful tokenCreate call with disabledStorage, disabledMemory, disabledStack set to false', async function () { + const res = await executeDebugTraceTransaction(tokenCreateTx.hash, { + tracer: 'opcodeLogger', + disableStorage: false, + disableMemory: false, + disableStack: false + }); + + expect(res.failed).to.be.false; + expect(res.structLogs.length).to.be.greaterThan(0); + res.structLogs.map(function (sl) { + expect(sl.storage).to.not.equal(null); + expect(sl.memory).to.not.equal(null); + expect(sl.stack).to.not.equal(null); + }); + }); + + // disabled until https://github.com/hashgraph/hedera-mirror-node/issues/8843 is resolved + it.skip('should return INVALID_TOKEN_ID as debugTrace revert reason when minting a token with incorrect address', async function () { + const tx = await opcodeLogger.executeHtsMintTokenRevertingCalls( + tokenCreateContractAddress, + tokenAddress, + [-1], + [], + Constants.GAS_LIMIT_10_000_000 + ); + + const res = await executeDebugTraceTransaction(tx.hash, { + tracer: 'opcodeLogger', + disableStorage: true, + disableMemory: true, + disableStack: true + }); + + const revertOperations = res.structLogs.filter(function (opLog) { + return opLog.op === "REVERT" + }); + + expect(revertOperations.length).to.be.greaterThan(0, 'No "revert" operations were found in debugTrace transaction response'); + expect(revertOperations[0].reason).to.not.be.null + expect(hexToASCII(revertOperations[0].reason)).to.contain( + "Minting reveted with INVALID_TOKEN_ID", + `\nActual revert reason: '${hexToASCII(revertOperations[0].reason)}'\n did not match with expected: 'Minting reveted with INVALID_TOKEN_ID'\n`); + }); + + it('should return TOKEN_MAX_SUPPLY_REACHED as debugTrace revert reason when minting a token with max supply reached', async function () { + const mintTokenAmounts = [3, 300,]; + const tx = await opcodeLogger.executeHtsMintTokenRevertingCalls( + tokenCreateContractAddress, + tokenAddress, + mintTokenAmounts, + [], + Constants.GAS_LIMIT_10_000_000 + ); + + const res = await executeDebugTraceTransaction(tx.hash, { + tracer: 'opcodeLogger', + disableStorage: true, + disableMemory: true, + disableStack: true + }); + + const revertOperations = res.structLogs.filter(function (opLog) { + return opLog.op === "REVERT" + }); + + expect(revertOperations.length).to.be.greaterThan(0, 'No "revert" operations were found in debugTrace transaction response'); + + for (let i = 0; i < mintTokenAmounts.length; i++) { + expect(revertOperations[i].reason).to.not.be.null + expect(hexToASCII(revertOperations[i].reason)).to.contain(`Minting ${mintTokenAmounts[i]} tokens reveted with TOKEN_MAX_SUPPLY_REACHED`); + }; + }); + + // disabled until https://github.com/hashgraph/hedera-mirror-node/issues/8843 is resolved + it.skip('should return correct debugTrace revert reason for HTS calls with the same call depth', async function () { + /* + * DO NOT use '0' as the function will not revert. + * Using values less than '0' e.g. '-1' to cause the function to revert with message: 'INVALID_TOKEN_ID' + * Values greater than '0' will cause the function to revert with message: 'Minting {value} tokens reveted with TOKEN_MAX_SUPPLY_REACHED' + */ + const mintTokenAmounts = [1, 10, 100, -1]; + + const tx = await opcodeLogger.executeHtsMintTokenRevertingCalls( + tokenCreateContractAddress, + tokenAddress, + mintTokenAmounts, + [], + Constants.GAS_LIMIT_10_000_000 + ); + + const res = await executeDebugTraceTransaction(tx.hash, { + tracer: 'opcodeLogger', + disableStorage: true, + disableMemory: true, + disableStack: true + }); + + const revertOperations = res.structLogs.filter(function (opLog) { + return opLog.op === "REVERT" + }); + + expect(revertOperations.length).to.be.greaterThan(0, 'No "revert" operations were found in debugTrace transaction response'); + + for (let i = 0; i < mintTokenAmounts.length; i++) { + expect(revertOperations[i].reason).to.not.be.null + + let expectedMessage = mintTokenAmounts[i] < 0 + ? 'Minting reveted with INVALID_TOKEN_ID' + : `Minting ${mintTokenAmounts[i]} tokens reveted with TOKEN_MAX_SUPPLY_REACHED`; + + expect(hexToASCII(revertOperations[i].reason)).to.contain( + expectedMessage, + `\nActual revert reason: '${hexToASCII(revertOperations[i].reason)}'\n did not match with expected: '${expectedMessage}'\n` + ); + expect(revertOperations[i].depth).to.equal(1); + }; + }); + + it('should return correct debugTrace revert reason for HTS calls with different call depth', async function () { + const mintTokenAmounts = [5, 2, 7,]; + const tx = await opcodeLogger.nestEverySecondHtsMintTokenCall( + tokenCreateContractAddress, + tokenAddress, + mintTokenAmounts, + [], + Constants.GAS_LIMIT_10_000_000 + ); + + const res = await executeDebugTraceTransaction(tx.hash, { + tracer: 'opcodeLogger', + disableStorage: true, + disableMemory: true, + disableStack: true + }); + + expect(res.failed).to.be.false; + expect(res.structLogs.length).to.be.greaterThan(0); + res.structLogs.map(function (sl) { + expect(sl.storage).to.equal(null); + expect(sl.memory).to.equal(null); + expect(sl.stack).to.equal(null); + }); + + const revertOperations = res.structLogs.filter(function (opLog) { + return opLog.op === "REVERT" + }); + + expect(revertOperations.length).to.be.greaterThan(0, 'No "revert" operations were found in debugTrace transaction response'); + + for (let i = 0; i < mintTokenAmounts.length; i++) { + let expectedMessage = `Minting ${mintTokenAmounts[i]} tokens reveted with TOKEN_MAX_SUPPLY_REACHED`; + expect(hexToASCII(revertOperations[i].reason)).to.contain(expectedMessage); + expect(revertOperations[i].depth).to.equal(i % 2 === 0 ? 1 : 2); + }; + }); + + it('should not mix revert reasons between different HTS calls', async function () { + const mintTokenAmounts = [2, 11, 17]; + const tx = await opcodeLogger.executeHtsMintTokenRevertingCallsAndFailToAssociate( + tokenCreateContractAddress, + tokenAddress, + mintTokenAmounts, + [], + Constants.GAS_LIMIT_10_000_000 + ); + + const res = await executeDebugTraceTransaction(tx.hash, { + tracer: 'opcodeLogger', + disableStorage: true, + disableMemory: true, + disableStack: true + }); + + const revertOperations = res.structLogs.filter(function (opLog) { + return opLog.op === "REVERT" + }); + + expect(revertOperations.length).to.be.greaterThan(0, 'No "revert" operations were found in debugTrace transaction response'); + + for (let i = 0; i < mintTokenAmounts.length; i++) { + expect(revertOperations[i].reason).to.not.be.null + + let expectedMessage = mintTokenAmounts[i] < 0 + ? 'Minting reveted with INVALID_TOKEN_ID' + : `Minting ${mintTokenAmounts[i]} tokens reveted with TOKEN_MAX_SUPPLY_REACHED`; + + expect(hexToASCII(revertOperations[i].reason)).to.contain(expectedMessage); + expect(revertOperations[i].depth).to.equal(1); + }; + expect(hexToASCII(revertOperations[revertOperations.length - 1].reason)).to.contain('Association reveted with TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT'); + }); + + it('should not contain revert operation when GAS is depleted (insufficient)', async function () { + const tx = await tokenCreateContract.createNonFungibleTokenPublic( + tokenCreateContractAddress, + { gasLimit: 21432 } + ); + + const res = await executeDebugTraceTransaction(tx.hash, { + tracer: 'opcodeLogger', + disableStorage: true, + disableMemory: true, + disableStack: true + }); + + const revertOperations = res.structLogs.filter(function (opLog) { + return opLog.op === "REVERT" + }); + expect(revertOperations.length).to.equal(0); + }); + }); + + describe('negative', async function () { + it('should fail to debug a transaction with invalid hash', async function () { + const res = await executeDebugTraceTransaction('0x0fdfb3da2d40cd9ac8776ca02c17cb4aae634d2726f5aad049ab4ce5056b1a5c', { + tracer: 'opcodeLogger', + disableStorage: true, + disableMemory: true, + disableStack: true + }); + + expect(res.failed).to.be.true; + expect(res.structLogs).to.be.empty; + }); + + it('should fail with invalid parameter value type for disableMemory, disableStack or disableStorage', async function () { + const tx = await opcodeLogger.call(opcodeLogger.target, '0xdbdf7fce'); // calling resetCounter() + await tx.wait(); + try { + await executeDebugTraceTransaction(tx.hash, { + tracer: 'opcodeLogger', + disableStorage: 'true', + disableMemory: 1, + disableStack: 0 + }); + + } catch (error) { + expect(error.name).to.equal('ProviderError'); + expect(error._isProviderError).to.be.true; + expect(error._stack).to.contain('Invalid parameter 2: Invalid tracerConfig'); + + return; + } + + assert.fail('Executing debug trace transaction with invalid parameter value types did not result in error') + }); + + it('should fail when executing debug trace transaction with incorrect tracer parameter', async function () { + const tx = await opcodeLogger.call(opcodeLogger.target, '0xdbdf7fce'); // calling resetCounter() + await tx.wait(); + const incorrectTracer = 'opcodeLogger1'; + + try { + await executeDebugTraceTransaction(tx.hash, { + tracer: incorrectTracer, + disableStorage: true, + disableMemory: true, + disableStack: true + }); + } catch (error) { + expect(error.name).to.equal('ProviderError'); + expect(error._isProviderError).to.be.true; + expect(error._stack).to.contain(`Invalid parameter 1: Invalid tracer type, value: ${incorrectTracer}`); + + return; + } + + assert.fail('Executing debug trace transaction with incorrect tracer parameter did not result in error') + }); }); }); diff --git a/test/utils.js b/test/utils.js index ed639e36a..67a1d2cf4 100644 --- a/test/utils.js +++ b/test/utils.js @@ -30,6 +30,15 @@ class Utils { static to32ByteString(str) { return str.toString(16).replace('0x', '').padStart(64, '0'); }; + + static hexToASCII(str) { + const hex = str.toString(); + let ascii = ''; + for (let n = 0; n < hex.length; n += 2) { + ascii += String.fromCharCode(parseInt(hex.substring(n, n + 2), 16)); + } + return ascii; + }; } module.exports = Utils;