OnchainAerodrome

Hex.pm

Aerodrome Finance bindings, Sugar-backed reads, and pure analytics for Elixir. Base only (chain id 8453). Built on the onchain core library.

Aerodrome is a ve(3,3) exchange combining Solidly-style v2 pools (volatile and stable) with Slipstream concentrated-liquidity pools. Emissions are directed weekly by veAERO voters; an epoch is one week and flips Thursday 00:00 UTC.

Status. The address registry, weekly epoch arithmetic, captured ABIs, pool/token structs (Types.Lp, .Position, .Swap, .Token), veAERO structs (Types.VeNFT, .Vote, .Relay, .LpEpoch, .Reward), and the LpSugar read binding (Bindings.LpSugar) are in place. Remaining Sugar bindings, the ergonomic Sugar.* read API, analytics, and writes are scoped in the monorepo ROADMAP.md (canonical source roadmap/tasks.toml at the repo root, tasks offset +5000). Dated protocol notes (official Sugar SDK sibling, tokenized stocks, B20 multiplier) live in docs/protocol-context.md.

Installation

def deps do
[
{:onchain_aerodrome, "~> 0.2"}
]
end

Layers

Each layer is usable on its own and depends only on the ones below it. This is enforced by .reach.exs (mix reach.check --arch), not merely documented.

Layer Namespace Needs
Registry / math Onchain.Aerodrome.Contracts, .Epoch, .Math nothing
Types Onchain.Aerodrome.Types.* nothing
Bindings Onchain.Aerodrome.Bindings.* an RPC endpoint
Analytics Onchain.Aerodrome.Analytics.* structs only — no network
Read API Onchain.Aerodrome.Sugar.* an RPC endpoint
Writes Onchain.Aerodrome.Write.* calldata by default; a signer only if you opt in

Analytics sits below the read API on purpose: APR, tick math and valuation take structs, not RPC options, so the whole analytics suite is testable with zero network access.

Quick start

iex> Onchain.Aerodrome.Contracts.address(:lp_sugar)
{:ok, "0x69dD9db6d8f8E7d83887A704f447b1a584b599A1"}
iex> Onchain.Aerodrome.Contracts.chain_id()
{:ok, 8453}
iex> Onchain.Aerodrome.Contracts.constants().max_lps
500
iex> Onchain.Aerodrome.Epoch.start(1_704_326_401)
1_704_326_400
iex> Onchain.Aerodrome.Epoch.epochs_per_year()
52

Runtime API discovery is provided by descripex:

OnchainAerodrome.describe() # module overview
OnchainAerodrome.describe(Onchain.Aerodrome.Bindings.LpSugar)

RPC requirements

Reads go through Sugar, a set of on-chain view contracts that batch protocol state into large structs. That makes the eth_call gas limit, not archive depth, the binding constraint.

Pagination has a trap

LpSugar.all/3 applies its _filter argument after fetching the page. With a non-zero filter, a full page returns fewer than limit rows while _offset still indexes the unfiltered space. Terminating the loop on a short page is a silent data-loss bug — it stops early and reports success. Drive offset to count() instead.

Hard per-call caps compiled into the contracts: MAX_LPS = 500, MAX_POSITIONS = 200, MAX_TOKENS = 2000.

APR denominator semantics

This library does not reproduce the numbers on aerodrome.finance, and that is deliberate. A single "APR" figure requires choosing one denominator and hiding it. This library reports the components with their denominators attached so the caller can decide.

If a figure here disagrees with the frontend, the answer is to state which denominator each uses — not to tune a constant until they match.

Address verification

Addresses live in lib/onchain/aerodrome/contracts.ex, each with a provenance comment. They are verified against two independent sources; a BaseScan label alone is never sufficient.

# 1. The Sugar team's own deployment manifest
gh api repos/velodrome-finance/sugar/contents/deployments/base.env --jq '.content' | base64 -d
# 2. A live probe confirming the contract answers as expected
cast call 0x69dD9db6d8f8E7d83887A704f447b1a584b599A1 "count()(uint256)" \
--rpc-url https://mainnet.base.org
cast call 0x69dD9db6d8f8E7d83887A704f447b1a584b599A1 "token_sugar()(address)" \
--rpc-url https://mainnet.base.org

Note that there are three Slipstream CL factories on Base, not one. Contracts.cl_factories/1 returns all of them; which are in scope for a given enumeration is an explicit decision.

ABIs

priv/abis/ holds ABIs captured from Sourcify v2, which serves the deployed ABI with named tuple components for field-count and field-order drift tests. Decode positionally with Onchain.RPC.eth_call/3, Onchain.ABI.decode_response/2, and hand-written from_raw/1 constructors. Bindings.Abi derives signatures from these captures. Avoid decode_structs: true, which raises on un-interned field atoms. priv/abis/README.md records the address, Sourcify match type, fetch date and exact curl for every file.

Sugar documentation drifts from the deployed contracts. Sugar's own readme.md documents LpSugar.all(limit, offset); the deployed contract is all(uint256,uint256,uint256) and the two-argument form reverts. The deployed ABI is the only authority. Re-capture and re-run the golden decode suite after any Sugar redeploy.

Development

mix deps.get
mix precommit # fast local loop
mix ci # full gate (= mix precommit.full)
mix test.json --quiet
mix test.json --quiet --include integration # requires Base RPC + BASE_SECONDARY_RPC_URL
mix aerodrome.capture_fixtures --block N # live Base RPC; not part of mix ci

BASE_RPC_URL is used when --rpc-url is omitted, falling back to https://mainnet.base.org. Portability integration tests also require BASE_SECONDARY_RPC_URL (Alchemy/Infura-class Base URL; no fallback). --block is required: a capture at latest is not a fixture. Re-running at the same block is byte-identical (captured_at is the block timestamp). For positive Position/Reward decode evidence, capture the separate nonempty/ collection with explicit selectors; either empty response fails before writing:

mix aerodrome.capture_fixtures --nonempty --block 51348944 \
--rpc-url https://mainnet.base.org \
--position-account 0x50f0249b824033cf0af0c8b9fe1c67c2842a34d5 \
--reward-venft-id 10 --reward-pool 0x42d4a22CaD0F5a49681a5715cE994Af73A43B76b

The original empty pagination fixtures remain in the main collection.

Golden-fixture decode tests require no network and are the primary defence against Sugar redeploy drift.

Calldata tests require Foundry cast on PATH (missing cast flunks). Install:

curl -L https://getfoundry.sh/install | bash
source ~/.bashrc # or source ~/.zshrc
foundryup

Onchain.Aerodrome.CalldataFixture.assert_calldata/3 compares library output against an independent cast calldata invocation; pass cast arguments as CLI strings. eth_call_as_sugar_owner/4 discovers a sender via VeSugar.byId and preserves simulated return bytes or RPC revert data. Its Voter.reset integration test requires both endpoints to serve historical block 51,348,944 and asserts the exact NotApprovedOrOwner() revert bytes for a non-owner, then decodes the simulated void return for the actual owner. See the helper's @doc for the limits of this evidence model.

License

MIT — see LICENSE.