From 0eef39200c681a6062a8eb2fe956c01a0654a07e Mon Sep 17 00:00:00 2001 From: LeoSagan Date: Tue, 17 Sep 2024 14:49:23 -0400 Subject: [PATCH 1/4] Adds calldata decoding task --- packages/ethernaut-util/src/tasks/calldata.js | 41 ++ .../test/tasks/calldata.test.js | 365 ++++++++++++++++++ 2 files changed, 406 insertions(+) create mode 100644 packages/ethernaut-util/src/tasks/calldata.js create mode 100644 packages/ethernaut-util/test/tasks/calldata.test.js diff --git a/packages/ethernaut-util/src/tasks/calldata.js b/packages/ethernaut-util/src/tasks/calldata.js new file mode 100644 index 00000000..0c917a8b --- /dev/null +++ b/packages/ethernaut-util/src/tasks/calldata.js @@ -0,0 +1,41 @@ +const { ethers } = require('ethers') +const types = require('ethernaut-common/src/validation/types') +const output = require('ethernaut-common/src/ui/output') +const EthernautCliError = require('ethernaut-common/src/error/error') + +require('../scopes/util') + .task('calldata', 'Decodes calldata of a tx') + .addPositionalParam( + 'transactionId', + 'The transaction to lookup', + undefined, + types.string, + ) + .addPositionalParam( + 'abi', + 'The ABI of the contract to decode the calldata', + undefined, + types.array, + ) + .setAction(async ({ transactionId, abi }, hre) => { + try { + const tx = await hre.ethers.provider.getTransaction(transactionId) + + if (!tx) { + throw new EthernautCliError('ethernaut-network', 'No tx found.', false) + } + + const calldata = tx.data + const iface = new ethers.utils.Interface(abi) + const decodedData = iface.parseTransaction({ data: calldata }) + + const callData = { + functionName: decodedData.name, + args: decodedData.args, + } + + return output.resultBox(callData) + } catch (err) { + return output.errorBox(err) + } + }) diff --git a/packages/ethernaut-util/test/tasks/calldata.test.js b/packages/ethernaut-util/test/tasks/calldata.test.js new file mode 100644 index 00000000..d989d46f --- /dev/null +++ b/packages/ethernaut-util/test/tasks/calldata.test.js @@ -0,0 +1,365 @@ +const assert = require('assert') + +describe('calldata', function () { + let output + describe('when decoding 0x4054d7d608e735608ac6fe70609f9a9616215309e576e975414c4f73374b0b78', function () { + before('calldata', async function () { + hre.ethers.provider = new hre.ethers.JsonRpcProvider( + 'https://ethereum-rpc.publicnode.com', + ) + output = await hre.run( + { + scope: 'util', + task: 'calldata', + }, + { + transactionId: + '0x4054d7d608e735608ac6fe70609f9a9616215309e576e975414c4f73374b0b78', + }, + { + abi: [ + { + inputs: [ + { internalType: 'uint256', name: 'chainId_', type: 'uint256' }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'src', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'guy', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'wad', + type: 'uint256', + }, + ], + name: 'Approval', + type: 'event', + }, + { + anonymous: true, + inputs: [ + { + indexed: true, + internalType: 'bytes4', + name: 'sig', + type: 'bytes4', + }, + { + indexed: true, + internalType: 'address', + name: 'usr', + type: 'address', + }, + { + indexed: true, + internalType: 'bytes32', + name: 'arg1', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'bytes32', + name: 'arg2', + type: 'bytes32', + }, + { + indexed: false, + internalType: 'bytes', + name: 'data', + type: 'bytes', + }, + ], + name: 'LogNote', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'src', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'dst', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'wad', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + { + constant: true, + inputs: [], + name: 'DOMAIN_SEPARATOR', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'PERMIT_TYPEHASH', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [ + { internalType: 'address', name: '', type: 'address' }, + { internalType: 'address', name: '', type: 'address' }, + ], + name: 'allowance', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: false, + inputs: [ + { internalType: 'address', name: 'usr', type: 'address' }, + { internalType: 'uint256', name: 'wad', type: 'uint256' }, + ], + name: 'approve', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: true, + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: false, + inputs: [ + { internalType: 'address', name: 'usr', type: 'address' }, + { internalType: 'uint256', name: 'wad', type: 'uint256' }, + ], + name: 'burn', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'decimals', + outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: false, + inputs: [ + { internalType: 'address', name: 'guy', type: 'address' }, + ], + name: 'deny', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: false, + inputs: [ + { internalType: 'address', name: 'usr', type: 'address' }, + { internalType: 'uint256', name: 'wad', type: 'uint256' }, + ], + name: 'mint', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: false, + inputs: [ + { internalType: 'address', name: 'src', type: 'address' }, + { internalType: 'address', name: 'dst', type: 'address' }, + { internalType: 'uint256', name: 'wad', type: 'uint256' }, + ], + name: 'move', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'name', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'nonces', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: false, + inputs: [ + { internalType: 'address', name: 'holder', type: 'address' }, + { internalType: 'address', name: 'spender', type: 'address' }, + { internalType: 'uint256', name: 'nonce', type: 'uint256' }, + { internalType: 'uint256', name: 'expiry', type: 'uint256' }, + { internalType: 'bool', name: 'allowed', type: 'bool' }, + { internalType: 'uint8', name: 'v', type: 'uint8' }, + { internalType: 'bytes32', name: 'r', type: 'bytes32' }, + { internalType: 'bytes32', name: 's', type: 'bytes32' }, + ], + name: 'permit', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: false, + inputs: [ + { internalType: 'address', name: 'usr', type: 'address' }, + { internalType: 'uint256', name: 'wad', type: 'uint256' }, + ], + name: 'pull', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: false, + inputs: [ + { internalType: 'address', name: 'usr', type: 'address' }, + { internalType: 'uint256', name: 'wad', type: 'uint256' }, + ], + name: 'push', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: false, + inputs: [ + { internalType: 'address', name: 'guy', type: 'address' }, + ], + name: 'rely', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'symbol', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: false, + inputs: [ + { internalType: 'address', name: 'dst', type: 'address' }, + { internalType: 'uint256', name: 'wad', type: 'uint256' }, + ], + name: 'transfer', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: false, + inputs: [ + { internalType: 'address', name: 'src', type: 'address' }, + { internalType: 'address', name: 'dst', type: 'address' }, + { internalType: 'uint256', name: 'wad', type: 'uint256' }, + ], + name: 'transferFrom', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'version', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'wards', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function', + }, + ], + }, + ) + }) + + it('finds the functionName', async function () { + assert.ok(output.includes('functionName')) + }) + }) +}) From 7e46547310261e9cdaf17f8f4b1fc8d72f4d3379 Mon Sep 17 00:00:00 2001 From: LeoSagan Date: Tue, 17 Sep 2024 15:07:06 -0400 Subject: [PATCH 2/4] Adds calldata decoding task --- package-lock.json | 3 +++ package.json | 3 +++ packages/ethernaut-util/README.md | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 6f8c80ce..c4915e94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,9 @@ "workspaces": [ "packages/*" ], + "dependencies": { + "ethernaut-util": "^1.1.11" + }, "devDependencies": { "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", diff --git a/package.json b/package.json index 81f9aedb..31c278af 100644 --- a/package.json +++ b/package.json @@ -23,5 +23,8 @@ "lerna": "^8.1.2", "lint-staged": "^15.2.2", "prettier": "^3.2.5" + }, + "dependencies": { + "ethernaut-util": "^1.1.11" } } diff --git a/packages/ethernaut-util/README.md b/packages/ethernaut-util/README.md index 42cc35bd..2411c1cf 100644 --- a/packages/ethernaut-util/README.md +++ b/packages/ethernaut-util/README.md @@ -41,7 +41,7 @@ This plugin adds the tasks listed below. - unit Converts between different units of Ether - gas Fetch gas info on the current network - chain Finds a network name from a chain ID, or vice versa -- hex Converts integers to hex +- calldata Decodes calldata from a transaction id ## Environment extensions From 9f65eb30b7d5ab62f8e5c042bb5e05af29ed9978 Mon Sep 17 00:00:00 2001 From: LeoSagan Date: Tue, 24 Sep 2024 14:54:16 -0400 Subject: [PATCH 3/4] Moves calldata to interact --- package.json | 3 - packages/ethernaut-interact/README.md | 1 + packages/ethernaut-interact/src/abis/dai.json | 333 ++++++++++++++++ .../ethernaut-interact/src/tasks/calldata.js | 71 ++++ .../test/tasks/calldata.test.js | 31 ++ packages/ethernaut-util/README.md | 1 - packages/ethernaut-util/src/tasks/calldata.js | 41 -- .../test/tasks/calldata.test.js | 365 ------------------ 8 files changed, 436 insertions(+), 410 deletions(-) create mode 100644 packages/ethernaut-interact/src/abis/dai.json create mode 100644 packages/ethernaut-interact/src/tasks/calldata.js create mode 100644 packages/ethernaut-interact/test/tasks/calldata.test.js delete mode 100644 packages/ethernaut-util/src/tasks/calldata.js delete mode 100644 packages/ethernaut-util/test/tasks/calldata.test.js diff --git a/package.json b/package.json index 31c278af..81f9aedb 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,5 @@ "lerna": "^8.1.2", "lint-staged": "^15.2.2", "prettier": "^3.2.5" - }, - "dependencies": { - "ethernaut-util": "^1.1.11" } } diff --git a/packages/ethernaut-interact/README.md b/packages/ethernaut-interact/README.md index ff139b55..153d2709 100644 --- a/packages/ethernaut-interact/README.md +++ b/packages/ethernaut-interact/README.md @@ -41,6 +41,7 @@ This plugin adds the following tasks: - logs Finds logs emitted by a contract - send Sends ether to an address - token Interacts with any ERC20 token +- calldata Decodes calldata from a transaction id ## Environment extensions diff --git a/packages/ethernaut-interact/src/abis/dai.json b/packages/ethernaut-interact/src/abis/dai.json new file mode 100644 index 00000000..025f28ec --- /dev/null +++ b/packages/ethernaut-interact/src/abis/dai.json @@ -0,0 +1,333 @@ +[ + { + "inputs": [ + { "internalType": "uint256", "name": "chainId_", "type": "uint256" } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "guy", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": true, + "inputs": [ + { + "indexed": true, + "internalType": "bytes4", + "name": "sig", + "type": "bytes4" + }, + { + "indexed": true, + "internalType": "address", + "name": "usr", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "arg1", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "arg2", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "LogNote", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "constant": true, + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "PERMIT_TYPEHASH", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "address", "name": "", "type": "address" } + ], + "name": "allowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "usr", "type": "address" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "approve", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "usr", "type": "address" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "burn", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "address", "name": "guy", "type": "address" }], + "name": "deny", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "usr", "type": "address" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "mint", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "src", "type": "address" }, + { "internalType": "address", "name": "dst", "type": "address" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "move", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "nonces", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "holder", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "nonce", "type": "uint256" }, + { "internalType": "uint256", "name": "expiry", "type": "uint256" }, + { "internalType": "bool", "name": "allowed", "type": "bool" }, + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "name": "permit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "usr", "type": "address" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "pull", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "usr", "type": "address" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "push", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "address", "name": "guy", "type": "address" }], + "name": "rely", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "dst", "type": "address" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "src", "type": "address" }, + { "internalType": "address", "name": "dst", "type": "address" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "version", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "wards", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + } +] diff --git a/packages/ethernaut-interact/src/tasks/calldata.js b/packages/ethernaut-interact/src/tasks/calldata.js new file mode 100644 index 00000000..0436d0af --- /dev/null +++ b/packages/ethernaut-interact/src/tasks/calldata.js @@ -0,0 +1,71 @@ +const { ethers } = require('ethers') +const types = require('ethernaut-common/src/validation/types') +const output = require('ethernaut-common/src/ui/output') +const EthernautCliError = require('ethernaut-common/src/error/error') +const abi = require('../../../ethernaut-interact-ui/src/suggest/abi') + +require('../scopes/interact') + .task( + 'calldata', + 'Decodes calldata of a provided one, or searches for it from a given tx id', + ) + .addOptionalPositionalParam( + 'calldata', + 'The calldata to decode', + undefined, + types.string, + ) + .addOptionalPositionalParam( + 'transactionId', + 'The transaction to lookup', + undefined, + types.bytes32, + ) + .addPositionalParam( + 'abi', + 'The ABI of the contract to decode the calldata', + undefined, + types.array, + ) + .addPositionalParam( + 'filter', + 'Some text to filter the list of known ABIs. Leave empty to list all. Results will be ordered by similarity to the filter text, so the first result will be the best match.', + undefined, + types.string, + ) + .setAction(async ({ calldata, transactionId }, hre) => { + try { + const iface = new ethers.utils.Interface(abi) + + if (calldata) { + const decodedData = iface.parseTransaction({ data: calldata }) + return output.resultBox({ + functionName: decodedData.name, + args: decodedData.args, + }) + } + + if (transactionId) { + const tx = await hre.ethers.provider.getTransaction(transactionId) + if (!tx) + throw new EthernautCliError( + 'ethernaut-network', + 'No tx found.', + false, + ) + const decodedData = iface.parseTransaction({ data: tx.data }) + return output.resultBox({ + functionName: decodedData.name, + args: decodedData.args, + }) + } + + throw new EthernautCliError( + 'ethernaut-interact', + 'Either transactionId or calldata must be provided', + false, + ) + } catch (err) { + return output.errorBox(err) + } + }) diff --git a/packages/ethernaut-interact/test/tasks/calldata.test.js b/packages/ethernaut-interact/test/tasks/calldata.test.js new file mode 100644 index 00000000..412a99a9 --- /dev/null +++ b/packages/ethernaut-interact/test/tasks/calldata.test.js @@ -0,0 +1,31 @@ +const assert = require('assert') + +const DAI_ABI = require('ethernaut-util/src/abis/dai.json') + +describe('calldata', function () { + let output + describe('when decoding 0x4054d7d608e735608ac6fe70609f9a9616215309e576e975414c4f73374b0b78', function () { + before('calldata', async function () { + hre.ethers.provider = new hre.ethers.JsonRpcProvider( + 'https://ethereum-rpc.publicnode.com', + ) + output = await hre.run( + { + scope: 'interact', + task: 'calldata', + }, + { + transactionId: + '0x4054d7d608e735608ac6fe70609f9a9616215309e576e975414c4f73374b0b78', + }, + { + abi: DAI_ABI, + }, + ) + }) + + it('finds the functionName', async function () { + assert.ok(output.includes('functionName')) + }) + }) +}) diff --git a/packages/ethernaut-util/README.md b/packages/ethernaut-util/README.md index 2411c1cf..38b43a40 100644 --- a/packages/ethernaut-util/README.md +++ b/packages/ethernaut-util/README.md @@ -41,7 +41,6 @@ This plugin adds the tasks listed below. - unit Converts between different units of Ether - gas Fetch gas info on the current network - chain Finds a network name from a chain ID, or vice versa -- calldata Decodes calldata from a transaction id ## Environment extensions diff --git a/packages/ethernaut-util/src/tasks/calldata.js b/packages/ethernaut-util/src/tasks/calldata.js deleted file mode 100644 index 0c917a8b..00000000 --- a/packages/ethernaut-util/src/tasks/calldata.js +++ /dev/null @@ -1,41 +0,0 @@ -const { ethers } = require('ethers') -const types = require('ethernaut-common/src/validation/types') -const output = require('ethernaut-common/src/ui/output') -const EthernautCliError = require('ethernaut-common/src/error/error') - -require('../scopes/util') - .task('calldata', 'Decodes calldata of a tx') - .addPositionalParam( - 'transactionId', - 'The transaction to lookup', - undefined, - types.string, - ) - .addPositionalParam( - 'abi', - 'The ABI of the contract to decode the calldata', - undefined, - types.array, - ) - .setAction(async ({ transactionId, abi }, hre) => { - try { - const tx = await hre.ethers.provider.getTransaction(transactionId) - - if (!tx) { - throw new EthernautCliError('ethernaut-network', 'No tx found.', false) - } - - const calldata = tx.data - const iface = new ethers.utils.Interface(abi) - const decodedData = iface.parseTransaction({ data: calldata }) - - const callData = { - functionName: decodedData.name, - args: decodedData.args, - } - - return output.resultBox(callData) - } catch (err) { - return output.errorBox(err) - } - }) diff --git a/packages/ethernaut-util/test/tasks/calldata.test.js b/packages/ethernaut-util/test/tasks/calldata.test.js deleted file mode 100644 index d989d46f..00000000 --- a/packages/ethernaut-util/test/tasks/calldata.test.js +++ /dev/null @@ -1,365 +0,0 @@ -const assert = require('assert') - -describe('calldata', function () { - let output - describe('when decoding 0x4054d7d608e735608ac6fe70609f9a9616215309e576e975414c4f73374b0b78', function () { - before('calldata', async function () { - hre.ethers.provider = new hre.ethers.JsonRpcProvider( - 'https://ethereum-rpc.publicnode.com', - ) - output = await hre.run( - { - scope: 'util', - task: 'calldata', - }, - { - transactionId: - '0x4054d7d608e735608ac6fe70609f9a9616215309e576e975414c4f73374b0b78', - }, - { - abi: [ - { - inputs: [ - { internalType: 'uint256', name: 'chainId_', type: 'uint256' }, - ], - payable: false, - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'src', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'guy', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'wad', - type: 'uint256', - }, - ], - name: 'Approval', - type: 'event', - }, - { - anonymous: true, - inputs: [ - { - indexed: true, - internalType: 'bytes4', - name: 'sig', - type: 'bytes4', - }, - { - indexed: true, - internalType: 'address', - name: 'usr', - type: 'address', - }, - { - indexed: true, - internalType: 'bytes32', - name: 'arg1', - type: 'bytes32', - }, - { - indexed: true, - internalType: 'bytes32', - name: 'arg2', - type: 'bytes32', - }, - { - indexed: false, - internalType: 'bytes', - name: 'data', - type: 'bytes', - }, - ], - name: 'LogNote', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'src', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'dst', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'wad', - type: 'uint256', - }, - ], - name: 'Transfer', - type: 'event', - }, - { - constant: true, - inputs: [], - name: 'DOMAIN_SEPARATOR', - outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'PERMIT_TYPEHASH', - outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [ - { internalType: 'address', name: '', type: 'address' }, - { internalType: 'address', name: '', type: 'address' }, - ], - name: 'allowance', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: false, - inputs: [ - { internalType: 'address', name: 'usr', type: 'address' }, - { internalType: 'uint256', name: 'wad', type: 'uint256' }, - ], - name: 'approve', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: true, - inputs: [{ internalType: 'address', name: '', type: 'address' }], - name: 'balanceOf', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: false, - inputs: [ - { internalType: 'address', name: 'usr', type: 'address' }, - { internalType: 'uint256', name: 'wad', type: 'uint256' }, - ], - name: 'burn', - outputs: [], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'decimals', - outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: false, - inputs: [ - { internalType: 'address', name: 'guy', type: 'address' }, - ], - name: 'deny', - outputs: [], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: false, - inputs: [ - { internalType: 'address', name: 'usr', type: 'address' }, - { internalType: 'uint256', name: 'wad', type: 'uint256' }, - ], - name: 'mint', - outputs: [], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: false, - inputs: [ - { internalType: 'address', name: 'src', type: 'address' }, - { internalType: 'address', name: 'dst', type: 'address' }, - { internalType: 'uint256', name: 'wad', type: 'uint256' }, - ], - name: 'move', - outputs: [], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'name', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [{ internalType: 'address', name: '', type: 'address' }], - name: 'nonces', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: false, - inputs: [ - { internalType: 'address', name: 'holder', type: 'address' }, - { internalType: 'address', name: 'spender', type: 'address' }, - { internalType: 'uint256', name: 'nonce', type: 'uint256' }, - { internalType: 'uint256', name: 'expiry', type: 'uint256' }, - { internalType: 'bool', name: 'allowed', type: 'bool' }, - { internalType: 'uint8', name: 'v', type: 'uint8' }, - { internalType: 'bytes32', name: 'r', type: 'bytes32' }, - { internalType: 'bytes32', name: 's', type: 'bytes32' }, - ], - name: 'permit', - outputs: [], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: false, - inputs: [ - { internalType: 'address', name: 'usr', type: 'address' }, - { internalType: 'uint256', name: 'wad', type: 'uint256' }, - ], - name: 'pull', - outputs: [], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: false, - inputs: [ - { internalType: 'address', name: 'usr', type: 'address' }, - { internalType: 'uint256', name: 'wad', type: 'uint256' }, - ], - name: 'push', - outputs: [], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: false, - inputs: [ - { internalType: 'address', name: 'guy', type: 'address' }, - ], - name: 'rely', - outputs: [], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'symbol', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'totalSupply', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: false, - inputs: [ - { internalType: 'address', name: 'dst', type: 'address' }, - { internalType: 'uint256', name: 'wad', type: 'uint256' }, - ], - name: 'transfer', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: false, - inputs: [ - { internalType: 'address', name: 'src', type: 'address' }, - { internalType: 'address', name: 'dst', type: 'address' }, - { internalType: 'uint256', name: 'wad', type: 'uint256' }, - ], - name: 'transferFrom', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'version', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [{ internalType: 'address', name: '', type: 'address' }], - name: 'wards', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - payable: false, - stateMutability: 'view', - type: 'function', - }, - ], - }, - ) - }) - - it('finds the functionName', async function () { - assert.ok(output.includes('functionName')) - }) - }) -}) From 91f5e90f5d3f17b3debd4a12bf2ae54e2fff4845 Mon Sep 17 00:00:00 2001 From: LeoSagan Date: Tue, 1 Oct 2024 16:46:55 -0400 Subject: [PATCH 4/4] Refactors calldata into get tx info --- packages/ethernaut-interact/README.md | 2 +- .../ethernaut-interact/src/tasks/calldata.js | 71 ------------------- packages/ethernaut-interact/src/tasks/tx.js | 42 +++++++++++ .../basic-project}/abis/dai.json | 0 .../tasks/{calldata.test.js => tx.test.js} | 12 ++-- packages/ethernaut-util/README.md | 1 + 6 files changed, 48 insertions(+), 80 deletions(-) delete mode 100644 packages/ethernaut-interact/src/tasks/calldata.js create mode 100644 packages/ethernaut-interact/src/tasks/tx.js rename packages/ethernaut-interact/{src => test/fixture-projects/basic-project}/abis/dai.json (100%) rename packages/ethernaut-interact/test/tasks/{calldata.test.js => tx.test.js} (74%) diff --git a/packages/ethernaut-interact/README.md b/packages/ethernaut-interact/README.md index 153d2709..e61753c6 100644 --- a/packages/ethernaut-interact/README.md +++ b/packages/ethernaut-interact/README.md @@ -41,7 +41,7 @@ This plugin adds the following tasks: - logs Finds logs emitted by a contract - send Sends ether to an address - token Interacts with any ERC20 token -- calldata Decodes calldata from a transaction id +- tx Gives information about a mined transaction ## Environment extensions diff --git a/packages/ethernaut-interact/src/tasks/calldata.js b/packages/ethernaut-interact/src/tasks/calldata.js deleted file mode 100644 index 0436d0af..00000000 --- a/packages/ethernaut-interact/src/tasks/calldata.js +++ /dev/null @@ -1,71 +0,0 @@ -const { ethers } = require('ethers') -const types = require('ethernaut-common/src/validation/types') -const output = require('ethernaut-common/src/ui/output') -const EthernautCliError = require('ethernaut-common/src/error/error') -const abi = require('../../../ethernaut-interact-ui/src/suggest/abi') - -require('../scopes/interact') - .task( - 'calldata', - 'Decodes calldata of a provided one, or searches for it from a given tx id', - ) - .addOptionalPositionalParam( - 'calldata', - 'The calldata to decode', - undefined, - types.string, - ) - .addOptionalPositionalParam( - 'transactionId', - 'The transaction to lookup', - undefined, - types.bytes32, - ) - .addPositionalParam( - 'abi', - 'The ABI of the contract to decode the calldata', - undefined, - types.array, - ) - .addPositionalParam( - 'filter', - 'Some text to filter the list of known ABIs. Leave empty to list all. Results will be ordered by similarity to the filter text, so the first result will be the best match.', - undefined, - types.string, - ) - .setAction(async ({ calldata, transactionId }, hre) => { - try { - const iface = new ethers.utils.Interface(abi) - - if (calldata) { - const decodedData = iface.parseTransaction({ data: calldata }) - return output.resultBox({ - functionName: decodedData.name, - args: decodedData.args, - }) - } - - if (transactionId) { - const tx = await hre.ethers.provider.getTransaction(transactionId) - if (!tx) - throw new EthernautCliError( - 'ethernaut-network', - 'No tx found.', - false, - ) - const decodedData = iface.parseTransaction({ data: tx.data }) - return output.resultBox({ - functionName: decodedData.name, - args: decodedData.args, - }) - } - - throw new EthernautCliError( - 'ethernaut-interact', - 'Either transactionId or calldata must be provided', - false, - ) - } catch (err) { - return output.errorBox(err) - } - }) diff --git a/packages/ethernaut-interact/src/tasks/tx.js b/packages/ethernaut-interact/src/tasks/tx.js new file mode 100644 index 00000000..93610679 --- /dev/null +++ b/packages/ethernaut-interact/src/tasks/tx.js @@ -0,0 +1,42 @@ +const types = require('ethernaut-common/src/validation/types') +const output = require('ethernaut-common/src/ui/output') +const EthernautCliError = require('ethernaut-common/src/error/error') +const loadAbi = require('../internal/load-abi') + +require('../scopes/interact') + .task('tx', 'Gives information about a mined transaction') + .addPositionalParam( + 'transactionId', + 'The transaction to lookup', + undefined, + types.bytes32, + ) + .addPositionalParam( + 'abi', + 'The ABI path of the file to decode the tx', + undefined, + types.string, + ) + .setAction(async ({ transactionId, abi }, hre) => { + try { + const _abi = loadAbi(abi) + const iface = new hre.ethers.Interface(_abi) + const tx = await hre.ethers.provider.getTransaction(transactionId) + if (!tx) + throw new EthernautCliError('ethernaut-interact', 'No tx found.', false) + const decodedData = iface.parseTransaction({ data: tx.data }) + + // console.log(decodedData) + + const str = ` + functionName: + ${decodedData.name}, + args: + (${decodedData.args.join(', ')}) + ` + + return output.resultBox(str) + } catch (err) { + return output.errorBox(err) + } + }) diff --git a/packages/ethernaut-interact/src/abis/dai.json b/packages/ethernaut-interact/test/fixture-projects/basic-project/abis/dai.json similarity index 100% rename from packages/ethernaut-interact/src/abis/dai.json rename to packages/ethernaut-interact/test/fixture-projects/basic-project/abis/dai.json diff --git a/packages/ethernaut-interact/test/tasks/calldata.test.js b/packages/ethernaut-interact/test/tasks/tx.test.js similarity index 74% rename from packages/ethernaut-interact/test/tasks/calldata.test.js rename to packages/ethernaut-interact/test/tasks/tx.test.js index 412a99a9..05d0829f 100644 --- a/packages/ethernaut-interact/test/tasks/calldata.test.js +++ b/packages/ethernaut-interact/test/tasks/tx.test.js @@ -1,25 +1,21 @@ const assert = require('assert') -const DAI_ABI = require('ethernaut-util/src/abis/dai.json') - -describe('calldata', function () { +describe('tx', function () { let output describe('when decoding 0x4054d7d608e735608ac6fe70609f9a9616215309e576e975414c4f73374b0b78', function () { - before('calldata', async function () { + before('tx', async function () { hre.ethers.provider = new hre.ethers.JsonRpcProvider( 'https://ethereum-rpc.publicnode.com', ) output = await hre.run( { scope: 'interact', - task: 'calldata', + task: 'tx', }, { transactionId: '0x4054d7d608e735608ac6fe70609f9a9616215309e576e975414c4f73374b0b78', - }, - { - abi: DAI_ABI, + abi: './abis/dai.json', }, ) }) diff --git a/packages/ethernaut-util/README.md b/packages/ethernaut-util/README.md index 38b43a40..42cc35bd 100644 --- a/packages/ethernaut-util/README.md +++ b/packages/ethernaut-util/README.md @@ -41,6 +41,7 @@ This plugin adds the tasks listed below. - unit Converts between different units of Ether - gas Fetch gas info on the current network - chain Finds a network name from a chain ID, or vice versa +- hex Converts integers to hex ## Environment extensions