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

  1. Strip the value: call the precompile as precompile.call(data) with no value option
  2. Mark the calling Solidity function nonpayable or refund excess msg.value before dispatching subcalls
  3. 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

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


AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22). Data as JSON: /api/errors/0ec6151a12180e7b. Report an issue: GitHub.