Current section
Files
Jump to
Current section
Files
onchain_aave
README.md
README.md
# 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](https://github.com/ZenHive/onchain-stack/tree/main/packages/onchain).
## Installation
```elixir
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](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](https://hex.pm/packages/descripex) for self-describing APIs:
```elixir
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](https://hex.pm/packages/onchain_evm) (revm through a Rustler NIF).
Add it yourself; `onchain_aave` does not pull it in at runtime:
```elixir
{:onchain_evm, "~> 0.6"}
```
Read Aave state at fork state, with no transaction and no gas:
```elixir
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:
```elixir
# 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:
```elixir
config :cartouche, :ethereum_node, "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"
```
Or pass `:rpc_url` per-call to any function.