Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 70 additions & 6 deletions src/adaptors/aerodrome-slipstream/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,47 @@ const SUBGRAPH = sdk.graph.modifyEndpoint(

const tickWidthMappings = { 1: 5, 50: 5, 100: 15, 200: 10, 2000: 2 };

const gaugeFeesAbi = {
name: 'gaugeFees',
type: 'function',
stateMutability: 'view',
inputs: [],
outputs: [
{ name: 'token0', type: 'uint128' },
{ name: 'token1', type: 'uint128' },
],
};

// Same counters sugar.all exposes, read per-pool. Used for pools the
// subgraph doesn't index (newer CL factories) — three multicalls over
// ~100 addresses instead of paginating ~12k sugar entries per block.
async function fetchPoolFeesTargeted(blockNumber, lps) {
const calls = lps.map((target) => ({ target }));
const opts = { calls, chain: CHAIN, block: blockNumber, permitFailure: true };
const [feesRes, liqRes, stakedRes] = await Promise.all([
sdk.api.abi.multiCall({ abi: gaugeFeesAbi, ...opts }),
sdk.api.abi.multiCall({ abi: 'uint128:liquidity', ...opts }),
sdk.api.abi.multiCall({ abi: 'uint128:stakedLiquidity', ...opts }),
Comment on lines +40 to +42

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

"stakedLiquidity" "Aerodrome" slipstream pool

💡 Result:

Slipstream is Aerodrome Finance's concentrated liquidity implementation on the Base network, designed to function similarly to Uniswap V3 [1][2][3]. In Slipstream pools, liquidity providers (LPs) choose specific price ranges for their deposits, which concentrates capital and improves fee efficiency within those ranges [2][4][5]. Staking liquidity in Slipstream pools is a mandatory step to maximize rewards [5]. Because Slipstream positions are represented as non-fungible tokens (NFTs) rather than fungible LP tokens, LPs must stake their NFT position into the corresponding pool gauge [2][4]. Key aspects of staking for Slipstream pools include: Mandatory Staking: Unstaked positions do not receive AERO emissions and result in the protocol withholding a portion (approximately 10%) of earned swap fees, which are then distributed to veAERO voters [4][5]. Mechanism: After depositing liquidity and receiving an NFT representing that position, users must stake the NFT via the Aerodrome interface [2][4][5]. Rewards: Staked positions are eligible for a "triple income stream": Trading fees accrued while the position remains within the active price range [2][4][5]. AERO emissions distributed through the gauge [4][5]. Additional rewards (bribes) from veAERO voters [5]. Management: Because Slipstream pools concentrate liquidity, positions require active monitoring [2]. If the market price exits the selected range, the position stops earning swap fees until the price returns to that range or the LP adjusts it [2][4][5]. For more technical details, including integration interfaces and contract addresses, the official Aerodrome Finance GitHub repository maintains the specification and core contracts for Slipstream [1][6][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant file and any nearby references to stakedLiquidity / liquidity fields.
git ls-files src/adaptors/aerodrome-slipstream index.js src/adaptors/aerodrome-slipstream
printf '\n--- index.js outline ---\n'
ast-grep outline src/adaptors/aerodrome-slipstream/index.js --view expanded || true
printf '\n--- occurrences ---\n'
rg -n "stakedLiquidity|liquidity|gaugeFeesAbi|multiCall" src/adaptors/aerodrome-slipstream src/adaptors -g '!**/node_modules/**' || true
printf '\n--- file excerpt ---\n'
cat -n src/adaptors/aerodrome-slipstream/index.js | sed -n '1,160p'

Repository: DefiLlama/yield-server

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=src/adaptors/aerodrome-slipstream/index.js
printf '%s\n' "--- ${file} (1-180) ---"
cat -n "$file" | sed -n '1,180p'

printf '\n--- direct matches in %s ---\n' "$file"
rg -n "stakedLiquidity|liquidity|gaugeFeesAbi|pool|gauge" "$file" || true

printf '\n--- nearby adaptor files mentioning aerodrome-slipstream ---\n'
rg -n "aerodrome-slipstream|stakedLiquidity" src/adaptors -g '!**/node_modules/**' | sed -n '1,120p' || true

Repository: DefiLlama/yield-server

Length of output: 12564


🌐 Web query:

"Aerodrome Slipstream" stakedLiquidity function pool ABI

💡 Result:

In Aerodrome Slipstream, there is no single function named stakedLiquidity defined in the core pool contract ABI [1][2]. Aerodrome Slipstream is a concentrated liquidity AMM (based on the Uniswap V3 architecture) where liquidity is managed via non-fungible position tokens (ERC-721 NFTs) [1][2][3]. The concept of "staked liquidity" refers to these NFT positions being deposited into a Gauge contract to earn AERO emissions [2]. To retrieve information related to staked liquidity, you must interact with the relevant Gauge contract and the NonfungiblePositionManager, rather than a single pool function [2]: 1. To check if a position is staked: You typically interact with the Gauge contract, which handles the staking of position NFTs [2]. 2. To calculate or view liquidity amounts: You query the NonfungiblePositionManager's positions(uint256 tokenId) function to get the position details, including the liquidity amount [4][5]. 3. For APR/Reward calculations: These are often derived by aggregating total staked liquidity within the gauge, which is documented at the pool/gauge level [6][7]. Because Slipstream follows the Concentrated Liquidity standard, the ABI for the pool itself exposes standard functions such as mint, burn, collect, and position management [1], while staking logic is externalized to the Gauge system [2]. If you are looking for specific data (like total staked liquidity), you should query the Gauge contract's view functions or use a protocol-specific subgraph/indexer that tracks gauge deposits [2][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=src/adaptors/aerodrome-slipstream/index.js

printf '%s\n' '--- pool/gauge target wiring around targeted fees ---'
sed -n '300,460p' "$file" | cat -n

printf '\n%s\n' '--- where fetchPoolFeesTargeted is called ---'
rg -n "fetchPoolFeesTargeted|fetchPoolFeesAtBlock|gaugeFees|stakedLiquidity" "$file" src/adaptors/aerodrome-slipstream -g '!**/node_modules/**'

printf '\n%s\n' '--- any ABI references for stakedLiquidity in repo ---'
rg -n '"stakedLiquidity"|stakedLiquidity\(' src/adaptors -g '!**/node_modules/**' | sed -n '1,120p' || true

Repository: DefiLlama/yield-server

Length of output: 8217


Use the gauge for stakedLiquidity
stakedLiquidity() isn’t part of the Slipstream pool ABI; query the gauge-side contract/helper here instead, or the fallback path will skip these pools.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adaptors/aerodrome-slipstream/index.js` around lines 40 - 42, Update the
stakedLiquidity multiCall in the Slipstream adaptor to query the gauge-side
contract/helper rather than the pool ABI, while keeping the existing liquidity
and gaugeFees calls unchanged so pools with staked liquidity are not skipped by
the fallback path.

]);
const fees = {};
lps.forEach((lp, i) => {
const f = feesRes.output[i];
const l = liqRes.output[i];
const s = stakedRes.output[i];
if (!f?.success || !l?.success || !s?.success || f.output == null) return;
// sugar zeroes gauge fees when nothing is staked (stale unclaimed
// counters would otherwise be misattributed to the current epoch)
const hasStaked = Number(s.output) > 0;
fees[lp] = {
token0_fees: hasStaked ? f.output.token0 ?? f.output[0] : '0',
token1_fees: hasStaked ? f.output.token1 ?? f.output[1] : '0',
liquidity: l.output,
gauge_liquidity: s.output,
};
});
return fees;
}

// Fetch gauge fees for all CL pools at a historical block, keyed by lp address.
// Reuses the shared pagination helper defined below.
async function fetchPoolFeesAtBlock(blockNumber) {
Expand Down Expand Up @@ -260,7 +301,10 @@ async function fetchTokenMetadata(addresses) {
return map;
}

const getGaugeApy = async ({ skipHistoricalFees = false } = {}) => {
const getGaugeApy = async ({
skipHistoricalFees = false,
subgraphCoveredPools = null,
} = {}) => {
const allPoolsRaw = await paginatePools();
const allPoolsData = allPoolsRaw.filter(
(t) => Number(t.type) > 0 && t.gauge != nullAddress
Expand Down Expand Up @@ -368,14 +412,29 @@ const getGaugeApy = async ({ skipHistoricalFees = false } = {}) => {
if (out?.success && out.output) allStakedData[activeIdx[j]] = out.output;
}

// archive-state fee fallback; skipped when subgraph is healthy
// on-chain fee snapshots: full sugar pagination when the subgraph is
// unhealthy, targeted per-pool multicalls for pools it doesn't index
const now = Math.floor(Date.now() / 1000);
const epochStart = Math.floor(now / WEEK) * WEEK;
const elapsedSeconds = now - epochStart;

let prevEpochFees = {};
let fees24hAgo = null;
let fetchFeesAtBlock = null;
if (!skipHistoricalFees) {
fetchFeesAtBlock = fetchPoolFeesAtBlock;
} else if (subgraphCoveredPools) {
const missingLps = allPoolsData
.map((p) => p.lp.toLowerCase())
.filter((lp) => !subgraphCoveredPools.has(lp));
if (missingLps.length) {
console.log(
`aerodrome-slipstream: targeted fee snapshots for ${missingLps.length} pools missing from subgraph`
);
fetchFeesAtBlock = (block) => fetchPoolFeesTargeted(block, missingLps);
}
}
if (fetchFeesAtBlock) {
// bounded timeout so a stuck archive RPC can't eat the 900s budget
const HISTORICAL_FETCH_TIMEOUT_MS = 180_000;
const withTimeout = (promise, ms, label) => {
Expand All @@ -397,15 +456,15 @@ const getGaugeApy = async ({ skipHistoricalFees = false } = {}) => {
const historicalBlocks = await utils.getBlocksByTime(timestamps, CHAIN);
const fetches = [
withTimeout(
fetchPoolFeesAtBlock(historicalBlocks[0]),
fetchFeesAtBlock(historicalBlocks[0]),
HISTORICAL_FETCH_TIMEOUT_MS,
'prev-epoch fee snapshot'
),
];
if (elapsedSeconds > 86400) {
fetches.push(
withTimeout(
fetchPoolFeesAtBlock(historicalBlocks[1]),
fetchFeesAtBlock(historicalBlocks[1]),
HISTORICAL_FETCH_TIMEOUT_MS,
'24h fee snapshot'
)
Expand Down Expand Up @@ -516,7 +575,7 @@ const getGaugeApy = async ({ skipHistoricalFees = false } = {}) => {
apyBase = ((totalFeeDelta * 365) / tvlUsd) * 100;
if (p.pool_fee > 0) volumeUsd1d = totalFeeDelta / (p.pool_fee / 1e6);
}
} else if (!skipHistoricalFees && prev && elapsedSeconds > 6 * 3600) {
} else if (prev && elapsedSeconds > 6 * 3600) {
// <24h into epoch: extrapolate. Needs `prev` to subtract pre-distribute
// residue from cumulative gauge fees; without it we'd overestimate APY.
const currentFeeUsd = calcFeeUsd(p);
Expand Down Expand Up @@ -598,7 +657,12 @@ async function main(timestamp = null) {
`aerodrome-slipstream: subgraph unhealthy (${reasons.join(', ')}); falling back to archive-state path`
);
}
const poolsApy = await getGaugeApy({ skipHistoricalFees: subgraphHealthy });
const poolsApy = await getGaugeApy({
skipHistoricalFees: subgraphHealthy,
subgraphCoveredPools: subgraphHealthy
? new Set(Object.keys(poolsVolumes))
: null,
});

// left-join volumes onto APY output to avoid filtering out pools
return Object.values(poolsApy).map((pool) => {
Expand Down
Loading