How to attribute volume and monitor strategies from on-chain data. Every field below is decoded from event data; see Events & interfaces for the exact signatures.
Strategy identity
A strategy is keyed by the tuple (chainId, maker, app, strategyHash), where strategyHash = keccak256(strategy) (the strategy bytes are already abi.encode(order), so no second encode) and is immutable after ship(). The router's Swapped.orderHash equals the strategy's strategyHash, which is the join key between fills and strategies.
Volume attribution
Attribute a fill to a strategy by joining Swapped.orderHash == Shipped.strategyHash. When aggregating across the aggregation router and Aqua, watch for double counting: a single user swap routed through Aqua produces one Aqua Swapped and may also appear in aggregation-layer accounting — attribute to one layer per methodology and state which.
Derived-metric methodology
Every derived number (volume, TVL-equivalent, APY, SLR) is an estimate from public data, not an official 1inch figure. Publish a method block with it: the formula, the price source, the price-as-of timestamp, and the methodology version.
Reference indexer
A hosted subgraph is not currently available. Build a reference indexer over the five registry/router events keyed on (maker, app, strategyHash). Backfill from each chain's deployment block (deployment-specific; read it from the contract's creation transaction) and reconcile poll vs subscribe as described on the Events page.
Event ABI (copy-paste)
The five events, source-verified (no indexed params). See Events & interfaces for topic0 hashes.
Solidity
123456789101112131415161718
[
{"type":"event","name":"Shipped","anonymous":false,"inputs":[
{"name":"maker","type":"address"},{"name":"app","type":"address"},
{"name":"strategyHash","type":"bytes32"},{"name":"strategy","type":"bytes"}]},
{"type":"event","name":"Docked","anonymous":false,"inputs":[
{"name":"maker","type":"address"},{"name":"app","type":"address"},
{"name":"strategyHash","type":"bytes32"}]},
{"type":"event","name":"Pulled","anonymous":false,"inputs":[
{"name":"maker","type":"address"},{"name":"app","type":"address"},
{"name":"strategyHash","type":"bytes32"},{"name":"token","type":"address"},{"name":"amount","type":"uint256"}]},
{"type":"event","name":"Pushed","anonymous":false,"inputs":[
{"name":"maker","type":"address"},{"name":"app","type":"address"},
{"name":"strategyHash","type":"bytes32"},{"name":"token","type":"address"},{"name":"amount","type":"uint256"}]},
{"type":"event","name":"Swapped","anonymous":false,"inputs":[
{"name":"orderHash","type":"bytes32"},{"name":"maker","type":"address"},{"name":"taker","type":"address"},
{"name":"tokenIn","type":"address"},{"name":"tokenOut","type":"address"},
{"name":"amountIn","type":"uint256"},{"name":"amountOut","type":"uint256"}]}
]
Decode example (Dune / ethers)
TypeScript
12345678910
-- Dune: Aqua volume by strategy (join fills to strategies on orderHash == strategyHash)
select s.maker, sw.token_in, sw.token_out,
sum(sw.amount_out) as out_volume, count(*) as fills
from aqua_router_evt_Swapped sw
join aqua_registry_evt_Shipped s on sw.orderHash = s.strategyHash
group by 1,2,3;
// ethers: decode a Swapped log
const iface = new ethers.Interface(AQUA_EVENT_ABI);
const { args } = iface.parseLog(log); // orderHash === strategyHash
Indexer completeness
Three details decide whether a backfill is complete and correctly attributed: the per-chain start block, log classification, and how the join key is derived. The Events & interfaces page has the signatures and topic0 hashes; Encoding has the exact strategy byte layout referenced below.
Per-chain deploy block
Registry events (Shipped, Docked, Pulled, Pushed) are emitted by the AquaRouter registry, a different contract from the SwapVM router that emits Swapped. The two contracts are deployed independently and land at different blocks on every chain. Your eth_getLogs fromBlock for registry events must be the registry's own creation block.
Do not reuse the swap-vm broadcast artifacts for the registry start block. The broadcast/**/run-latest.json files in the swap-vm repo record the SwapVM router deployment, not the AquaRouter registry. Read the registry's deploy block per chain from its own contract-creation transaction (the block of the tx that created the registry address), then use that as fromBlock. Using the router's block will silently skip or over-scan registry logs.
Pin one (chainId → registry address → creation block) row per chain in config and treat it as the backfill floor. Note that ship() emits a Shipped plus one Pushed per seeded token in the same transaction, so the earliest registry activity for a maker is always at or after that floor.
Log classification by contract and topic0
None of the five events declare indexed parameters, so every log carries exactly one topic — topic0, the event-signature hash — and all payload fields live in data. Classify a raw log on two dimensions:
| Dimension | Registry logs | Router logs |
|---|---|---|
Emitting contract (log.address) |
AquaRouter registry address | SwapVM router address |
topic0 |
Shipped / Docked / Pulled / Pushed signature hash | Swapped signature hash |
| Topic count | 1 (no indexed params) | 1 (no indexed params) |
Match on (log.address, topic0) together. The address alone separates registry from router; topic0 alone separates the four registry event types from each other. Once matched, ABI-decode the non-indexed tuple from data to recover the fields.
Join key. strategyHash = keccak256(strategy) — the keccak256 of the exact strategy bytes passed to ship() (verified at aqua src/Aqua.sol, not keccak256(abi.encode(strategy))). It is immutable after ship and is the strategyHash in every registry event. The router's Swapped.orderHash equals this value for Aqua orders — see Encoding for why the shipped bytes and the order encoding coincide — which is the fills-to-strategies join.
Warehouse schema sketch
A minimal two-table schema keyed on (chainId, maker, app, strategyHash). Store addresses and hashes as fixed-width binary; keep strategy raw so strategyHash is reproducible offline.
Solidity
123456789101112131415161718192021222324252627282930313233343536373839
-- Strategies: one row per shipped strategy (keyed by identity tuple)
CREATE TABLE strategies (
chain_id BIGINT NOT NULL,
maker BYTEA NOT NULL, -- 20 bytes
app BYTEA NOT NULL, -- 20 bytes
strategy_hash BYTEA NOT NULL, -- 32 bytes = keccak256(strategy)
strategy BYTEA NOT NULL, -- raw shipped bytes (Shipped.strategy)
status TEXT NOT NULL DEFAULT 'active', -- 'active' | 'docked'
shipped_block BIGINT NOT NULL,
shipped_tx BYTEA NOT NULL,
shipped_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (chain_id, maker, app, strategy_hash)
);
-- Fills: one row per Swapped log (orderHash = strategy_hash)
CREATE TABLE fills (
chain_id BIGINT NOT NULL,
order_hash BYTEA NOT NULL, -- 32 bytes; joins strategies.strategy_hash
maker BYTEA NOT NULL,
taker BYTEA NOT NULL,
token_in BYTEA NOT NULL,
token_out BYTEA NOT NULL,
amount_in NUMERIC(78,0) NOT NULL, -- uint256
amount_out NUMERIC(78,0) NOT NULL, -- uint256
block_number BIGINT NOT NULL,
tx_hash BYTEA NOT NULL,
log_index INTEGER NOT NULL,
PRIMARY KEY (chain_id, tx_hash, log_index)
);
CREATE INDEX fills_by_strategy ON fills (chain_id, order_hash);
-- Attribute fills to strategies
SELECT s.maker, s.app, f.token_in, f.token_out,
SUM(f.amount_out) AS out_volume, COUNT(*) AS n_fills
FROM fills f
JOIN strategies s
ON s.chain_id = f.chain_id
AND s.strategy_hash = f.order_hash
GROUP BY 1,2,3,4;
Apply Docked to flip status, and fold Pulled/Pushed into a per-(chainId, maker, app, strategyHash, token) running balance if you track inventory. The identity tuple is stable across all of them.
Dune caveat
Because no parameter is indexed, Dune's decoded evt_* tables and any manual decode must pull every field from data; there are no topic1..topic3 columns to filter on. Select rows by topic0 (the event-signature hash) against logs, and scope by the emitting contract address to keep registry and router logs apart — topic0 plus contract_address, never a topic-indexed field. When decoding by hand, remember dynamic bytes (Shipped.strategy) is offset-encoded within data, not a fixed slot.
Cross-layer volume de-dup (exact rule)
The Volume attribution note above tells you double counting can happen; this section replaces that directional guidance with an exact rule. A single user swap that the 1inch aggregation router lands on Aqua produces one Aqua Swapped log and one aggregation-layer entry, both in the same transaction. Counting both inflates any combined total by exactly the overlapping set. Pick one of the two equivalent rules below and state which you used in the method block.
The overlap is identifiable two ways, and they agree. The aggregation-routed fills are exactly the Swapped rows whose taker is a permitted resolver and whose (chain_id, tx_hash) matches an aggregation-router transaction. Swapped.taker is msg.sender — the address that called the SwapVM router (verified at swap-vm src/SwapVM.sol, emit Swapped(orderHash, order.maker, msg.sender, ...)). For an aggregation-routed swap that caller is the resolver contract; for a direct Aqua swap it is an ordinary taker.
Rule A — union and de-dup on (chain_id, tx_hash)
Build the combined figure as the union of your aggregation source and Aqua fills, then collapse to one row per (chain_id, tx_hash) so a swap that traversed both layers is counted once. Choose the surviving layer deterministically and record the choice.
-- Combined 1inch volume, each swap counted exactly once
WITH agg AS ( -- your aggregation-router source, priced
SELECT chain_id, tx_hash, amount_usd, 'aggregation' AS layer
FROM aggregation_fills
),
aqua AS ( -- Aqua Swapped fills, priced to the same numeraire
SELECT f.chain_id, f.tx_hash,
(f.amount_in / p.scale) * p.price AS amount_usd,
'aqua' AS layer
FROM fills f
JOIN prices p ON p.token = f.token_in AND p.as_of = :price_as_of
),
unified AS (
SELECT * FROM agg
UNION ALL
SELECT * FROM aqua
)
SELECT SUM(amount_usd) AS combined_volume_usd
FROM (
SELECT DISTINCT ON (chain_id, tx_hash) chain_id, tx_hash, amount_usd
FROM unified
ORDER BY chain_id, tx_hash, layer -- deterministic: 'aggregation' wins ties
) deduped;
Rule B — scope by permitted resolver
Equivalently, reconcile on the Aqua side alone: the resolver-executed fills are the aggregation-routed overlap, and they match the aggregation router's own transaction 1:1 on (chain_id, tx_hash). Attribute that resolver-taker set to exactly one layer; the remaining non-resolver takers are direct Aqua flow and are never in the aggregation source, so they cannot be double counted.
Solidity
12345678910111213
-- Aggregation-routed Aqua volume (the reconcilable, resolver-executed set)
SELECT SUM(f.amount_in) AS routed_volume_raw -- raw uint256 units; price outside SQL
FROM fills f
JOIN permitted_resolvers r
ON r.chain_id = f.chain_id
AND r.resolver = f.taker; -- Swapped.taker = msg.sender
-- Direct (non-resolver) Aqua volume, attributed separately
SELECT SUM(f.amount_in) AS direct_volume_raw
FROM fills f
LEFT JOIN permitted_resolvers r
ON r.chain_id = f.chain_id AND r.resolver = f.taker
WHERE r.resolver IS NULL;
Swapped.taker is msg.sender, the immediate caller of the SwapVM router. If a resolver executes through an intermediary contract, msg.sender is that intermediary, not the resolver EOA — add a tx.origin check against your resolver allowlist for those cases. Maintain the permitted_resolvers set per chain_id; a stale allowlist silently misclassifies routed fills as direct and reopens the double count.
Shared Liquidity Ratio (SLR): worked formula
SLR measures capital reuse — how much swap flow a strategy's committed, on-chain-available inventory supported over a window. It is a turnover/availability figure, not leverage and not yield: the denominator is the maker's own committed balance (no borrowed funds), and the ratio says nothing about return. Like every number on this page it is an estimate from public data; publish the method block with it.
Inputs
| Symbol | Meaning | Source |
|---|---|---|
V |
Swap flow routed by the strategy over the window, in a common numeraire | Swapped.amountIn (fills), priced at price_as_of |
C |
Committed, available inventory of the strategy, same numeraire | rawBalances(maker, app, strategyHash, token) → uint248 balance, priced at price_as_of |
SLR |
V / C — units of flow served per unit of committed capital |
derived |
rawBalances is the on-chain view rawBalances(address maker, address app, bytes32 strategyHash, address token) returns (uint248 balance, uint8 tokensCount) (verified at aqua src/Aqua.sol). Snapshot balance at the price-as-of block, or reconstruct the identical value offline by folding Pushed minus Pulled deltas per (chainId, maker, app, strategyHash, token). Both balance and amountIn are raw integer token units — divide by 10^decimals before pricing.
Arithmetic
SLR = V / C
V = sum over fills in [from_block, to_block] of (amountIn / 10^dec_in) * price(tokenIn)
C = sum over strategy tokens of (rawBalances.balance / 10^dec) * price(token)
-- SLR per strategy, mirroring the volume-attribution join (order_hash = strategy_hash)
WITH vol AS ( -- V: window flow, priced to numeraire
SELECT f.chain_id, s.maker, s.app, f.order_hash AS strategy_hash,
SUM((f.amount_in / p.scale) * p.price) AS v_usd
FROM fills f
JOIN strategies s
ON s.chain_id = f.chain_id
AND s.strategy_hash = f.order_hash
JOIN prices p
ON p.token = f.token_in AND p.as_of = :price_as_of
WHERE f.block_number BETWEEN :from_block AND :to_block
GROUP BY 1,2,3,4
),
cap AS ( -- C: committed inventory from rawBalances(), priced
SELECT b.chain_id, b.maker, b.app, b.strategy_hash,
SUM((b.balance / p.scale) * p.price) AS c_usd
FROM raw_balances b -- one row per (chain,maker,app,strategy_hash,token)
JOIN prices p
ON p.token = b.token AND p.as_of = :price_as_of
GROUP BY 1,2,3,4
)
SELECT v.chain_id, v.maker, v.app, v.strategy_hash,
v.v_usd, c.c_usd,
v.v_usd / NULLIF(c.c_usd, 0) AS slr
FROM vol v
JOIN cap c USING (chain_id, maker, app, strategy_hash);
Worked sample
Strategy committed inventory (rawBalances at price_as_of):
USDC 1,000,000 (6 dec) x $1.00 = $1,000,000
WETH 300 (18 dec) x $3,000.00 = $ 900,000
C = $1,900,000
7-day window flow (sum of amountIn, priced):
V = $9,500,000
SLR = V / C = 9,500,000 / 1,900,000 = 5.0
Read: over the window, each unit of committed, available inventory supported 5 units of swap flow — a capital-reuse measure. It does not imply borrowed capital (no leverage) or any return (no yield).
Method block (publish with the number). Formula SLR = V / C, methodology version; window [from_block, to_block] and its UTC timestamps; V from Swapped.amountIn, C from rawBalances().balance snapshotted at price_as_of; price source and the price_as_of timestamp; the numeraire. State whether cross-layer de-dup (Rule A or Rule B above) was applied to V. As with all figures here, this is an estimate from public data, not an official 1inch number.