My second contribution to DefiLlama, fee/revenue adapters for Bitfi’s bfBTC and bfUSD (Basis) across multiple blockchain networks.
- Protocols: Bitfi BTC, Bitfi Basis
- Type: Protocol-level fees (multi-chain)
- Start Date: Varies by chain (2024-09-17 earliest)
- Approach: Event-based fee tracking with
getLogs
1. Background story
a. Gaining confidence
After my first contribution (Taiko) merged, I wanted to prove I could implement adapters independently without needing corrections. I saw an issue asking to track Bitfi fees/revenue.
b. The confusion
I looked at DefiLlama and saw: https://defillama.com/protocol/bitfi
It showed one protocol page called “Bitfi” with combined TVL and stats. Naturally, I thought: “This is one protocol called Bitfi”.
I started researching and found Bitfi had multiple products:
- bfBTC (Bitfi Bitcoin) - Withdrawal fees on EVM chains
- bfUSD (Bitfi Basis) - Stablecoin with redemption fees
But since the DefiLlama page showed them as one combined “Bitfi” protocol, I assumed I should track them together in one adapter. The UI made it look like one unified protocol!
c. The reality check
After I submitted my implementation, the reviewer pointed out something I missed:
“can you also add support for basis? https://defillama.com/protocol/bitfi-basis”
Wait… there’s a separate page for Bitfi Basis?
I then realized DefiLlama actually has three protocol pages:
- bitfi - https://defillama.com/protocol/bitfi (combined/aggregated view)
- bitfi-btc - https://defillama.com/protocol/bitfi-btc (bfBTC only)
- bitfi-basis - https://defillama.com/protocol/bitfi-basis (bfUSD only)
The “Bitfi” page I initially found was showing combined/aggregated data from both bitfi-btc and bitfi-basis. But in the backend, they need two separate adapters with their own slugs.
This was confusing because:
- The combined “bitfi” page makes it look like one protocol
- But the actual data tracking requires two separate implementations
- Each product has its own dedicated protocol page (bitfi-btc, bitfi-basis)
- The main “bitfi” page just aggregates data from both
2. Initial implementation
a. My original code structure
Since I thought “Bitfi” was one protocol (based on the combined UI page), I built one adapter that tracked both bfBTC and bfUSD together. It made sense to me: “They’re all under the ‘Bitfi’ protocol, so one adapter file should handle everything.”
// Separate configs for bfBTC and bfUSD
const bfBTCConfigs: Record<string, ChainConfig> = {
[CHAIN.ETHEREUM]: { token: '0x...', start: '2025-02-06' },
[CHAIN.BSC]: { token: '0x...', start: '2025-02-06' },
// ... more chains
}
const bfUSDConfig: Record<string, BfUSDConfig> = {
[CHAIN.ETHEREUM]: {
token: '0x...',
instantRedeemer: '0x...',
start: '2025-02-06'
},
}
// Separate fetch functions
async function fetchBfBTC(options: FetchOptions): Promise<FetchResultV2> {
// Track FeeCollected events
const feeCollectedLogs = await options.getLogs({
target: token,
eventAbi: 'event FeeCollected(...)',
})
for (const log of feeCollectedLogs) {
dailyFees.add(token, log.amount, METRIC.DEPOSIT_WITHDRAW_FEES)
}
}
async function fetchBfUSD(options: FetchOptions): Promise<FetchResultV2> {
// Track CrossChainFeeCollected and InstantRedemption events
}
// Special handler for Ethereum (both bfBTC and bfUSD)
async function fetchEthereum(options: FetchOptions): Promise<FetchResultV2> {
// Manually combine both protocols' fees
}
// Complex adapter structure
const adapter: Adapter = {
adapter: {
[CHAIN.ETHEREUM]: {
fetch: fetchEthereum, // Special combined handler
start: '2025-02-06',
},
...Object.fromEntries(
Object.entries(bfBTCConfigs)
.filter(([chain]) => chain !== CHAIN.ETHEREUM)
.map(([chain, config]) => [
chain,
{ fetch: fetchBfBTC, start: config.start }
])
),
},
version: 2,
} b. What I thought
I was confident this was the right approach because:
- The DefiLlama UI showed “Bitfi” as one protocol
- Both bfBTC and bfUSD were clearly Bitfi products
- Having them in one file matched the unified “Bitfi” branding
- The special
fetchEthereumfunction elegantly combined both on Ethereum - It used
Object.fromEntriesto dynamically generate chain configs - It felt organized and “DRY” (Don’t Repeat Yourself)
I thought: “This makes sense. One protocol page on DefiLlama = one adapter file in the codebase.”
I didn’t realize: The UI’s combined view was just for presentation. Backend tracking requires separate adapters for separate protocol slugs.
3. The problems
a. Wrong start dates
Issue: All chains were set to start: '2025-02-06'
Reality: Each contract was deployed on different dates:
- Ethereum bfBTC:
2024-09-17(much earlier!) - BSC:
2024-12-26 - Base:
2025-05-21 - And so on…
Impact: This would prevent historical data backfill, one of DefiLlama’s core features.
Reviewer feedback from treeoflife2:
“i can see you have added start date as 2025-02-06, is there anyway to refill for full history for older version? lmk if you need any help”
b. Wrong fee calculation
Issue: I used log.amount as the total fee
for (const log of feeCollectedLogs) {
dailyFees.add(token, log.amount, METRIC.DEPOSIT_WITHDRAW_FEES)
} Reality: The FeeCollected event structure is:
event FeeCollected(
address indexed user,
uint8 indexed feeType,
uint256 id,
uint256 amount, // ← This is the TOTAL withdrawal amount
uint256 percentageFee, // ← This is the percentage fee
uint256 fixedFee // ← This is the fixed fee
) The correct calculation:
const totalFee = log.percentageFee + log.fixedFee // Fee = percentage + fixed
dailyFees.add(token, totalFee, METRIC.DEPOSIT_WITHDRAW_FEES) Impact: I was tracking withdrawal amounts instead of actual fees! This would massively inflate the fee statistics.
c. Misunderstanding protocol slugs
Issue: I combined both products in one file because I saw one “Bitfi” page on DefiLlama UI
Reality: DefiLlama has three protocol pages:
bitfi→ combined/aggregated view of both productsbitfi-btc→ tracks bfBTC only (needs its own adapter)bitfi-basis→ tracks bfUSD only (needs its own adapter)
Why they’re separate:
- Two adapters needed: one for
bitfi-btc, one forbitfi-basis - The
bitfipage automatically aggregates data from both - Each product needs independent fees/revenue tracking
- Users can look up each protocol individually OR see combined stats
What I learned:
- UI presentation ≠ backend structure
- One brand can have:
- Individual protocol pages (bitfi-btc, bitfi-basis) ← need adapters
- Combined aggregated page (bitfi) ← auto-generated from the two adapters
- Always search for variations:
protocol/{name},protocol/{name}-*
The confusion came from:
- Finding https://defillama.com/protocol/bitfi first (the combined page)
- Not realizing there were separate pages for bitfi-btc and bitfi-basis
- Assuming I should create one adapter because I saw one page
- Not understanding that the “bitfi” page is just an aggregation layer
Maintainer action: Created two separate files (bitfi-btc.ts and bitfi-basis.ts) for two separate protocol slugs
d. Incomplete start date research
Issue: I used a placeholder date (2025-02-06) for all chains
What I should have done: Check contract deployment dates on block explorers:
- Ethereum: https://etherscan.io/address/0xCdFb58c8C859Cb3F62ebe9Cf2767F9e036C7fb15
- BSC: https://bscscan.com/address/0x623F2774d9f27B59bc6b954544487532CE79d9DF
- etc.
Impact: Wrong start dates = incomplete historical data
4. The fix
a. Maintainer’s refactoring
Instead of asking me to fix multiple issues, treeoflife2 closed my PR and created a new one (#5124) with this comment:
“I’ve refactored and created new PR as can’t edit this one.”
This was actually kind. He saw that:
- I correctly identified both protocols and their events
- I found the right contract addresses
- The core logic was sound
- But the execution (combined file, wrong calculations, wrong dates) needed significant refactoring
Rather than making me go through multiple revision cycles, he cleaned it up himself. My research and initial structure still contributed to the final implementation.
b. Key changes in final implementation
Split into two files
bitfi-btc.ts (bfBTC only):
const chainConfig: Record<string, config> = {
[CHAIN.ETHEREUM]: {
token: '0xCdFb58c8C859Cb3F62ebe9Cf2767F9e036C7fb15',
start: '2024-09-17', // ← Correct date
},
// ... other chains with correct dates
}
async function fetch(options: FetchOptions): Promise<FetchResultV2> {
const { token } = chainConfig[options.chain]
const feeCollectedLogs = await options.getLogs({
target: token,
eventAbi: 'event FeeCollected(...)',
})
for (const log of feeCollectedLogs) {
const totalFee = log.percentageFee + log.fixedFee // ← Fixed calculation
dailyFees.add(token, totalFee, METRIC.DEPOSIT_WITHDRAW_FEES)
}
return {
dailyFees,
dailyUserFees: dailyFees,
dailyRevenue: dailyFees,
dailyProtocolRevenue: dailyFees,
dailySupplySideRevenue: 0, // ← Explicit zero
}
}
const adapter: Adapter = {
version: 2,
fetch,
adapter: chainConfig, // ← Simple, direct mapping
methodology: {
Fees: 'Withdrawal fees from bfBTC (on EVM chains and Bitcoin network) paid by users.',
UserFees: 'Withdrawal fees from bfBTC (on EVM chains and Bitcoin network) paid by users.',
Revenue: 'All fees collected are sent to BitFi protocol fee receiver address.',
ProtocolRevenue: 'All fees collected are sent to BitFi protocol fee receiver address.',
SupplySideRevenue: 'No withdrawal fees go to supply side. bfBTC holders earn yield through exchange rate appreciation from strategies.'
},
} bitfi-basis.ts (bfUSD only):
const bfUSDConfig: Record<string, BfUSDConfig> = {
[CHAIN.ETHEREUM]: {
token: '0xa3eB7A9e57FCa4e40b79E394eD5eB37fEd205A24',
instantRedeemer: '0x0971cB672b4eF3E19284Aa64717aFb154A6fbeDF',
start: '2025-11-03' // ← Correct date
},
}
const fetch = async (options: FetchOptions) => {
const { token, instantRedeemer } = bfUSDConfig[options.chain]
// Cross-chain transfer fees
const crossChainFeeLogs = await options.getLogs({
target: token,
eventAbi: 'event CrossChainFeeCollected(address indexed user, uint256 amount, uint256 fee)',
})
for (const log of crossChainFeeLogs) {
dailyFees.add(token, log.fee, METRIC.DEPOSIT_WITHDRAW_FEES)
}
// Instant redemption fees
const instantRedemptionLogs = await options.getLogs({
target: instantRedeemer,
eventAbi: 'event InstantRedemption(..., uint256 feeAmount)',
})
for (const log of instantRedemptionLogs) {
dailyFees.add(token, log.feeAmount, METRIC.MINT_REDEEM_FEES)
}
return {
dailyFees,
dailyUserFees: dailyFees,
dailyRevenue: dailyFees,
dailyProtocolRevenue: dailyFees,
}
}
const adapter: Adapter = {
version: 2,
fetch,
chains: [CHAIN.ETHEREUM], // ← Simple array, not complex mapping
start: '2025-11-02',
methodology: {
Fees: 'Instant redemption and cross-chain transfer fees from bfUSD paid by users.',
UserFees: 'Instant redemption and cross-chain transfer fees from bfUSD paid by users.',
Revenue: 'All fees collected are sent to BitFi protocol fee receiver address.',
ProtocolRevenue: 'All fees collected are sent to BitFi protocol fee receiver address.',
},
} Key improvements:
- Separation of Concerns: Two protocols → two files
- Correct Fee Calculation:
percentageFee + fixedFeeinstead ofamount - Accurate Start Dates: Researched from block explorers
- Simple Adapter Structure: Direct config mapping, no
Object.fromEntriescomplexity - Clear Methodology: Specific explanations for each protocol’s fee structure
- Supply Side Revenue: Explicitly set to 0 with explanation for bfBTC
5. Key learnings
a. Technical
Read Event Structures Carefully: I assumed
amountwas the fee, but it was actually the withdrawal amount. The actual fees were in separate fields (percentageFee + fixedFee).Verify Start Dates: Never use placeholder dates. Always check block explorers for actual contract deployment timestamps.
Understand Protocol Slugs: One brand (Bitfi) can have multiple protocol pages:
- Aggregated page (
bitfi) - auto-generated, no adapter needed - Individual pages (
bitfi-btc,bitfi-basis) - each needs its own adapter
- Aggregated page (
UI ≠ Backend Structure: Combined views are just aggregation layers. The actual adapters track individual protocol slugs.
Simple > Smart:
Object.fromEntriesmapping felt smart, but direct config mapping is clearer and more maintainable.Supply Side Revenue Matters: Always consider if there’s supply-side revenue. For bfBTC, explicitly noting it’s zero (holders earn through exchange rate appreciation) provides clarity.
b. Process
Verify Protocol Structure First: Before coding, check if a brand has multiple protocol slugs. Search for
https://defillama.com/protocol/{brand-name}*variations.Don’t Assume from UI: The UI might show aggregated/combined views for user convenience, but backend tracking follows protocol slug structure.
Confidence ≠ Correctness: I was overconfident after my first successful contribution. Being eager to prove independence made me skip verification steps.
Don’t Rush Research: I should have spent more time understanding:
- Event structures (which fields are the actual fees?)
- Contract deployment dates (not placeholder dates)
- Protocol slug structure (are there multiple pages?)
Maintainers Will Help (Again): Even when I got multiple things wrong, the maintainer didn’t reject it outright. He refactored it himself. This is collaboration, not failure.
Close ≠ Rejection: My PR was closed, but my research and initial structure still contributed. The maintainer built on my work rather than starting from scratch.
c. Personal growth
Humility: I wrote in the PR that I was “too arrogant and overconfident”. This was a valuable lesson in staying humble and thorough.
Learning from Mistakes: Getting it wrong and seeing the corrections taught me more than getting it right the first time would have.
Contribution Still Valuable: Even though my code wasn’t used directly, I still helped by:
- Identifying that Bitfi has two separate products (bfBTC and bfUSD)
- Researching both protocols’ fee structures
- Finding all contract addresses across multiple chains
- Identifying the correct events to track
- Drafting the initial structure
Question Your Assumptions: I assumed “one UI page = one adapter” without verifying. I should have searched for other Bitfi-related protocol pages first.
6. Comparison: then vs now
| Aspect | My Implementation | Final Implementation |
|---|---|---|
| File Structure | One file, combined logic | Two separate files |
| Fee Calculation | log.amount (wrong) | log.percentageFee + log.fixedFee (correct) |
| Start Dates | All 2025-02-06 (wrong) | Researched actual dates |
| Adapter Pattern | Complex Object.fromEntries | Simple config mapping |
| Protocol Understanding | One combined “Bitfi” page = one adapter | Three pages: bitfi (aggregated), bitfi-btc (adapter), bitfi-basis (adapter) |
| Ethereum Handling | Special fetchEthereum function combining both | Separate files, no special case |
| Supply Side Revenue | Not addressed | Explicitly set to 0 with explanation |
7. Resources
- Original PR #4917
- Merged PR #5124
- Bitfi Documentation
- DefiLlama Protocol Pages:
- Bitfi (Combined) - Aggregated view
- Bitfi BTC - bfBTC only
- Bitfi Basis - bfUSD only
- Contracts:
8. Final thoughts
This contribution was more valuable as a learning experience than my first successful one. Getting it wrong and seeing how it should be done taught me:
- Check protocol slug structure first - don’t assume from UI. Search for
protocol/{name}*variations on DefiLlama. - Always verify event structures - don’t assume field names. Read the actual event parameters carefully.
- Research deployment dates - block explorers are your friend. Never use placeholder dates.
- UI ≠ Backend - aggregated views don’t mean combined tracking. Each slug needs its own adapter.
- Test edge cases - don’t just test that it runs, verify the data makes sense.
- Stay humble - confidence is good, overconfidence leads to mistakes.
a. What I learned
I’m grateful treeoflife2 took the time to refactor rather than just reject. Seeing the corrected version taught me:
- Protocol slugs ≠ brand names - one brand can have multiple protocol pages
- Aggregated pages exist - combined views like
bitfiauto-aggregate from individual pages - Adapters track individual slugs - create adapters for
bitfi-btcandbitfi-basis, notbitfi - Verify before assuming - search for
protocol/{name}*variations before starting - Simple separate files beat “clever” combined implementations
- Event field names matter -
amount≠ fee
b. For next time
Before implementing any protocol adapter, I’ll:
Search for https://defillama.com/protocol/{name}* to find all related pages and variations.
Determine which pages are aggregated (combined views) vs individual protocol slugs.
- Aggregated pages (e.g.,
bitfi) = no adapter needed, auto-generated - Individual pages (e.g.,
bitfi-btc,bitfi-basis) = need separate adapters
Check block explorer to understand the exact event parameters and field names.
Verify deployment dates from block explorer, don’t use placeholder dates.
Create one adapter per individual protocol slug, not for aggregated pages.
The key lesson: Don’t let aggregated UI pages fool you. DefiLlama can show combined views for user convenience, but adapters track individual protocol slugs. Always verify the protocol structure first:
- Aggregated page = auto-generated from multiple adapters
- Individual pages = each needs its own adapter