Data & Analytics

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
  {"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
1
2
3
4
5
6
7
8
9
10
-- 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
-- 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
1
2
3
4
5
6
7
8
9
10
11
12
13
-- 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.

Did you find what you need?