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')) - }) - }) -})