Skip to main content
All articles

EVM: How Does a Function Call Become a State Change in the Same Ledger?

· 11 min read
Bankware Global Engineering

A wants to send B 20 out of a token balance of 100. A has authorized S to spend up to 30, and S submits this transaction. Afterward, A will hold 80, B will hold 20, and S will have 10 left to spend.

One way to perform this transfer is to write the result into the transaction in advance. The transaction specifies which records to use and which records to leave behind, then asks a program to check whether those changes are allowed. That is the approach we explored in Native Program proposal validation.

In a typical contract call, however, S does not specify the resulting balances and allowance as outputs. S asks the token contract to execute transferFrom(A, B, 20). Code running on a node reads the current values and computes the next ones.

Without a concrete proposal for the resulting changes, how does a function call become a state change in the same ledger? Let us look inside a single call through EVM, nigo-protocol's third execution path.

The request specifies a function, not its result

Call the example token contract T. T stores balances by address and the spending allowances that owners have granted to other users. S is not the owner of A's tokens; S is a user authorized to transfer them within the limit set by A.

This example does not move the Native Asset from the earlier article into EVM. It expresses the same transfer operation using values stored by a separate token contract. Nor does it transfer another 20 after the earlier article's transaction. It starts afresh from the following state:

Current values stored by T
A's balance: 100
B's balance: 0
A's allowance for S: 30

Business request S will sign
Call target: T
Function: transferFrom
Arguments: A, B, 20

transferFrom lets an authorized spender transfer another user's tokens. Here, we assume a simple T that reduces the allowance by the amount spent. There is no separate transfer fee, minting, or burning, and this function does not call any other contract. Protocol fees are zero, but execution limits still apply. The amount of the protocol's base coin attached to the call is also zero, separate from the transfer of 20 tokens.

The actual request also carries information needed for processing, such as a signature and execution limit. The diagram shows only the business request. S specifies the target and function arguments; S does not specify the resulting A=80, B=20, and allowance=10 as output cells in the transaction.

Balances and allowance are computed during execution

EVM is a virtual machine that executes contract code. The node loads T's code and executes it with this call's signer, S, and the function arguments. Suppose T's function performs these steps:

  1. Read A's balance of 100 and A's allowance of 30 for S.
  2. Check that both the balance and allowance cover the transfer amount of 20.
  3. Read B's balance of 0 and compute the new balances and remaining allowance.
  4. Store A's balance of 80, B's balance of 20, and the remaining allowance of 10, then emit a transfer event.

The result matches the original goal. The sum of token balances satisfies 100 + 0 = 80 + 20, and the allowance becomes 30 - 20 = 10. The allowance of 30 is not a separate token holding, so it is not added to the balance total.

The values used in these calculations come from the state at the time this transaction executes, not the numbers the signer saw on a screen while preparing the request. If an earlier transaction reduces the allowance to 10 after S prepares the request, the same request to transfer 20 will fail T's checks.

A function call can be simulated in advance to predict its result. That result, however, depends on the chosen state and execution environment. Fixing a function's arguments in the request is different from fixing the current values the function will read. Which conditions—balances, allowances, deadlines, or others—are checked depends on the contract code and request design.

Contract storage is also represented as StateCells

T's code works with balances by address and spending allowances. In the storage that EVM actually accesses, these values occupy locations called slots. A slot can be understood as an address used to find a value within a contract's storage. The diagram below omits the actual calculation of the slot number:

Value used by T's code
A's balance

EVM storage location
Contract T + its slot

nigo-protocol record
StateCell for that location
Value: 100 → 80

nigo-protocol uses the contract address and slot to find the cell for that storage location. A read returns the cell's value; a write replaces the value in the cell corresponding to the same location. In this example, the locations for B's balance and the allowance change along with the location for A's balance.

This is an important difference from Direct Cell. A Direct Cell transaction explicitly specifies its input cells and new output cells, creating new output IDs distinct from the inputs. EVM storage keeps the same Cell ID for the same contract and slot while replacing the value at that location. Sharing the StateCell record format does not make every change an identical UTXO transaction.

Even when the stored value means “A's balance,” the storage cell belongs to T's storage. A's address is business data that T uses to distinguish balances; it does not authorize A to change that cell directly through a Direct Cell transaction. Ordinary Direct Cell and Native Program proposals cannot overwrite the EVM state domain. The permitted EVM execution path manages it.

The distinction between the party that manages state and the party with business rights, introduced in StateCell ownership, appears here too. The common record format does not merge those two roles. It provides a foundation for recording the results of different execution rules in the same ledger.

Who enforces token rules, and who enforces ledger rules?

T's code determines whether a stored value represents a balance, an allowance, or a vote count. EVM executes that code's instructions, while Core, nigo-protocol's execution layer, enforces the permitted state changes and transaction processing rules.

In this example, T contains the rule that subtracts 20 from A's balance and adds 20 to B's. Core does not interpret every EVM storage value as a Native Asset quantity and check its total. If a faulty contract subtracts 20 from A but adds 25 to B, EVM's instruction execution rules alone cannot identify that business error.

Conversely, a contract cannot redefine every ledger rule. The protocol's execution layer applies common rules such as execution limits, permitted state access, and transfers and fees involving the protocol's base coin. T's token balances are not treated as the same accounting system as the protocol's base coin.

This distinction also explains why the EVM path is available alongside the others. Developers express balances by address and other conditions through storage and functions, while callers prepare function arguments. They can use a development model that reads storage to compute the next state instead of constructing every proposed change in advance. The contract is correspondingly responsible for the correctness of its business rules.

The Native Asset policy replacement rules discussed in asset policies and existing approvals do not automatically apply to an ordinary EVM token's storage either. Whether T's code can change, and how existing allowance values are interpreted afterward, depend on the separate design adopted by T.

Separate intermediate values from the values left on the ledger

What if B's balance is still 0 at the moment T records A's balance as 80? An intermediate value from an unfinished operation must not remain in the final ledger state.

The production execution path collects contract state changes in a temporary workspace. If EVM execution and the subsequent transition checks pass, it applies the changes and creates a receipt, a record of the execution result. The receipt records information such as success or failure, execution usage, and events. Collecting these results into a block, finalizing them through consensus, and storing them durably belong to the common ledger processing that follows.

If the example's top-level call succeeds, the business changes—A=80, B=20, and allowance=10—remain together. If the entire call fails with REVERT during execution, that call's storage changes and events are discarded. With a starting state of A=100, B=0, and allowance=30, the token's business state returns to those values.

That does not mean every trace of a failed transaction disappears. The result record and fees for an execution failure included in a block, and the replay-protection state associated with each transaction format, follow separate rules. For now, the distinction is that the scope of rolling back the contract's business changes differs from the scope of processing the transaction itself.

The same EVM executor can serve a different role in Native Program

Solidity and EVM also appeared in the earlier Native Program article. If both paths can use the same code executor, why distinguish their execution models?

The Native Program proposal validation covered in this series checks the inputs, outputs, and context supplied by Core. The validation program is not allowed to produce the next state by reading and writing arbitrary ledger storage or calling other contracts. The party constructing the transaction prepares the records to use and the proposed result.

In an ordinary EVM call, code reads and writes storage and can call other contracts when needed. T's direct reading and updating of balances and allowance is an example. The caller provides function arguments; the executing side computes the next state.

The distinction is therefore not whether the program was written in Solidity or compiled to EVM bytecode. It is which state access the program is allowed, and whether its job is to check a proposed change or to produce the resulting change. The same executor can be used with different responsibilities and restrictions.

This execution path also provides a foundation for reusing existing contracts and SDKs. EVM compatibility and reusing development tools examines the deployment, signing, and query flow verified with token samples, along with support for set_code account delegation and standard precompiles.

Getting the same result differs from knowing the access set in advance

Computing the next state during EVM execution does not make the result arbitrary. Given the same starting state, code, request, block context, and execution rules, nodes must compute the same result. Direct Cell and Native Program require this determinism too.

But whether we can know enough about which state a transaction will read and write before executing it is a different question. T is relatively simple, but general contracts may branch on stored values or call other contracts. The function name and arguments alone may not immediately reveal all the state that will actually be accessed.

Currently, nigo-protocol does not establish the complete, exact state access of an ordinary EVM transaction in advance. It therefore treats that transaction as a boundary for sequential processing during parallel execution. It applies the results of the parallel work accumulated so far, then executes the EVM transaction and evaluates subsequent transactions against the resulting state. This is neither a lack of determinism in EVM nor a claim that EVM transactions cannot be parallelized in principle. It is a choice aligned with the range of independence the current executor can safely establish.

We can now bring the three paths together around one question. Direct Cell checks a proposal prepared in advance against Core's ledger rules. The Native Program path combines a program's checks of business conditions with Core's ledger checks. EVM produces the next state by executing a function. All arrive at the same ledger, but they differ in where changes are prepared and what information is visible in advance.

Preserving the Same Ledger Under Parallel Execution extends that distinction to the execution of multiple transactions. The next question is how to identify transactions that can be computed together and produce the same ledger regardless of the order in which their computations finish.