linera-io/linera-protocol · critical · EvmExecutionError

The balances are incoherent for address {0}, balances {1}, {

Error message

The balances are incoherent for address {0}, balances {1}, {2}

What it means

Raised by the EVM adapter's ContractDatabase::check_balance during commit_changes for every writable, non-faucet contract account: the Linera-side balance of the contract's owner (read via the Linera runtime) must equal the balance the EVM (revm) computed for that address. The EVM bridge mirrors ETH balances in Linera's native accounting; any divergence means the EVM execution changed a contract's value without the corresponding Linera transfer (or vice versa), so the block is rejected rather than committing incoherent state.

Source

Thrown at linera-execution/src/evm/database.rs:604

    pub fn new(runtime: Runtime) -> Self {
        Self {
            inner: InnerDatabase::new(runtime),
            modules: Arc::new(Mutex::new(HashMap::new())),
        }
    }

    pub fn lock_runtime(&self) -> std::sync::MutexGuard<'_, Runtime> {
        self.inner.lock_runtime()
    }

    /// Balances of the contracts have to be checked when
    /// writing. There is a balance in Linera and a balance
    /// in EVM and they have to be coherent.
    fn check_balance(&self, address: Address, revm_balance: U256) -> Result<(), ExecutionError> {
        let mut runtime = self.inner.runtime.lock().unwrap();
        let owner = address.into();
        let linera_balance: U256 = runtime.read_owner_balance(owner)?.into();
        ensure!(
            linera_balance == revm_balance,
            EvmExecutionError::IncoherentBalances(address, linera_balance, revm_balance)
        );
        Ok(())
    }

    /// Effectively commits changes to storage.
    pub fn commit_contract_changes(
        &self,
        account: &revm_state::Account,
    ) -> Result<(), ExecutionError> {
        let mut runtime = self.inner.runtime.lock().unwrap();
        let mut batch = Batch::new();
        let key_prefix = get_category_key(KeyCategory::Storage);
        let key_info = get_category_key(KeyCategory::AccountInfo);
        if account.is_selfdestructed() {
            batch.delete_key_prefix(key_prefix);
            batch.put_key_value(key_info, &AccountInfo::default())?;

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Ensure the contract's ETH transfers only use the supported paths (the bridge's transfer precompile at address 0x0b / the faucet address flow)
  2. Reproduce with the contract on a local testnet and inspect which transaction desynchronizes the balance (the error prints address, Linera balance, and revm balance)
  3. Update to the latest linera-protocol version — EVM balance-coherency fixes have landed repeatedly
  4. If it reproduces on current versions with a minimal contract, report it as a bug with the bytecode and transaction
Defensive patterns

Strategy: try-catch

Validate before calling

// In app tests, run the ETH-transfer scenario on a local testnet first; the adapter's
// check fires at commit time, so a dry-run execution surfaces the incoherence before mainnet.

Type guard

fn is_incoherent_balance(err: &ExecutionError) -> Option<(&Address, &U256, &U256)> {
    match err {
        ExecutionError::EvmError(evm_error!::IncoherentBalances(addr, linera, evm)) => Some((addr, linera, evm)),
        _ => None,
    }
}

Try / catch

match client.execute_operations(ops, vec![]).await {
    Err(e) if is_incoherent_balance(&e).is_some() => {
        // Address and both balances are in the error: log them, halt the deploy,
        // and audit the contract's ETH-transfer paths.
        tracing::error!(error = %e, "EVM/Linera balance divergence; blocking further submissions");
        return Err(e.into());
    }
    other => other,
}

Prevention

When it happens

Trigger: An EVM application moves ETH in a way the bridge does not mirror back into Linera balances: raw value transfers on paths outside the faucet precompile, selfdestruct value-recovery flows, precompile transfers, or bugs in the balance wiring. FAUCET_ADDRESS (0x...4000) is explicitly exempt from checks; ordinary contract accounts are not.

Common situations: Porting Solidity contracts that rely on value semantics Linera's bridge does not support; running a newer/older contract bytecode against a linera-execution version whose balance accounting changed; genuine protocol bugs in the EVM adapter itself.

Related errors


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