OnchainAave

Aave V3 and V4 protocol wrappers for Elixir -- V3 pool reads/writes, V4 Hub-and-Spoke reads and Position Manager writes, oracle, math, and type structs. Built on onchain.

Installation

def deps do
[
{:onchain_aave, "~> 0.6"}
]
end

onchain (~> 0.12) arrives transitively — declare it directly only if you call it yourself, and then with a bound that admits ~> 0.12.

Node compatibility

Every call in this package is a plain eth_call against a deployed Aave V3 contract, so it runs on any mainstream Ethereum JSON-RPC endpoint — Alchemy, Infura, QuickNode, a self-hosted Geth/reth/Erigon. No debug_*/trace_* namespace, no client-specific extensions, no WebSocket.

One requirement is worth planning for: reading reserve state, rates, or user positions at a past block is a historical-state read, which needs an archive node or a hosted plan that retains history. Without it the endpoint answers -32001 Unable to complete request (or a "missing trie node" error, depending on client) rather than returning stale data. Current-block reads — the default — have no such requirement.

The chain you point at must actually have Aave V3 deployed; see Onchain.Aave.Types for the addresses this package knows about.

V4 addresses cover Ethereum and Avalanche. Resolve any registered Hub by name, e.g. Onchain.Aave.V4.Hub.hub_address(:global_dollar) or Onchain.Aave.V4.Hub.hub_address(:core, network: :avalanche). See V4_SCOPING.md for the pinned address book and naming aliases.

Modules

Module Purpose
Onchain.Aave.Pool Pool read + write calls (getUserAccountData, getReserveVariableDebtToken, supply, borrow, repay; get_user_account_data_many batches many users via Multicall3). :stable interest-rate mode is rejected locally.
Onchain.Aave.Oracle Asset price oracle + Chainlink
Onchain.Aave.Math USD conversion, LTV, health factor, V3 WadRayMath / MathUtils (revm- and mutation-checked)
Onchain.Aave.Math.V4 Aave V4 ray/wad math (pinned-bytecode revm and mutation-checked)
Onchain.Aave.DebtToken Variable debt-token credit delegation (approveDelegation, borrowAllowance)
Onchain.Aave.Contracts Verified address registry (mainnet + multi-chain, V3 + V4)
Onchain.Aave.UiPoolDataProvider Reserves and user reserves data
Onchain.Aave.Faucet Testnet faucet interactions (mint test tokens)
Onchain.Aave.V4.Hub V4 Hub reads across registered Hubs (Ethereum and Avalanche) (member Spokes, credit-line inventory and caps, rate environment, share/asset previews, bound constants)
Onchain.Aave.V4.Oracle V4 Spoke-scoped IAaveOracle reads (reserve prices, sources, decimals) plus Chainlink feeds
Onchain.Aave.V4.PositionManager V4 Giver/Taker/Config writes (supply/repay/borrow/withdraw on-behalf-of, Spoke position-manager authorization, collateral toggle, risk-premium/dynamic-config permissions) plus Taker allowances
Onchain.Aave.V4.Spoke V4 Spoke reads (reserve/user data, position-manager checks)
Onchain.Aave.V4.TokenizationSpoke V4 ERC-4626 Tokenization Spoke reads (lookup(hub, asset), share accounting, Hub/asset metadata)

Discovery

All modules use descripex for self-describing APIs:

OnchainAave.describe() # Module overview
OnchainAave.describe(:pool) # Function listings
OnchainAave.describe(:pool, :supply) # Full function details

Local simulation against forked state

Every call in this library — reads and writes — also runs inside a local EVM forked from any RPC endpoint, via onchain_evm (revm through a Rustler NIF). Add it yourself; onchain_aave does not pull it in at runtime:

{:onchain_evm, "~> 0.6"}

Read Aave state at fork state, with no transaction and no gas:

pool = "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2"
{:ok, data} = Onchain.ABI.encode_call("getUserAccountData(address)", [user_bin])
{:ok, out} = Onchain.EVM.simulate_call(pool, data, rpc_url: rpc_url)
{:ok, raw} = Onchain.ABI.decode_types("(uint256,uint256,uint256,uint256,uint256,uint256)", out)
Onchain.Aave.Types.UserAccountData.from_raw(raw)
#=> %UserAccountData{total_collateral_base: #Decimal<…>, health_factor: #Decimal<…>, …}

simulate_batch/2 runs several calls on one fork and carries state between them, and :state_overrides seeds accounts beforehand — so a wallet holding nothing can approve, supply, and be scored, entirely locally. "balance", "nonce" and "code" are hex strings; "storage" is a JSON string of a slot→value map:

# WETH's balanceOf mapping lives at storage slot 3
slot = Cartouche.Hash.keccak(<<0::96>> <> user_bin <> <<3::256>>) |> Onchain.Hex.encode()
{:ok, approve} = Onchain.ABI.encode_call("approve(address,uint256)", [pool_bin, amount])
{:ok, supply} = Onchain.ABI.encode_call("supply(address,uint256,address,uint16)", [weth_bin, amount, user_bin, 0])
{:ok, query} = Onchain.ABI.encode_call("getUserAccountData(address)", [user_bin])
{:ok, [_approve, supply_result, account]} =
Onchain.EVM.simulate_batch(
[{weth, approve}, {pool, supply}, {pool, query}],
rpc_url: rpc_url,
from: user,
state_overrides: %{
user => %{"balance" => "0x8AC7230489E80000"},
weth => %{"storage" => JSON.encode!(%{slot => "0x8AC7230489E80000"})}
}
)
supply_result.success #=> true
supply_result.gas_used #=> 184324
supply_result.logs #=> ReserveDataUpdated, aWETH Transfer/Mint, Supply
{:ok, raw} = Onchain.ABI.decode_types("(uint256,uint256,uint256,uint256,uint256,uint256)", account.output)
Onchain.Aave.Types.UserAccountData.from_raw(raw)
#=> %UserAccountData{total_collateral_base: #Decimal<24262.09170529>,
#=> ltv: #Decimal<0.805>, current_liquidation_threshold: #Decimal<0.83>, …}

The fork's block environment comes from the forked block header — number, timestamp, basefee, gas limit, coinbase and prevrandao — so interest accrual, oracle staleness checks and anything else reading block.timestamp behave as they do on chain. :state_overrides are applied on top of the fetched account rather than replacing it, so the "storage" patch above leaves WETH's deployed code intact.

test/onchain/aave/v4/deployed_integration_test.exs runs the same path against V4: it supplies, borrows, withdraws and repays through the PositionManager against mainnet state at a pinned block, authorizes and revokes position managers on the Spoke, and toggles collateral through both Spoke and Config wrappers.

Config Position Manager updates require the corresponding permission even when acting on your own position. First authorize Config using PositionManager.set_user_position_manager/4, then grant set_can_update_user_risk_premium_permission/4 or set_can_update_user_dynamic_config_permission/4 before the matching update_user_*_on_behalf_of/3 call. Each setter takes (spoke, delegatee, status, opts); false revokes the permission. Collateral permission does not grant either update.

Configuration

Requires an Ethereum JSON-RPC endpoint, configured via onchain/cartouche:

config :cartouche, :ethereum_node, "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"

Or pass :rpc_url per-call to any function.