From 096a817fb89e902c7f07b8a7a2c27d4593d6e8e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20Emin=20Erdo=C4=9Fdu?= Date: Thu, 14 May 2026 13:46:16 +0300 Subject: [PATCH 1/3] feat: add Brix wiTRY yield adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a yield adapter for wiTRY, the yield-bearing staking wrapper for Brix's iTRY (a TRY-pegged stablecoin backed by Turkish money market funds). APY is computed entirely on-chain, anchored to actual yield-distribution events rather than fixed-window block sampling: 1. Collect RewardsReceived events from the wiTRY vault (last 21d) 2. end_event = latest distribution start_event = latest event with ts <= end_event.ts - 7d 3. Sample convertToAssets at (event.ts + getVestingPeriod()) so each anchor event's distribution is fully reflected — vesting-aware 4. elapsedDays dynamic (typically 7-9) 5. APY = (rateEnd / rateStart) ^ (365 / elapsedDays) - 1 This is robust to multi-day distribution gaps (Turkish public holidays, weekends) — both anchors freeze symmetrically when no new distribution arrives, so the displayed APY stays flat through dry periods and the catch-up distribution at the end of a gap is absorbed without a spike. The 10% performance fee is deducted by YieldForwarder before yield is streamed into the wiTRY vault, so convertToAssets growth already reflects net yield — no fee multiplier needed. Co-Authored-By: Claude Opus 4.7 --- src/adaptors/brix/index.js | 149 +++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 src/adaptors/brix/index.js diff --git a/src/adaptors/brix/index.js b/src/adaptors/brix/index.js new file mode 100644 index 0000000000..4a82192fcb --- /dev/null +++ b/src/adaptors/brix/index.js @@ -0,0 +1,149 @@ +const sdk = require('@defillama/sdk'); +const axios = require('axios'); + +const ITRY = '0xb492B4aFD9658093694CF9452D5C272e8230F3B0'; +const WITRY = '0xE346C29b5B60Ef870b9724c57ccfbBc631e47DEE'; + +const LOOKBACK_DAYS = 7; +const SCAN_DAYS = 21; +const SECONDS_PER_DAY = 86400; +const DAYS_PER_YEAR = 365; +const ONE_WITRY = '1000000000000000000'; + +const abiConvertToAssets = + 'function convertToAssets(uint256 shares) view returns (uint256)'; +const abiTotalAssets = 'uint256:totalAssets'; +const abiGetVestingPeriod = 'function getVestingPeriod() view returns (uint256)'; +const eventRewardsReceived = 'event RewardsReceived(uint256 amount)'; + +async function getRateAtBlock(block) { + const { output } = await sdk.api.abi.call({ + target: WITRY, + abi: abiConvertToAssets, + params: [ONE_WITRY], + chain: 'ethereum', + block, + }); + return Number(output) / 1e18; +} + +// Distribution-anchored APY. +// +// Both endpoints of the lookback are anchored to actual on-chain +// RewardsReceived events. When no distribution arrives (Turkish holidays, +// weekends, bayrams) both anchors freeze → APY stays flat. The catch-up +// distribution at the end of a gap is absorbed symmetrically by both +// anchors → no spike. +// +// Vesting-aware sampling: wiTRY follows the Ethena V2 pattern where +// RewardsReceived adds to vestingAmount and unlocks linearly over +// getVestingPeriod() seconds (currently 1h). totalAssets() subtracts the +// unvested amount, so convertToAssets() at the event's own block reflects +// the pre-distribution state. To capture the distribution's effect we +// sample the rate at event.ts + vestingPeriod (clamped to latest block). +const apy = async () => { + const latest = await sdk.api.util.getLatestBlock('ethereum'); + + // Query vestingPeriod from the contract — don't hardcode. Wired up + // because the VestingPeriodUpdated admin event implies this is mutable. + const { output: vestingPeriodRaw } = await sdk.api.abi.call({ + target: WITRY, + abi: abiGetVestingPeriod, + chain: 'ethereum', + }); + const vestingPeriod = Number(vestingPeriodRaw); + + const fromTs = latest.timestamp - SCAN_DAYS * SECONDS_PER_DAY; + const { block: fromBlock } = await sdk.api.util.lookupBlock(fromTs, { + chain: 'ethereum', + }); + + // Pull every distribution event in the scan window. + const rawLogs = await sdk.getEventLogs({ + target: WITRY, + eventAbi: eventRewardsReceived, + fromBlock, + toBlock: latest.number, + chain: 'ethereum', + }); + + // Each log needs a timestamp — fetch in parallel. + const events = await Promise.all( + rawLogs.map(async (l) => { + const ts = await sdk.api.util.getTimestamp(l.blockNumber, 'ethereum'); + return { block: l.blockNumber, ts }; + }), + ); + events.sort((a, b) => a.ts - b.ts); + + // Anchor selection. With <2 events we still try with whatever we have + // (early launch days); the fallback collapses both anchors to the same + // point, yielding apyBase = 0 rather than null — preserves a stable + // last value for downstream charts. + let apyBase = 0; + if (events.length >= 2) { + const endEvent = events[events.length - 1]; + const startTarget = endEvent.ts - LOOKBACK_DAYS * SECONDS_PER_DAY; + let startEvent = events[0]; + for (let i = events.length - 1; i >= 0; i--) { + if (events[i].ts <= startTarget) { + startEvent = events[i]; + break; + } + } + + // Sample rates at (event.ts + vestingPeriod) so the distribution + // emitted by each anchor event is fully reflected in convertToAssets. + // Clamp end to the latest block so we never query the future. + const endSampleTs = Math.min(latest.timestamp, endEvent.ts + vestingPeriod); + const startSampleTs = startEvent.ts + vestingPeriod; + const [{ block: endBlock }, { block: startBlock }] = await Promise.all([ + sdk.api.util.lookupBlock(endSampleTs, { chain: 'ethereum' }), + sdk.api.util.lookupBlock(startSampleTs, { chain: 'ethereum' }), + ]); + + const rateEnd = await getRateAtBlock(endBlock); + const rateStart = await getRateAtBlock(startBlock); + const elapsedDays = (endSampleTs - startSampleTs) / SECONDS_PER_DAY; + + // convertToAssets growth is already net of the 10% performance fee — + // YieldForwarder deducts it before streaming iTRY into the vault. + if (elapsedDays > 0 && rateStart > 0) { + const ratioReturn = rateEnd / rateStart - 1; + apyBase = + (Math.pow(1 + ratioReturn, DAYS_PER_YEAR / elapsedDays) - 1) * 100; + } + } + + const { output: totalAssetsRaw } = await sdk.api.abi.call({ + target: WITRY, + abi: abiTotalAssets, + chain: 'ethereum', + }); + const priceKey = `ethereum:${ITRY}`; + const priceResp = await axios.get( + `https://coins.llama.fi/prices/current/${priceKey}`, + ); + const iTryPrice = priceResp.data.coins[priceKey]?.price ?? 0; + const tvlUsd = (Number(totalAssetsRaw) / 1e18) * iTryPrice; + + return [ + { + pool: `${WITRY.toLowerCase()}-ethereum`, + chain: 'Ethereum', + project: 'brix', + symbol: 'wiTRY', + tvlUsd, + apyBase, + underlyingTokens: [ITRY], + poolMeta: 'APY · distribution-anchored 7d', + url: 'https://app.brix.money', + }, + ]; +}; + +module.exports = { + timetravel: false, + apy, + url: 'https://app.brix.money', +}; From 4c9922872058e4b8d6a3817dd179883952804db7 Mon Sep 17 00:00:00 2001 From: ismailemin Date: Wed, 3 Jun 2026 11:26:12 +0300 Subject: [PATCH 2/3] address review: null apyBase fallback, add pricePerShare - use null instead of 0 for the <2-events fallback so the empty case is not ingested into the time-series and doesn't skew smoothing - add pricePerShare (current convertToAssets(1e18) at latest block) to the pool object --- src/adaptors/brix/index.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/adaptors/brix/index.js b/src/adaptors/brix/index.js index 4a82192fcb..7eb3bbfe51 100644 --- a/src/adaptors/brix/index.js +++ b/src/adaptors/brix/index.js @@ -76,11 +76,11 @@ const apy = async () => { ); events.sort((a, b) => a.ts - b.ts); - // Anchor selection. With <2 events we still try with whatever we have - // (early launch days); the fallback collapses both anchors to the same - // point, yielding apyBase = 0 rather than null — preserves a stable - // last value for downstream charts. - let apyBase = 0; + // Anchor selection. With <2 events we can't compute a meaningful + // 7-day rate (early launch days). Leave apyBase as null so the value + // is not ingested into the DefiLlama time-series — a 0 would be read + // as "APY was 0%" and skew downstream smoothing / averages. + let apyBase = null; if (events.length >= 2) { const endEvent = events[events.length - 1]; const startTarget = endEvent.ts - LOOKBACK_DAYS * SECONDS_PER_DAY; @@ -127,6 +127,11 @@ const apy = async () => { const iTryPrice = priceResp.data.coins[priceKey]?.price ?? 0; const tvlUsd = (Number(totalAssetsRaw) / 1e18) * iTryPrice; + // Current pricePerShare for the ERC-4626 vault: 1 wiTRY → N iTRY at the + // latest block. Sampled live (not from the APY anchors) so the UI always + // shows the current redemption rate. + const pricePerShare = await getRateAtBlock(latest.number); + return [ { pool: `${WITRY.toLowerCase()}-ethereum`, @@ -136,6 +141,7 @@ const apy = async () => { tvlUsd, apyBase, underlyingTokens: [ITRY], + pricePerShare, poolMeta: 'APY · distribution-anchored 7d', url: 'https://app.brix.money', }, From c649e188e88085042ff0cf981d5c90da6a8f22f5 Mon Sep 17 00:00:00 2001 From: kr3p <123127490+0xkr3p@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:58:51 +0100 Subject: [PATCH 3/3] add isIntrinsicSource field --- src/adaptors/brix/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/adaptors/brix/index.js b/src/adaptors/brix/index.js index 7eb3bbfe51..aa25faa565 100644 --- a/src/adaptors/brix/index.js +++ b/src/adaptors/brix/index.js @@ -142,6 +142,7 @@ const apy = async () => { apyBase, underlyingTokens: [ITRY], pricePerShare, + isIntrinsicSource: true, poolMeta: 'APY · distribution-anchored 7d', url: 'https://app.brix.money', },