To build a product that indexes ETH balances on RWALayer, it’s essential to understand RWALayer’s unique account model. Unlike other EVM chains, RWALayer represents ETH balances in terms of shares. Each account has a certain number of shares, which are multiplied by a global share price to determine its ETH balance. This method allows account balances to automatically rebase as new yield is reported and the global share price increases.

For accurate indexing, save individual account shares and the global share price instead of the direct account balances. At read time, an account’s balance can be derived by multiplying its shares with the global share price.

This document details how to accurately index every account’s ETH balance on RWALayer using this approach.

Yield Modes Explained

RWALayer accounts offer three different yield modes, providing users and developers with control over rebasing when new yield is reported:

Default Settings

By default, all RWALayer accounts are set to AUTOMATIC yield mode, meaning their ETH balances will rebase automatically. However, when a smart contract is deployed to an address, the account switches to VOID mode. This means that smart contract accounts default to VOID mode, while EOAs (Externally Owned Accounts) default to AUTOMATIC mode.

It’s possible (though uncommon) for ETH to be held by an account before a smart contract is deployed to it.

Both smart contracts and EOAs can change their yield modes (e.g., a smart contract opting in or an EOA opting out).

RWALayer’s Account Model

RWALayer employs a modified version of op-geth (RWALayer-geth) to represent balances in shares, enabling all accounts to update as yield is reported. Specifically, RWALayer-geth modifies the StateAccount struct to achieve this:

op-geth (Upstream)

type StateAccount struct {
  Nonce    uint64
  Root     common.Hash
  CodeHash []byte
  Balance  *big.Int
}

RWALayer-geth

type StateAccount struct {
  Nonce     uint64
  Root      common.Hash
  CodeHash  []byte
  Flags     uint8
  Fixed     *big.Int
  Shares    *big.Int
  Remainder *big.Int
}

The desired yield mode is stored in the Flags field, while the Fixed, Shares, and Remainder fields store the data required to calculate account balances in each yield mode:

Global Share Price

The global share price that determines how much ETH each share is worth is stored in the SharesBase predeploy at. This contract tracks the share price, the total number of shares, and the total amount of ETH yet to be reflected in the share price. You can read the current ETH share price by calling the price() function on this contract.

Indexing Account Shares

For accurate results, index account shares instead of account balances directly. This works similarly to indexing balances on ETH mainnet: identify which accounts were touched in transactions within a given block and re-fetch their balances on that block.

RWALayer nodes expose a new RPC method, eth_getBalanceValues, to support indexing the flags, shares, remainder, and fixed fields for accounts.

eth_getBalanceValues Request

{
  "jsonrpc": "2.0",
  "method": "eth_getBalanceValues",
  "params": ["<address>", "latest"],
  "id": 1
}

eth_getBalanceValues Response

{
  "jsonrpc": "2.0",
  "result": {
    "fixed": "0x0",
    "flags": "0x1",
    "remainder": "0x0",
    "shares": "0x0"
  },
  "id": 1
}

Edge Cases

On Ethereum mainnet, selfdestruct allows ETH to be sent to a beneficiary address without triggering any code at that address. As a result, the beneficiary account’s balance changes without there being any call made. Indexers must handle this edge case to ensure perfect accounting.

RWALayer introduces a similar edge case that indexers should be aware of. Accounts set to CLAIMABLE yield mode can claim accumulated yield to an arbitrary beneficiary address. This claim operation works the same as selfdestruct in that it can increase an arbitrary account’s balance without a call.