Repository navigation
Add BTR LP pools (Monad) #3051
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Add BTR LP pools (Monad) #3051
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| const sdk = require('@defillama/sdk'); | ||
| const utils = require('../utils'); | ||
|
|
||
| const PROJECT = 'btr'; | ||
| const CHAIN = 'monad'; | ||
| const DISPLAY_CHAIN = 'Monad'; | ||
| const FACTORY = '0xbbbbbbbbbd5e955E20F323cA1F95f8191fD0E0BF'; | ||
| const SECONDS_PER_DAY = 24 * 60 * 60; | ||
| const DAYS_PER_YEAR = 365; | ||
|
|
||
| // BTR DEX (AIMM) - adaptive inventory market maker. Each pool leg (base + spokes) has its own | ||
| // ERC-20 receipt (`Pool.lpToken(token)`). A leg's **liquid** economic balance for a leg is | ||
| // `Pool.getBuffer(token).reserves - .invested` (hook-invested tranches are pushed to external | ||
| // venues such as Euler/Aave, which DefiLlama lists separately; counting them here would double | ||
| // count them). A leg's LP fees accrue on the output side of swaps: the `Swapped` event packs | ||
| // `protoFee | lpFee << 128` in tokenOut units (source: dex-evm/src/interfaces/IPool.sol, | ||
| // dex-evm/src/libraries/PricingLib.sol). | ||
| // | ||
| // Only Monad (143) has a live pool factory; BNB (56) is a zeroed scaffold and Base/Arc have no | ||
| // pool factory record, so only monad is listed. | ||
| const SWAPPED = | ||
| 'event Swapped(address indexed sender, address indexed recipient, address indexed tokenIn, address tokenOut, uint256 amounts, uint256 fees, uint256 prices, uint256 outBook, uint256 outState)'; | ||
|
Comment on lines
+21
to
+22
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Match the deployed BTR’s published 🤖 Prompt for AI Agents |
||
|
|
||
| const abi = { | ||
| getOfficialPoolsCount: | ||
| 'function getOfficialPoolsCount() view returns (uint256)', | ||
| officialPools: 'function officialPools(uint256) view returns (address)', | ||
| getPoolTokens: 'function getPoolTokens(address pool) view returns (address[])', | ||
| getBuffer: | ||
| 'function getBuffer(address token) view returns (uint256 reserves, uint256 invested, uint256 minLiquidity)', | ||
| lpToken: 'function lpToken(address token) view returns (address)', | ||
| }; | ||
|
|
||
| const U128 = (1n << 128n) - 1n; | ||
|
|
||
| const toBigInt = (value) => BigInt(value.toString()); | ||
|
|
||
| const normalizeTimestamp = (timestamp) => | ||
| timestamp === null || timestamp === undefined | ||
| ? Math.floor(Date.now() / 1000) | ||
| : Number(timestamp); | ||
|
|
||
| const getBlocks = async (timestamp) => { | ||
| const [weekStartBlock, dayStartBlock, endBlock] = | ||
| await utils.getBlocksByTime( | ||
| [ | ||
| timestamp - 7 * SECONDS_PER_DAY, | ||
| timestamp - SECONDS_PER_DAY, | ||
| timestamp, | ||
| ], | ||
| CHAIN | ||
| ); | ||
| return { weekStartBlock, dayStartBlock, endBlock }; | ||
| }; | ||
|
|
||
| const getLegs = async () => { | ||
| const { output: count } = await sdk.api.abi.call({ | ||
| target: FACTORY, | ||
| abi: abi.getOfficialPoolsCount, | ||
| chain: CHAIN, | ||
| }); | ||
| if (!Number(count)) return []; | ||
| const { output: pools } = await sdk.api.abi.multiCall({ | ||
| target: FACTORY, | ||
| abi: abi.officialPools, | ||
| calls: Array.from({ length: Number(count) }, (_, i) => ({ params: [i] })), | ||
| chain: CHAIN, | ||
| }); | ||
| const poolAddresses = pools.map((p) => p.output); | ||
| const { output: tokenLists } = await sdk.api.abi.multiCall({ | ||
| target: FACTORY, | ||
| abi: abi.getPoolTokens, | ||
| calls: poolAddresses.map((pool) => ({ params: [pool] })), | ||
| chain: CHAIN, | ||
| }); | ||
|
|
||
| const legs = []; | ||
| tokenLists.forEach((list, i) => { | ||
| list.output.forEach((token) => | ||
| legs.push({ pool: poolAddresses[i], token }) | ||
| ); | ||
| }); | ||
|
|
||
| const { output: buffers } = await sdk.api.abi.multiCall({ | ||
| abi: abi.getBuffer, | ||
| calls: legs.map(({ pool, token }) => ({ target: pool, params: [token] })), | ||
| chain: CHAIN, | ||
| }); | ||
| const { output: lpTokens } = await sdk.api.abi.multiCall({ | ||
| abi: abi.lpToken, | ||
| calls: legs.map(({ pool, token }) => ({ target: pool, params: [token] })), | ||
| chain: CHAIN, | ||
| }); | ||
| legs.forEach((leg, i) => { | ||
| const buf = buffers[i].output; | ||
| leg.reserves = toBigInt(buf.reserves ?? buf[0]); | ||
| leg.invested = toBigInt(buf.invested ?? buf[1] ?? 0); | ||
| leg.lpToken = lpTokens[i].output; | ||
| }); | ||
| return legs; | ||
| }; | ||
|
|
||
| const getFeeRevenue = async (legs, fromBlock, toBlock) => { | ||
| const pools = [...new Set(legs.map((leg) => leg.pool))]; | ||
| if (!pools.length || fromBlock >= toBlock) return new Map(); | ||
| const logs = await sdk.getEventLogs({ | ||
| targets: pools, | ||
| eventAbi: SWAPPED, | ||
| fromBlock, | ||
| toBlock, | ||
| chain: CHAIN, | ||
| }); | ||
| // Key by emitting pool + output token: two official pools can share a base asset, and | ||
| // keying by token alone would attribute both pools' fees to every leg and overstate APY. | ||
| // | ||
| // Every fill counts, including BTR's own coop-arbitrage fills (CoopArb). A coop fill books a | ||
| // protocol fee of zero but still pays a non-zero LP fee, so it is real LP revenue; while coop | ||
| // arb was the only flow, excluding it left apyBase at zero. This matches the volume/fees | ||
| // adapter, which counts coop fills as well. | ||
| const accrued = new Map(); // `${pool}:${tokenOut}` (lowercase) -> BigInt lpFee in token units | ||
| for (const log of logs) { | ||
| const args = log.args ?? log; | ||
| const pool = String(log.address).toLowerCase(); | ||
| const tokenOut = String(args.tokenOut).toLowerCase(); | ||
| const lpFee = toBigInt(args.fees) >> 128n; | ||
| const key = `${pool}:${tokenOut}`; | ||
| accrued.set(key, (accrued.get(key) ?? 0n) + lpFee); | ||
| } | ||
| return accrued; | ||
| }; | ||
|
|
||
| const apy = async (timestampArg = null) => { | ||
| const timestamp = normalizeTimestamp(timestampArg); | ||
| const [legs, blocks] = await Promise.all([getLegs(), getBlocks(timestamp)]); | ||
| if (!legs.length) return []; | ||
|
|
||
| const tokens = [...new Set(legs.map((leg) => leg.token))]; | ||
| const { pricesByAddress } = await utils.getPrices(tokens, CHAIN); | ||
| const { output: meta } = await sdk.api.abi.multiCall({ | ||
| abi: 'erc20:symbol', | ||
| calls: tokens.map((token) => ({ target: token })), | ||
| chain: CHAIN, | ||
| }); | ||
| const { output: supplies } = await sdk.api.abi.multiCall({ | ||
| abi: 'erc20:totalSupply', | ||
| calls: legs.map((leg) => ({ target: leg.lpToken })), | ||
| chain: CHAIN, | ||
| }); | ||
| const { output: decimals } = await sdk.api.abi.multiCall({ | ||
| abi: 'erc20:decimals', | ||
| calls: tokens.map((token) => ({ target: token })), | ||
| chain: CHAIN, | ||
| }); | ||
| const symbolByToken = {}; | ||
| tokens.forEach((token, i) => { | ||
| symbolByToken[token.toLowerCase()] = meta[i].output; | ||
| }); | ||
| const decimalsByToken = {}; | ||
| tokens.forEach((token, i) => { | ||
| decimalsByToken[token.toLowerCase()] = Number(decimals[i].output); | ||
| }); | ||
|
|
||
| const [dayFees, weekFees] = await Promise.all([ | ||
| getFeeRevenue(legs, blocks.dayStartBlock, blocks.endBlock), | ||
| getFeeRevenue(legs, blocks.weekStartBlock, blocks.endBlock), | ||
| ]); | ||
|
|
||
| const pools = []; | ||
| legs.forEach((leg, i) => { | ||
| const token = leg.token.toLowerCase(); | ||
| const price = pricesByAddress[token]; | ||
| if (!Number.isFinite(price)) return; // skip unpriced legs rather than emit a wrong TVL | ||
| const decimalsForLeg = decimalsByToken[token]; | ||
| const scale = 10 ** decimalsForLeg; | ||
| // Liquid reserves only: hook-invested tranches are in Euler/Aave, which DefiLlama lists | ||
| // separately, so counting them here would double count (matches the TVL adapter). | ||
| const liquidReserves = leg.reserves - leg.invested; | ||
| const reserveAmount = Number(liquidReserves) / scale; | ||
| const tvlUsd = reserveAmount * price; | ||
| if (!Number.isFinite(tvlUsd) || tvlUsd <= 0) return; | ||
| const feeKey = `${leg.pool.toLowerCase()}:${token}`; | ||
| const dayFeeUsd = | ||
| (Number(dayFees.get(feeKey) ?? 0n) / scale) * price; | ||
| const weekFeeUsd = | ||
| (Number(weekFees.get(feeKey) ?? 0n) / scale) * price; | ||
|
|
||
| pools.push({ | ||
| pool: `${leg.lpToken.toLowerCase()}-${CHAIN}`, | ||
| chain: DISPLAY_CHAIN, | ||
| project: PROJECT, | ||
| symbol: symbolByToken[token], | ||
| tvlUsd, | ||
| apyBase: (dayFeeUsd / tvlUsd) * DAYS_PER_YEAR * 100, | ||
| apyBase7d: (weekFeeUsd / tvlUsd) * (DAYS_PER_YEAR / 7) * 100, | ||
| underlyingTokens: [leg.token], | ||
| token: leg.lpToken, | ||
| url: 'https://btr.markets', | ||
| }); | ||
| }); | ||
|
|
||
| return pools.filter((pool) => utils.keepFinite(pool)); | ||
| }; | ||
|
|
||
| module.exports = { | ||
| timetravel: false, | ||
| protocolId: '9073', // DefiLlama protocol id for slug `btr` | ||
| apy, | ||
| url: 'https://btr.markets', | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Use the deployed Monad PoolFactory address.
FACTORYdiffers from BTR’s published Monad PoolFactory address,0xbBBbBBBb323D7f7E51976b8A8DF2Bb4B4608d9B3. The first call ingetLegs()therefore targets the wrong contract and cannot discover official pools. Replace this constant with the deployment address. (btr.markets)🤖 Prompt for AI Agents