Skip to content
Closed
Show file tree
Hide file tree
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
74 changes: 74 additions & 0 deletions dexs/virtus-protocol-cl/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { FetchOptions, FetchResult, SimpleAdapter } from "../../adapters/types";
import { CHAIN } from "../../helpers/chains";
import { addOneToken } from '../../helpers/prices';

const CONFIG = {
CLFactory: '0x0e5Ab24beBdA7e5Bb3961f7E9b3532a83aE86B48',
fromBlock: 42960000,
}

const swapEvent = 'event Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)'
const poolCreatedEvent = 'event PoolCreated(address indexed token0, address indexed token1, int24 indexed tickSpacing, address pool)'

const fetch = async (fetchOptions: FetchOptions): Promise<FetchResult> => {
const { api, createBalances, chain, getLogs } = fetchOptions
const dailyVolume = createBalances()
const dailyFees = createBalances()

const rawPools = await getLogs({
target: CONFIG.CLFactory,
fromBlock: CONFIG.fromBlock,
eventAbi: poolCreatedEvent,
cacheInCloud: true,
skipIndexer: true,
})

if (!rawPools?.length) return { dailyVolume, dailyFees }

const pools = rawPools.map((i: any) => i.pool.toLowerCase())
const fees = await api.multiCall({ abi: 'uint256:fee', calls: pools, permitFailure: true })

const poolTokens: Record<string, [string, string]> = {}
const poolFees: Record<string, number> = {}
const validPools: string[] = []

rawPools.forEach(({ token0, token1, pool }: any, index: number) => {
const p = pool.toLowerCase()
const rawFee = fees[index]
if (rawFee === null || rawFee === undefined) return;
poolTokens[p] = [token0, token1]
poolFees[p] = rawFee / 1e6
validPools.push(p)
})

if (!validPools.length) return { dailyVolume, dailyFees }

const allLogs = await getLogs({ targets: validPools, eventAbi: swapEvent, flatten: false })

allLogs.forEach((logs: any, index: number) => {
if (!logs?.length) return;
const pool = validPools[index]
const [token0, token1] = poolTokens[pool]
const fee = poolFees[pool]
logs.forEach((log: any) => {
const amount0 = Number(log.amount0)
const amount1 = Number(log.amount1)
addOneToken({ chain, balances: dailyVolume, token0, token1, amount0, amount1 })
addOneToken({ chain, balances: dailyFees, token0, token1, amount0: amount0 * fee, amount1: amount1 * fee })
})
})

return { dailyVolume, dailyFees, dailyRevenue: dailyFees, dailyHoldersRevenue: dailyFees }
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how fees are distributed in similar Aerodrome-fork adapters
rg -n -A5 "dailyRevenue.*dailyFees|dailyHoldersRevenue.*dailyFees" --type=ts dexs/ -g '!node_modules' | head -60

Repository: DefiLlama/dimension-adapters

Length of output: 3189


🌐 Web query:

What is the fee distribution mechanism for Aerodrome concentrated liquidity pools between LPs, veNFT holders, and protocol?

💡 Result:

The fee distribution mechanism for Aerodrome concentrated liquidity pools (Slipstream) follows the same model as standard pools, with a split between Liquidity Providers (LPs), veNFT (veAERO) holders, and protocol (minimal or none). Trading fees from swaps are generated based on pool fee tiers (e.g., 1bps to 0.3%) plus dynamic adjustments. - LPs earn fees directly proportional to their liquidity contribution if they do not stake their position NFT in the gauge. Staked LPs forgo swap fees to earn AERO emissions instead. - A portion of fees accrues to staked liquidity (gauged positions). These fees, along with an "unstaked liquidity fee" (default 10%, max 50%) skimmed from unstaked LP fees, are collected by the gauge via collectProtocol at the end of each epoch. These fees are then distributed to veAERO holders who voted for the pool in the previous epoch, proportional to their voting power. - Protocol receives no direct cut; 100% of collected fees flow to veAERO voters. No explicit protocol treasury retention is mentioned. This creates incentives: LPs choose fees (unstaked) or emissions (staked); voters earn fees by directing emissions to high-fee pools; emissions are separate AERO rewards to staked LPs.

Citations:


Fix double-counted revenue fields for Aerodrome-style fee distribution.

Setting both dailyRevenue and dailyHoldersRevenue to the full dailyFees value misrepresents the fee distribution. In Aerodrome concentrated liquidity (and VIRTUS if following the same model):

  • Protocol receives 0% of fees
  • veAERO holders receive 100% of collected fees

This same issue appears in dexs/velodrome-slipstream/index.ts:119 and dexs/virtus-protocol/index.ts:65.

Change to: dailyRevenue: 0, dailyHoldersRevenue: dailyFees (or verify if VIRTUS deviates from Aerodrome's model with an explicit protocol treasury cut).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dexs/virtus-protocol-cl/index.ts` at line 61, The return object currently
sets both dailyRevenue and dailyHoldersRevenue to dailyFees, double-counting
revenue; update the return to set dailyRevenue: 0 and dailyHoldersRevenue:
dailyFees so protocol revenue is zero and holders get 100% of fees (adjust the
same pattern wherever you find the same return with dailyVolume, dailyFees,
dailyRevenue, dailyHoldersRevenue).

}

const adapters: SimpleAdapter = {
version: 2,
adapter: {
[CHAIN.BASE]: {
fetch: fetch as any,
start: '2026-03-05',
}
}
}

export default adapters
78 changes: 78 additions & 0 deletions dexs/virtus-protocol/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { FetchOptions, FetchResult, SimpleAdapter } from "../../adapters/types";
import { CHAIN } from "../../helpers/chains";
import { addOneToken } from '../../helpers/prices';

const CONFIG = {
PoolFactory: '0x7F03ae4452192b0E280fB0d4f9c225DDa88C7623',
}

const swapEvent = 'event Swap(address indexed sender, address indexed to, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out)'

const factoryAbis = {
allPoolsLength: 'uint256:allPoolsLength',
allPools: 'function allPools(uint256) external view returns (address)',
getFee: 'function getFee(address pool, bool _stable) external view returns (uint256)',
}

const fetch = async (fetchOptions: FetchOptions): Promise<FetchResult> => {
const { createBalances, api, chain, getLogs } = fetchOptions
const dailyVolume = createBalances()
const dailyFees = createBalances()

const poolCount = await api.call({ target: CONFIG.PoolFactory, abi: factoryAbis.allPoolsLength })
if (!poolCount || Number(poolCount) === 0) return { dailyVolume, dailyFees }

const calls = []
for (let i = 0; i < Number(poolCount); i++) {
calls.push({ target: CONFIG.PoolFactory, params: [i] })
}
const pools: string[] = await api.multiCall({ abi: factoryAbis.allPools, calls })
const token0s: string[] = await api.multiCall({ abi: 'address:token0', calls: pools })
const token1s: string[] = await api.multiCall({ abi: 'address:token1', calls: pools })
const stables: boolean[] = await api.multiCall({ abi: 'bool:stable', calls: pools, permitFailure: true })

const fees = await api.multiCall({
abi: factoryAbis.getFee,
target: CONFIG.PoolFactory,
calls: pools.map((pool, i) => ({ params: [pool, stables[i] ?? false] })),
permitFailure: true,
})

const poolTokens: Record<string, [string, string]> = {}
const poolFees: Record<string, number> = {}
pools.forEach((pool, index) => {
const p = pool.toLowerCase()
poolTokens[p] = [token0s[index], token1s[index]]
poolFees[p] = (fees[index] ?? 30) / 1e4
})

const targets = pools.map(p => p.toLowerCase())
const allLogs = await getLogs({ targets, eventAbi: swapEvent, flatten: false })

allLogs.forEach((logs: any, index: number) => {
if (!logs?.length) return;
const pool = targets[index]
const [token0, token1] = poolTokens[pool]
const fee = poolFees[pool]
logs.forEach((log: any) => {
const amount0 = Number(log.amount0In) + Number(log.amount0Out)
const amount1 = Number(log.amount1In) + Number(log.amount1Out)
addOneToken({ chain, balances: dailyVolume, token0, token1, amount0, amount1 })
addOneToken({ chain, balances: dailyFees, token0, token1, amount0: amount0 * fee, amount1: amount1 * fee })
})
})

return { dailyVolume, dailyFees, dailyRevenue: dailyFees, dailyHoldersRevenue: dailyFees }
}

const adapters: SimpleAdapter = {
version: 2,
adapter: {
[CHAIN.BASE]: {
fetch: fetch as any,
start: '2026-03-05',
}
}
}

export default adapters
Loading