Skip to content

Commit

Permalink
Merge pull request NomicFoundation#1372 from nomiclabs/support-older-…
Browse files Browse the repository at this point in the history
…hardforks

Support older hard forks
  • Loading branch information
fvictorio committed Apr 6, 2021
2 parents 8ac46c6 + 644259a commit 888378a
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 8 deletions.
2 changes: 1 addition & 1 deletion docs/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ You can set the following fields on the `hardhat` config:

- `blockGasLimit`: The block gas limit to use in Hardhat Network's blockchain. Default value: `9500000`

- `hardfork`: This setting changes how Hardhat Network works, to mimic Ethereum's mainnet at a given hardfork. It must be one of `"byzantium"`, `"constantinople"`, `"petersburg"`, `"istanbul"`, and `"muirGlacier"`. Default value: `"muirGlacier"`
- `hardfork`: This setting changes how Hardhat Network works, to mimic Ethereum's mainnet at a given hardfork. It must be one of `"chainstart"`, `"homestead"`, `"dao"`, `"tangerineWhistle"`, `"spuriousDragon"`, `"byzantium"`, `"constantinople"`, `"petersburg"`, `"istanbul"`, `"muirGlacier"`, and `"berlin"`. Default value: `"muirGlacier"`

- `throwOnTransactionFailures`: A boolean that controls if Hardhat Network throws on transaction failures.
If this value is `true`, Hardhat Network will throw [combined JavaScript and Soldity stack traces](../hardhat-network/README.md#solidity-stack-traces)
Expand Down
6 changes: 6 additions & 0 deletions packages/hardhat-core/src/internal/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ export const HARDHAT_NETWORK_NAME = "hardhat";
export const SOLIDITY_FILES_CACHE_FILENAME = "solidity-files-cache.json";

export const HARDHAT_NETWORK_SUPPORTED_HARDFORKS = [
"chainstart",
"homestead",
"dao",
"tangerineWhistle",
"spuriousDragon",
"byzantium",
"constantinople",
"petersburg",
"istanbul",
"muirGlacier",
"berlin",
];

export const ARTIFACT_FORMAT_VERSION = "hh-sol-artifact-1";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { CompilerInput, CompilerOutput } from "../../../types";
import { HARDHAT_NETWORK_DEFAULT_GAS_PRICE } from "../../core/config/default-config";
import { assertHardhatInvariant, HardhatError } from "../../core/errors";
import {
InternalError,
InvalidInputError,
TransactionExecutionError,
} from "../../core/providers/errors";
Expand Down Expand Up @@ -131,6 +132,12 @@ export class HardhatNode extends EventEmitter {
} = await makeForkClient(config.forkConfig, config.forkCachePath);
common = await makeForkCommon(config);

this._validateHardforks(
config.forkConfig.blockNumber,
common,
forkClient.getNetworkId()
);

const forkStateManager = new ForkStateManager(
forkClient,
forkBlockNumber
Expand Down Expand Up @@ -188,6 +195,42 @@ export class HardhatNode extends EventEmitter {
return [common, node];
}

private static _validateHardforks(
forkBlockNumber: number | undefined,
common: Common,
remoteChainId: number
): void {
if (!common.gteHardfork("spuriousDragon")) {
throw new InternalError(
`Invalid hardfork selected in Hardhat Network's config.
The hardfork must be at least spuriousDragon, but ${common.hardfork()} was given.`
);
}

if (forkBlockNumber !== undefined) {
let upstreamCommon: Common;
try {
upstreamCommon = new Common({ chain: remoteChainId });
} catch (error) {
// If ethereumjs doesn't have a common it will throw and we won't have
// info about the activation block of each hardfork, so we don't run
// this validation.
return;
}

upstreamCommon.setHardforkByBlockNumber(forkBlockNumber);

if (!upstreamCommon.gteHardfork("spuriousDragon")) {
throw new InternalError(
`Cannot fork ${upstreamCommon.chainName()} from block ${forkBlockNumber}.
Hardhat Network's forking functionality only works with blocks from at least spuriousDragon.`
);
}
}
}

private readonly _localAccounts: Map<string, Buffer> = new Map(); // address => private key
private readonly _impersonatedAccounts: Set<string> = new Set(); // address

Expand Down Expand Up @@ -990,6 +1033,8 @@ export class HardhatNode extends EventEmitter {
blockOpts: { calcDifficultyFromHeader: parentBlock.header },
});

const transactions = [];

try {
const traces: GatherTracesResult[] = [];

Expand All @@ -1010,6 +1055,7 @@ export class HardhatNode extends EventEmitter {
if (tx.gasLimit.gt(blockGasLimit.sub(blockBuilder.gasUsed))) {
txHeap.pop();
} else {
transactions.push(tx);
const txResult = await blockBuilder.addTransaction(tx);
const { txReceipt } = await generateTxReceipt.bind(this._vm)(
tx,
Expand All @@ -1035,6 +1081,15 @@ export class HardhatNode extends EventEmitter {

const block = await blockBuilder.build();

// We replace the block's transactions with the actual ones,
// as the block builder recreates them, turning fake transactions
// into real ones.
//
// IMPORTANT: this workaround only works because while BlockBuilder#addTransaction
// recreates the transactions you pass it, it actually runs yours.
block.transactions.splice(0);
block.transactions.push(...transactions);

await this._txPool.updatePendingAndQueued();

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,15 @@ export interface RpcReceiptOutput {
gasUsed: string;
logs: RpcLogOutput[];
logsBloom: string;
status: string;
to: string | null;
transactionHash: string;
transactionIndex: string;

// Only present after Byzantium
status?: string;

// Only present before Byzantium
root?: string;
}

export interface RpcLogOutput {
Expand Down Expand Up @@ -182,7 +187,7 @@ export function getRpcReceipts(
getRpcLogOutput(log, tx, block, i, logIndex)
);

receipts.push({
const rpcReceipt: RpcReceiptOutput = {
transactionHash: bufferToRpcData(tx.hash()),
transactionIndex: numberToRpcQuantity(i),
blockHash: bufferToRpcData(block.hash()),
Expand All @@ -197,8 +202,15 @@ export function getRpcReceipts(
: null,
logs,
logsBloom: bufferToRpcData(receipt.bitvector),
status: numberToRpcQuantity((receipt as PostByzantiumTxReceipt)?.status),
});
};

if ("stateRoot" in receipt) {
rpcReceipt.root = bufferToRpcData(receipt.stateRoot);
} else {
rpcReceipt.status = numberToRpcQuantity(receipt.status);
}

receipts.push(rpcReceipt);
}

return receipts;
Expand Down
1 change: 1 addition & 0 deletions packages/hardhat-core/test/fixture-projects/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/*/cache
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
networks: {
hardhat: {
forking: {
url: process.env.ALCHEMY_URL,
blockNumber: 2463000,
},
},
},
solidity: "0.5.15",
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
networks: {
hardhat: {
hardfork: "tangerineWhistle",
forking: {
url: process.env.ALCHEMY_URL,
},
},
},
solidity: "0.5.15",
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
networks: {
hardhat: {
hardfork: "spuriousDragon",
},
},
solidity: "0.5.15",
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { assert } from "chai";

import { numberToRpcQuantity } from "../../../../src/internal/core/jsonrpc/types/base-types";
import { InternalError } from "../../../../src/internal/core/providers/errors";
import {
dateToTimestampSeconds,
parseDateString,
Expand All @@ -9,6 +10,7 @@ import { HardhatNetworkUserConfig } from "../../../../src/types";
import { useEnvironment } from "../../../helpers/environment";
import { expectErrorAsync } from "../../../helpers/errors";
import { useFixtureProject } from "../../../helpers/project";
import { ALCHEMY_URL } from "../../../setup";

describe("Hardhat Network special options", function () {
describe("allowUnlimitedContractSize", function () {
Expand Down Expand Up @@ -83,4 +85,58 @@ describe("Hardhat Network special options", function () {
assert.isBelow(+secondBlock.timestamp, initialDate + 20);
});
});

describe("hardfork", function () {
describe("When not forking", function () {
useFixtureProject("hardhat-network-spurious-dragon");
useEnvironment();

it("should accept hardforks as late as chainstart and use them", async function () {
const [sender] = await this.env.network.provider.send("eth_accounts");
await assert.isRejected(
this.env.network.provider.send("eth_sendTransaction", [
{
from: sender,
// This is a deployment with a constructor that just executes CHAIN_ID
// which was added in Istanbul
data: "0x46",
},
]),
"Transaction reverted"
);
});
});

describe("When forking", function () {
if (ALCHEMY_URL === undefined) {
return;
}

describe("Local hardfork validation", function () {
useFixtureProject("hardhat-network-fork-tangerine-whistle");
useEnvironment();

it("Shouldn't work with hardforks before spurious dragon", async function () {
await assert.isRejected(
this.env.network.provider.send("eth_accounts"),
InternalError,
"Invalid hardfork"
);
});
});

describe("Remote hardfork validation", function () {
useFixtureProject("hardhat-network-fork-from-old-block");
useEnvironment();

it("Shouldn't work with block numbers from before spurious dragon", async function () {
await assert.isRejected(
this.env.network.provider.send("eth_accounts"),
InternalError,
"Cannot fork mainnet from block"
);
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
EthSubscription,
ProviderMessage,
} from "../../../../../src/types";
import { useEnvironment } from "../../../../helpers/environment";
import { useFixtureProject } from "../../../../helpers/project";
import { workaroundWindowsCiFailures } from "../../../../utils/workaround-windows-ci-failures";
import {
assertInvalidArgumentsError,
Expand Down Expand Up @@ -4506,3 +4508,47 @@ describe("Eth module", function () {
});
});
});

describe("Eth module - special tests", function () {
describe("Receipts formatting", function () {
describe("Before byzantium", function () {
useFixtureProject("hardhat-network-spurious-dragon");
useEnvironment();

it("Should have a root field, and shouldn't have a status one", async function () {
const [sender] = await this.env.network.provider.send("eth_accounts");
const tx = await this.env.network.provider.send("eth_sendTransaction", [
{ from: sender, to: sender },
]);

const receipt = await this.env.network.provider.send(
"eth_getTransactionReceipt",
[tx]
);

assert.isDefined(receipt.root);
assert.isUndefined(receipt.status);
});
});

describe("After byzantium", function () {
useFixtureProject("default-config-project");
useEnvironment();

it("Should have a status field and not a root one", async function () {
const [sender] = await this.env.network.provider.send("eth_accounts");
const tx = await this.env.network.provider.send("eth_sendTransaction", [
{ from: sender, to: sender },
]);

const receipt = await this.env.network.provider.send(
"eth_getTransactionReceipt",
[tx]
);

assert.isDefined(receipt.status);
assert.isUndefined(receipt.root);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
numberToRpcQuantity,
rpcQuantityToNumber,
} from "../../../../../src/internal/core/jsonrpc/types/base-types";
import { expectErrorAsync } from "../../../../helpers/errors";
import { ALCHEMY_URL } from "../../../../setup";
import { workaroundWindowsCiFailures } from "../../../../utils/workaround-windows-ci-failures";
import { assertInvalidArgumentsError } from "../../helpers/assertions";
Expand Down Expand Up @@ -62,17 +63,46 @@ describe("Hardhat module", function () {
},
]);

// The tx's msg.sender should be correct during execution

// msg.sender assertion contract:
//
// pragma solidity 0.7.0;
//
// contract C {
// constructor() {
// require(msg.sender == 0xC014BA5EC014ba5ec014Ba5EC014ba5Ec014bA5E);
// }
// }
const CODE =
"0x6080604052348015600f57600080fd5b5073c014ba5ec014ba5ec014ba5ec014ba5ec014ba5e73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614605b57600080fd5b603f8060686000396000f3fe6080604052600080fdfea26469706673582212208048da4076c3540ec6ad48a816e6531a302449e979836bd7955dc6bd2c87a52064736f6c63430007000033";

await this.provider.send("hardhat_impersonateAccount", [
impersonatedAddress,
]);

await this.provider.send("eth_sendTransaction", [
await expectErrorAsync(() =>
deployContract(this.provider, CODE, DEFAULT_ACCOUNTS_ADDRESSES[0])
);

// deploying with the right address should work
await deployContract(this.provider, CODE, impersonatedAddress);

// Getting the tx through the RPC should give the right from

const tx = await this.provider.send("eth_sendTransaction", [
{
from: impersonatedAddress,
to: DEFAULT_ACCOUNTS_ADDRESSES[0],
value: "0x10",
to: impersonatedAddress,
},
]);

const receipt = await this.provider.send(
"eth_getTransactionReceipt",
[tx]
);

assert.equal(receipt.from, impersonatedAddress.toLowerCase());
});

it("lets you deploy a contract from an impersonated account", async function () {
Expand Down

0 comments on commit 888378a

Please sign in to comment.