Arbitrum Precompile Etching & InvalidFEOpcode
October 21st, 2024

📚 Background: Arbitrum Precompiles

The Synthetix v3 instance on Arbitrum relies on the ArbGasInfo precompile to determine gas costs incurred by third parties, such as liquidators, who act autonomously to help maintain system stability.

Authors: Jared Borders, Andrew Chiaramonte

🚧 Problem: Fork Testing

When writing tests for the Synthetix v3 protocol, it is often necessary to fork the Arbitrum network. This approach simulates a realistic system state, enabling more efficient and higher-quality testing with less effort.

However, an issue arises when forking because precompile addresses do not contain bytecode. As a result, any protocol methods relying on these precompiles will revert when called.

💡 Precompiles 101

Precompiles are smart contracts with special addresses that provide specific functionality, executed natively by the EVM client rather than at the bytecode level. This allows precompiles to perform tasks that would otherwise be costly, difficult, or even impossible to implement with standard smart contract code. As a result, they enable smoother and more efficient execution of specialized tasks within the EVM.

Precompiles offer functionality such as cryptographic methods, memory copying utilities, and tools for facilitating L1 ↔ L2 interactions.

Spotting The Fork Testing Issue

When fork testing encounters a problem with precompiles, it typically manifests as follows:

  1. The test reverts with an InvalidFEOpcode

  2. This revert occurs in conjunction with a call to a precompile address

Note: The FE in InvalidFEOpcode refers to the INVALID opcode, which serves as a catch-all for undefined bytecode.

Identifying a Precompile:

After the test reverts with an InvalidFEOpcode, check if the function being called (e.g., getPricesInWei()) is on a contract deployed at "GENESIS" when viewed on a block explorer: A contract deployed at “GENESIS” indicates a precompile.

⛏️ Solution: Etching with Foundry

Leveraging Foundry's library of cheatcodes, we can easily inject bytecode into the address where the protocol expects it to exist. This is achieved using Foundry's etch cheatcode.

Foundry's documentation specifically mentions using etch to address precompile-related issues:

Some chains, like Blast or Arbitrum, run with custom precompiles. Foundry is operating on vanilla EVM and is not aware of those. If you are encountering reverts due to not available precompile, you can use vm.etch cheatcode to inject mock of the missing precompile to the address it is expected to appear at.

Given this, the following Solidity code demonstrates how to circumvent the dependency issue that arises between Synthetix v3 and Arbitrum's ArbGasInfo precompile:

pragma solidity 0.8.27;

import {Test} from "forge-std/Test.sol";

contract BootstrapArbitrumFork is Test {

    address ARB_GAS_INFO_PRECOMPILE = 0x000000000000000000000000000000000000006C;
		
		/// @dev expand the parameters to fine-tune the mock
    function mockArbGasInfo(uint256 x) public {
        ArbGasInfoMock arbGasInfoMock = new ArbGasInfoMock({
            _L2_TX: x,
            _L1_CALLDATA_BYTE: x,
            _STORAGE_ALLOCATION: x,
            _ARB_GAS_BASE: x,
            _ARB_GAS_CONGESTION: x,
            _ARB_GAS_TOTAL: x,
            _L1_BASE_FEE_ESTIMATE: x
        });
        
        vm.etch(ARB_GAS_INFO_PRECOMPILE, address(arbGasInfoMock).code);
    }

}

contract ArbGasInfoMock {

    uint256 immutable L2_TX;
    uint256 immutable L1_CALLDATA_BYTE;
    uint256 immutable STORAGE_ALLOCATION;
    uint256 immutable ARB_GAS_BASE;
    uint256 immutable ARB_GAS_CONGESTION;
    uint256 immutable ARB_GAS_TOTAL;
    uint256 immutable L1_BASE_FEE_ESTIMATE;

    constructor(
        uint256 _L2_TX,
        uint256 _L1_CALLDATA_BYTE,
        uint256 _STORAGE_ALLOCATION,
        uint256 _ARB_GAS_BASE,
        uint256 _ARB_GAS_CONGESTION,
        uint256 _ARB_GAS_TOTAL,
        uint256 _L1_BASE_FEE_ESTIMATE
    ) {
        L2_TX = _L2_TX;
        L1_CALLDATA_BYTE = _L1_CALLDATA_BYTE;
        STORAGE_ALLOCATION = _STORAGE_ALLOCATION;
        ARB_GAS_BASE = _ARB_GAS_BASE;
        ARB_GAS_CONGESTION = _ARB_GAS_CONGESTION;
        ARB_GAS_TOTAL = _ARB_GAS_TOTAL;
        L1_BASE_FEE_ESTIMATE = _L1_BASE_FEE_ESTIMATE;
    }

    function getL1BaseFeeEstimate() external view returns (uint256) {
        return L1_BASE_FEE_ESTIMATE;
    }

}

Mocking Real World Scenarios

When deploying the ArbGasInfoMock in practice, dummy values can be used for all variables, as seen here. But in the case of precompiles that require precise values, you can look at the precompile contract on a block explorer and mock the values you get from the read functions.

Subscribe to Andrew Chiaramonte
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from Andrew Chiaramonte

Skeleton

Skeleton

Skeleton