Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

feat: Allow contract code and balance overrides #905

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions src/chains/ethereum/ethereum/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1846,7 +1850,11 @@ export default class EthereumApi implements types.Api {
block
};

return blockchain.simulateTransaction(simulatedTransaction, parentBlock);
return blockchain.simulateTransaction(
simulatedTransaction,
parentBlock,
overrides
);
}
//#endregion

Expand Down
29 changes: 28 additions & 1 deletion src/chains/ethereum/ethereum/src/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<Buffer> =>
new Promise((resolve, reject) => {
trie.get(address, (err, data) => {
Expand Down Expand Up @@ -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;
Expand All @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
130 changes: 130 additions & 0 deletions src/chains/ethereum/ethereum/tests/api/eth/ethCall.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
});
});