linera-io/linera-protocol · error · EvmExecutionError
Non-zero transfer precompile
Error message
Non-zero transfer precompile
What it means
call_or_fail inspects every EVM internal call: if the target address is one of the registered Linera precompile addresses and the call carries a CallValue::Transfer with a non-zero amount, execution fails with NonZeroTransferPrecompile (revm.rs:1096). Precompiles are pure system endpoints and cannot receive native value.
Source
Thrown at linera-execution/src/evm/revm.rs:1096
/// The precompile calls do not have associated transfers.
/// For other contract calls, the corresponding transfer
/// is executed in Linera.
///
/// Note that in the EVM transferring ethers is the same
/// as calling a function. In Linera, transferring native
/// tokens and calling a function are different operations.
/// However, the block is accepted completely or not at all.
/// Therefore, we can ensure the atomicity of the operations.
fn call_or_fail(
&self,
_context: &mut ContractCtx<'_, Runtime>,
inputs: &CallInputs,
) -> Result<Option<CallOutcome>, ExecutionError> {
let is_precompile = self.precompile_addresses.contains(&inputs.target_address);
let is_first_call = inputs.target_address == self.contract_address;
if is_precompile {
if let CallValue::Transfer(value) = inputs.value {
ensure!(
value == U256::ZERO,
EvmExecutionError::NonZeroTransferPrecompile
);
}
}
if is_precompile || is_first_call {
// Precompile calls are handled by the precompile code.
return Ok(None);
}
// Handling the balances.
if let CallValue::Transfer(value) = inputs.value {
if value != U256::ZERO {
let source: AccountOwner = inputs.caller.into();
let owner: AccountOwner = inputs.bytecode_address.into();
let mut runtime = self.db.lock_runtime();
let amount = Amount::try_from(value).map_err(EvmExecutionError::from)?;
let chain_id = runtime.chain_id()?;
let destination = Account { chain_id, owner };View on GitHub (pinned to 6c226ddcb3)
Solutions
- Strip the value: call the precompile as precompile.call(data) with no value option
- Mark the calling Solidity function nonpayable or refund excess msg.value before dispatching subcalls
- If value must move, send it in a separate explicit transfer to a contract account, never to a precompile address
Example fix
// before: value forwarded to a precompile
(bool ok, ) = PRECOMPILE.call{value: msg.value}(data);
// after: zero-value call to the precompile, funds handled separately
(bool ok, ) = PRECOMPILE.call(data);
if (msg.value > 0) payable(recipient).transfer(msg.value); Defensive patterns
Strategy: validation
Validate before calling
// Solidity: never attach value to a precompile call
function callPrecompile(bytes memory data) internal returns (bytes memory) {
(bool ok, bytes memory out) = PRECOMPILE.call(data); // no value attached
require(ok, PrecompileCallFailed());
return out;
} Type guard
fn is_nonzero_transfer_precompile(err: &ExecutionError) -> bool {
matches!(
err,
ExecutionError::EvmError(EvmExecutionError::NonZeroTransferPrecompile)
)
} Try / catch
match evm_call(input) {
Ok(out) => out,
Err(ref e) if is_nonzero_transfer_precompile(e) => {
// deterministic misuse: strip the value and fail loudly, do not blind-retry
return Err(anyhow!("precompile called with non-zero value; remove the value option"));
}
Err(e) => return Err(e.into()),
} Prevention
- Keep precompile wrappers in nonpayable functions; reject msg.value > 0 in dispatchers
- Audit forwarder and multicall code for value forwarding on outbound calls
- Send native value only to contract accounts, never to precompile addresses
When it happens
Trigger: Solidity code performing precompile.call{value: amount}(data), or any payable wrapper or fallback that attaches msg.value to every outbound external call, where the target is a Linera precompile address such as 0x0b.
Common situations: Contracts with catch-all forwarding logic that forwards msg.value along; multisend and multicall patterns that preserve value on every subcall; copy-pasted Ethereum code that tips precompiles when ported to Linera; payable functions that blindly relay callvalue.
Related errors
- The balances are incoherent for address {0}, balances {1}, {
- It is illegal to call function execute_message from an opera
- It is illegal to call function process_streams from an opera
- It is illegal to call function summarize_events from an oper
- It is illegal to call function instantiate from an operation
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/0ec6151a12180e7b.
Report an issue: GitHub.