Skip to content

Commit

Permalink
test: Expand ctx/storage tests for precompiles
Browse files Browse the repository at this point in the history
This implements shared ABI interfaces for both the mock contracts and
later on, precompiles, that are used for the tests.

The main goal is to test precompiles handle different message call types
correctly, msg.sender, msg.value, storage location.

This expands the current set of tests that simply define the behavior of
call message types to a set of tests that can also enforce the same
behavior on a set of precompiles.
  • Loading branch information
drklee3 committed Oct 24, 2024
1 parent 6fcd3f8 commit 428d6c2
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 27 deletions.
18 changes: 12 additions & 6 deletions tests/e2e-evm/contracts/ContextInspector.sol
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/**
* @title A contract to inspect the msg context.
* @notice This contract is used to test the expected msg.sender and msg.value
* of a contract call in various scenarios.
*/
contract ContextInspector {
interface ContextInspector {
/**
* @dev Emitted when the emitMsgSender() function is called.
*/
Expand All @@ -19,6 +14,17 @@ contract ContextInspector {
*/
event MsgValue(uint256 value);

function emitMsgSender() external;
function emitMsgValue() external payable;
function getMsgSender() external view returns (address);
}

/**
* @title A contract to inspect the msg context.
* @notice This contract is used to test the expected msg.sender and msg.value
* of a contract call in various scenarios.
*/
contract ContextInspectorMock is ContextInspector {
function emitMsgSender() external {
emit MsgSender(msg.sender);
}
Expand Down
6 changes: 5 additions & 1 deletion tests/e2e-evm/contracts/StorageTests.sol
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface StorageBasic {
function setStorageValue(uint256 value) external;
}

/**
* @title A basic contract with a storage value.
* @notice This contract is used to test storage reads and writes, primarily
* for testing storage behavior in delegateCall.
*/
contract StorageBasic {
contract StorageBasicMock is StorageBasic {
uint256 public storageValue;

function setStorageValue(uint256 value) external {
Expand Down
51 changes: 31 additions & 20 deletions tests/e2e-evm/test/message_call.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ describe("Message calls", () => {

interface TestContext<Impl extends keyof ArtifactsMap> {
lowLevelCaller: GetContractReturnType<ArtifactsMap["Caller"]["abi"]>;
implementationContract: GetContractReturnType<ArtifactsMap[Impl]["abi"]>;
// Using a generic to enforce type safety on functions that use this Abi,
// eg. decodeFunctionResult would have the functionName parameter type
// checked.
implementationAbi: ArtifactsMap[Impl]["abi"];
implementationAddress: Address;
}

type CallType = "call" | "callcode" | "delegatecall" | "staticcall";
Expand Down Expand Up @@ -198,7 +202,7 @@ describe("Message calls", () => {

for (const tc of testCases) {
it(tc.name, async function () {
let functionName: ContractFunctionName<typeof ctx.implementationContract.abi> = "emitMsgSender";
let functionName: ContractFunctionName<typeof ctx.implementationAbi> = "emitMsgSender";

// Static Call cannot emit events so we use a different function that
// just returns the msg.sender.
Expand All @@ -208,13 +212,13 @@ describe("Message calls", () => {

// ContextInspector function call data
const baseData = encodeFunctionData({
abi: ctx.implementationContract.abi,
abi: ctx.implementationAbi,
functionName,
args: [],
});

// Modify the call data based on the test case type
const txData = buildCallData(tc.type, ctx.implementationContract.address, baseData);
const txData = buildCallData(tc.type, ctx.implementationAddress, baseData);
// No value for this test
txData.value = 0n;

Expand All @@ -235,7 +239,7 @@ describe("Message calls", () => {

// Decode dataBytes as an address
const address = decodeFunctionResult({
abi: ctx.implementationContract.abi,
abi: ctx.implementationAbi,
functionName: "getMsgSender",
data: dataBytes,
});
Expand All @@ -254,7 +258,7 @@ describe("Message calls", () => {
expect(txReceipt.status).to.equal("success");

const [receivedAddress] = getResponseFromReceiptLogs(
ctx.implementationContract.abi,
ctx.implementationAbi,
"MsgSender",
parseAbiParameters("address"),
txReceipt,
Expand Down Expand Up @@ -330,13 +334,13 @@ describe("Message calls", () => {
it(tc.name, async function () {
// Initial data of a emitMsgSender() call.
const baseData = encodeFunctionData({
abi: ctx.implementationContract.abi,
abi: ctx.implementationAbi,
functionName: "emitMsgValue",
args: [],
});

// Modify the call data based on the test case type
const txData = buildCallData(tc.type, ctx.implementationContract.address, baseData, tc.giveChildCallValue);
const txData = buildCallData(tc.type, ctx.implementationAddress, baseData, tc.giveChildCallValue);
txData.value = tc.giveParentValue;

if (!tc.wantRevertReason) {
Expand All @@ -357,7 +361,7 @@ describe("Message calls", () => {
expect(txReceipt.status).to.equal("success");

const [emittedAmount] = getResponseFromReceiptLogs(
ctx.implementationContract.abi,
ctx.implementationAbi,
"MsgValue",
parseAbiParameters("uint256"),
txReceipt,
Expand Down Expand Up @@ -390,7 +394,7 @@ describe("Message calls", () => {
{
name: "call storage in implementation",
callType: "call",
wantStorageContract: (ctx) => ctx.implementationContract.address,
wantStorageContract: (ctx) => ctx.implementationAddress,
},
{
name: "callcode storage in caller",
Expand Down Expand Up @@ -421,12 +425,12 @@ describe("Message calls", () => {
giveStoreValue++;

const baseData = encodeFunctionData({
abi: ctx.implementationContract.abi,
abi: ctx.implementationAbi,
functionName: "setStorageValue",
args: [giveStoreValue],
});

const txData = buildCallData(tc.callType, ctx.implementationContract.address, baseData);
const txData = buildCallData(tc.callType, ctx.implementationAddress, baseData);
// Signer is the whale
txData.account = whaleAddress;
// Call gas + storage gas
Expand Down Expand Up @@ -472,28 +476,35 @@ describe("Message calls", () => {

// Test context and storage for mock contracts as a baseline check
describe("Mock", () => {
let contextInspector: GetContractReturnType<ArtifactsMap["ContextInspector"]["abi"]>;
let storageBasic: GetContractReturnType<ArtifactsMap["StorageBasic"]["abi"]>;
let contextInspectorMock: GetContractReturnType<ArtifactsMap["ContextInspectorMock"]["abi"]>;
let storageBasicMock: GetContractReturnType<ArtifactsMap["StorageBasicMock"]["abi"]>;

before("deploy mock contracts", async function () {
contextInspector = await hre.viem.deployContract("ContextInspector");
// Mock contracts & precompiles need to implement these interfaces. These
// ABIs will be used to test the message call behavior.
const contextInspectorAbi = hre.artifacts.readArtifactSync("ContextInspector").abi;
const storageBasicAbi = hre.artifacts.readArtifactSync("StorageBasic").abi;

storageBasic = await hre.viem.deployContract("StorageBasic");
before("deploy mock contracts", async function () {
contextInspectorMock = await hre.viem.deployContract("ContextInspectorMock");
storageBasicMock = await hre.viem.deployContract("StorageBasicMock");
});

itHasCorrectMsgSender(() => ({
lowLevelCaller: lowLevelCaller,
implementationContract: contextInspector,
implementationAbi: contextInspectorAbi,
implementationAddress: contextInspectorMock.address,
}));

itHasCorrectMsgValue(() => ({
lowLevelCaller: lowLevelCaller,
implementationContract: contextInspector,
implementationAbi: contextInspectorAbi,
implementationAddress: contextInspectorMock.address,
}));

itHasCorrectStorageLocation(() => ({
lowLevelCaller: lowLevelCaller,
implementationContract: storageBasic,
implementationAbi: storageBasicAbi,
implementationAddress: storageBasicMock.address,
}));
});

Expand Down

0 comments on commit 428d6c2

Please sign in to comment.