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
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.
When fork testing encounters a problem with precompiles, it typically manifests as follows:
The test reverts with an InvalidFEOpcode
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.
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.
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;
}
}
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.