Direct Precompile Calls
How to interact with allowlist precompiles directly via Solidity to manage roles.
When you call a precompile from Solidity, you're not calling a deployed smart contract—you're invoking Go code built into the VM. This lesson explains how direct precompile calls work.
When Do You Call the Precompile Directly?
You call the precompile directly when you want to manage the allowlist:
- Add an address as Admin, Manager, or Enabled
- Remove an address (set to None)
- Read an address's current role
The Call Flow
When your transaction calls a precompile (like the Transaction AllowList), here's what happens:
Step-by-Step Breakdown
1. Your Solidity Call
When you call a precompile from Solidity, it looks like any other contract call:
IAllowList allowlist = IAllowList(0x0200000000000000000000000000000000000002);
allowlist.setEnabled(0xBob);The compiler generates a CALL opcode targeting the precompile address with your function call encoded as ABI data.
2. EVM Recognizes the Precompile Address
When the EVM encounters a call to an address in the reserved range (0x0200...), it checks if there's a registered precompile:
// PrecompileOverride checks if address has a registered precompile
func (r RulesExtra) PrecompileOverride(addr common.Address) (libevm.PrecompiledContract, bool) {
module, ok := modules.GetPrecompileModuleByAddress(addr)
if !ok {
return nil, false // Not a precompile, use normal execution
}
return makePrecompile(module.Contract), true // Route to Go code
}Reserved address ranges for precompiles:
0x0100...00to0x0100...ff0x0200...00to0x0200...ff(where allowlist precompiles live)0x0300...00to0x0300...ff
3. Function Selector Dispatch
The precompile parses the function selector (first 4 bytes) to determine which function you're calling:
func (c *Contract) Run(
accessibleState contract.AccessibleState,
caller common.Address,
addr common.Address,
input []byte,
suppliedGas uint64,
readOnly bool,
) (ret []byte, remainingGas uint64, err error) {
selector := input[:4]
switch {
case bytes.Equal(selector, setAdminSelector):
return c.setAdmin(accessibleState, caller, input[4:], suppliedGas)
case bytes.Equal(selector, setEnabledSelector):
return c.setEnabled(accessibleState, caller, input[4:], suppliedGas)
case bytes.Equal(selector, readAllowListSelector):
return c.readAllowList(accessibleState, input[4:], suppliedGas)
}
}4. Permission Check & State Write
Before modifying state, the precompile checks the caller's permission level:
The state is stored using GetState and SetState:
// Write a role to storage
func setAllowListRole(state StateDB, addr common.Address, role uint8) {
key := crypto.Keccak256Hash(addr.Bytes())
value := common.BytesToHash([]byte{role})
state.SetState(PrecompileAddress, key, value)
}Gas Costs
Precompiles have fixed gas costs:
| Operation | Gas Cost |
|---|---|
| Read role | 5,000 |
| Write role | 20,000 |
Key Takeaways
| Concept | What Happens |
|---|---|
| Solidity Call | Generates CALL opcode to precompile address with ABI-encoded data |
| EVM Routing | PrecompileOverride intercepts calls to reserved addresses |
| Function Dispatch | First 4 bytes of input determine which Go function runs |
| State Storage | Precompiles use same GetState/SetState as regular contracts |
What This Accomplishes
Direct precompile calls let you manage the allowlist:
- Grant permissions to addresses
- Revoke permissions from addresses
- Query current permission levels
But this is only half the story. In the next lesson, you'll learn how the blockchain enforces these permissions when users try to send transactions or deploy contracts.
Is this guide helpful?
