From 0185dfe3ee4c6b70598294f166dd706d56422e1b Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Tue, 18 May 2021 13:57:10 +1000 Subject: [PATCH] feat: Allow contract code and balance overrides --- src/chains/ethereum/ethereum/src/api.ts | 16 ++- .../ethereum/ethereum/src/blockchain.ts | 29 +++- .../tests/api/eth/contracts/Inspector.sol | 33 +++++ .../ethereum/tests/api/eth/ethCall.test.ts | 130 ++++++++++++++++++ 4 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 src/chains/ethereum/ethereum/tests/api/eth/contracts/Inspector.sol create mode 100644 src/chains/ethereum/ethereum/tests/api/eth/ethCall.test.ts diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 4c682e0359..81391e2248 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -29,7 +29,10 @@ import { import { TypedData as NotTypedData, signTypedData_v4 } from "eth-sig-util"; import { EthereumInternalOptions, Hardfork } from "@ganache/ethereum-options"; import { types, Data, Quantity, PromiEvent, utils } from "@ganache/utils"; -import Blockchain, { TransactionTraceOptions } from "./blockchain"; +import Blockchain, { + SimulationOverrides, + TransactionTraceOptions +} from "./blockchain"; import Wallet from "./wallet"; import { decode as rlpDecode } from "rlp"; import { $INLINE_JSON } from "ts-transformer-inline-file"; @@ -1787,10 +1790,11 @@ export default class EthereumApi implements types.Api { * * @returns the return value of executed contract. */ - @assertArgLength(1, 2) + @assertArgLength(1, 3) async eth_call( transaction: any, - blockNumber: string | Buffer | Tag = Tag.LATEST + blockNumber: string | Buffer | Tag = Tag.LATEST, + overrides: SimulationOverrides = {} ) { const blockchain = this.#blockchain; const blocks = blockchain.blocks; @@ -1846,7 +1850,11 @@ export default class EthereumApi implements types.Api { block }; - return blockchain.simulateTransaction(simulatedTransaction, parentBlock); + return blockchain.simulateTransaction( + simulatedTransaction, + parentBlock, + overrides + ); } //#endregion diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index b70253297f..22819b2cd1 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -132,6 +132,10 @@ export type BlockchainOptions = { logger: Logger; }; +export type SimulationOverrides = { + [address: string]: Partial<{ code: string; nonce: string; balance: string }>; +}; + /** * Sets the provided VM state manager's state root *without* first * checking for checkpoints or flushing the existing cache. @@ -545,6 +549,26 @@ export default class Blockchain extends Emittery.Typed< }); }; + applySimulationOverrides = async (vm: VM, overrides: SimulationOverrides) => { + for (const [address, override] of Object.entries(overrides)) { + const addressBuffer = Address.from(address).toBuffer(); + if (override.code) { + await vm.pStateManager.putContractCode( + addressBuffer, + Data.from(override.code).toBuffer() + ); + } + const account = await vm.pStateManager.getAccount(addressBuffer); + if (override.nonce) { + account.nonce = Quantity.from(override.nonce).toBuffer(); + } + if (override.balance) { + account.balance = Quantity.from(override.balance).toBuffer(); + } + await vm.pStateManager.putAccount(addressBuffer, account); + } + }; + getFromTrie = (trie: SecureTrie, address: Buffer): Promise => new Promise((resolve, reject) => { trie.get(address, (err, data) => { @@ -816,7 +840,8 @@ export default class Blockchain extends Emittery.Typed< public async simulateTransaction( transaction: SimulationTransaction, - parentBlock: Block + parentBlock: Block, + overrides: SimulationOverrides ) { let result: EVMResult; const options = this.#options; @@ -840,6 +865,8 @@ export default class Blockchain extends Emittery.Typed< this.vm.allowUnlimitedContractSize ); + await this.applySimulationOverrides(vm, overrides); + result = await vm.runCall({ caller: transaction.from.toBuffer(), data: transaction.data && transaction.data.toBuffer(), diff --git a/src/chains/ethereum/ethereum/tests/api/eth/contracts/Inspector.sol b/src/chains/ethereum/ethereum/tests/api/eth/contracts/Inspector.sol new file mode 100644 index 0000000000..51d664243d --- /dev/null +++ b/src/chains/ethereum/ethereum/tests/api/eth/contracts/Inspector.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.4; +pragma experimental ABIEncoderV2; + +contract Inspector { + function getBalance(address addr) + public + view + returns (uint256) + { + return addr.balance; + } + + function getCode(address addr) + public + view + returns (bytes memory code) + { + assembly { + // retrieve the size of the code, this needs assembly + let size := extcodesize(addr) + // allocate output byte array - this could also be done without assembly + // by using o_code = new bytes(size) + code := mload(0x40) + // new "memory end" including padding + mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f)))) + // store length in memory + mstore(code, size) + // actually retrieve the code, this needs assembly + extcodecopy(addr, add(code, 0x20), 0, size) + } + } +} diff --git a/src/chains/ethereum/ethereum/tests/api/eth/ethCall.test.ts b/src/chains/ethereum/ethereum/tests/api/eth/ethCall.test.ts new file mode 100644 index 0000000000..00759c9991 --- /dev/null +++ b/src/chains/ethereum/ethereum/tests/api/eth/ethCall.test.ts @@ -0,0 +1,130 @@ +import assert from "assert"; +import getProvider from "../../helpers/getProvider"; +import compile from "../../helpers/compile"; +import { join } from "path"; +import EthereumProvider from "../../../src/provider"; +import { simpleEncode, rawEncode } from "ethereumjs-abi"; + +describe("api", () => { + describe("eth", () => { + describe("sendTransaction", () => { + describe("options", () => { + const testAddress = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const contractDir = join(__dirname, "contracts"); + async function deployContract( + provider: EthereumProvider, + accounts: string[] + ) { + const contract = compile(join(contractDir, "Inspector.sol")); + + const from = accounts[0]; + + await provider.send("eth_subscribe", ["newHeads"]); + + const transactionHash = await provider.send("eth_sendTransaction", [ + { + from, + data: contract.code, + gas: 3141592 + } + ]); + + await provider.once("message"); + + const receipt = await provider.send("eth_getTransactionReceipt", [ + transactionHash + ]); + assert.strictEqual(receipt.blockNumber, "0x1"); + + const contractAddress = receipt.contractAddress; + return { + contract, + contractAddress + }; + } + + it("allows override of account code", async () => { + const provider = await getProvider({ + chain: { vmErrorsOnRPCResponse: true } + }); + const accounts = await provider.send("eth_accounts"); + const { contractAddress } = await deployContract(provider, accounts); + + const data = `0x${simpleEncode( + "getCode(address)", + testAddress + ).toString("hex")}`; + + const result = await provider.send("eth_call", [ + { + from: accounts[0], + to: contractAddress, + data + }, + "latest", + { [testAddress]: { code: "0x123456" } } + ]); + + assert.strictEqual( + result, + "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000031234560000000000000000000000000000000000000000000000000000000000" + ); + }); + + it("allows override of account balance", async () => { + const provider = await getProvider({ + chain: { vmErrorsOnRPCResponse: true } + }); + const accounts = await provider.send("eth_accounts"); + const { contractAddress } = await deployContract(provider, accounts); + + const data = `0x${simpleEncode( + "getBalance(address)", + testAddress + ).toString("hex")}`; + + const result = await provider.send("eth_call", [ + { + from: accounts[0], + to: contractAddress, + data + }, + "latest", + { [testAddress]: { balance: "0x1e240" } } + ]); + + assert.strictEqual( + result, + `0x${rawEncode(["uint256"], [123456]).toString("hex")}` + ); + }); + + it("does not persist overrides", async () => { + const provider = await getProvider({ + chain: { vmErrorsOnRPCResponse: true } + }); + const accounts = await provider.send("eth_accounts"); + const { contractAddress } = await deployContract(provider, accounts); + + const data = `0x${simpleEncode( + "getCode(address)", + testAddress + ).toString("hex")}`; + + const result = await provider.send("eth_call", [ + { + from: accounts[0], + to: contractAddress, + data + }, + "latest" + ]); + + const rawEmptyBytesEncoded = + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; + assert.strictEqual(result, rawEmptyBytesEncoded); + }); + }); + }); + }); +});