Skip to content

[TermMax] fee adapter#5794

Open
tkspring-henry wants to merge 5 commits intoDefiLlama:masterfrom
tkspring-henry:feature/termmax
Open

[TermMax] fee adapter#5794
tkspring-henry wants to merge 5 commits intoDefiLlama:masterfrom
tkspring-henry:feature/termmax

Conversation

@tkspring-henry
Copy link

@tkspring-henry tkspring-henry commented Feb 2, 2026

This PR is intended to replace #5301.

The previous implementation only tracked the yield generated by the vault itself, but the calculation method was incorrect.

TermMax currently has three revenue streams:

  • Protocol fee
  • Liquidation penalty
  • Performance fee

TermMax charges fees in the form of FT (Fixed-rate Token). However, DefiLlama currently does not provide an oracle for FT, so the real value of the fees would be underestimated. These FT tokens are redeemable 1:1 into the underlying asset at maturity. To simplify the calculation (since the exact underlying asset price at redemption cannot be predicted), we treat the FT’s value as the underlying asset price at the time the fee is charged. Because FT trades at a discount, this approach still underestimates the fee value, but it is closer to the real fee value than treating FT as having zero value (as before).

If a borrower cannot repay the debt before maturity, the liquidation penalty at maturity will also flow into the treasury in the form of collateral, which is also revenue for TermMax.

The vault’s idle funds are deployed into pools and parked on Aave / Morpho to earn yield. After that yield is distributed according to TermMax’s documented rules, a designated wallet transfers TermMax’s allocated share of the revenue into the treasury.

Summary by CodeRabbit

  • New Chains
    • Added support for Berachain network.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 2, 2026

📝 Walkthrough

Walkthrough

The fee aggregation system in TermMax has been refactored from a treasury-based approach to a log-driven, multi-path classification system. This change introduces transfer log analysis to identify and categorize liquidation penalties, protocol fees, and performance fees from distinct sources, with extended support for additional blockchain networks.

Changes

Cohort / File(s) Summary
Log-driven fee classification
fees/termmax.ts
Introduced getTransfers and handleLogs functions to fetch ERC20 Transfer logs and classify them into liquidation penalties, protocol fees, and performance fees based on source/destination addresses. Replaced vault-yield accounting with per-transfer classification logic. Added PERFORMANCE_FEE_MANAGER constant and integrated market address/GT config resolution via multiCall.

Sequence Diagram

sequenceDiagram
    participant Adapter as TermMax Adapter
    participant Logs as ERC20 Transfer Logs
    participant Market as Market Address Resolver
    participant GT as GT Config Fetcher
    participant Metrics as Fee Metrics Accumulator

    Adapter->>Logs: getTransfers(blockRange) for TREASURY
    Logs-->>Adapter: Transfer events
    Adapter->>Market: Resolve market addresses
    Market-->>Adapter: Market configs
    Adapter->>GT: Batch multiCall for GT configs
    GT-->>Adapter: GT configuration data
    Adapter->>Adapter: handleLogs - classify each transfer
    alt Transfer from liquidation (GT)
        Adapter->>Metrics: LIQUIDATION_FEES += amount
    else Transfer from FT market
        Adapter->>Metrics: PROTOCOL_FEES += valuated amount
    else Transfer from PERFORMANCE_FEE_MANAGER
        Adapter->>Metrics: PERFORMANCE_FEES += amount
    else Unknown source
        Adapter->>Adapter: Ignore transfer
    end
    Metrics-->>Adapter: Accumulated dailyRevenue
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested Labels

Refill

Poem

🐰 A bunny hops through transfer logs with glee,
Classifying fees in harmony—
Liquidations, protocols, performance too,
Each path sorted clean and true!
Now TermMax tracks them all so fine,

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title '[TermMax] fee adapter' is generic and does not clearly convey the main change: replacing the previous implementation with a log-driven, multi-path fee classification system that addresses calculation errors and adds support for protocol fees, liquidation penalties, and performance fees. Consider a more specific title such as '[TermMax] Replace fee calculation with multi-path log-driven classification' to better reflect the substantial methodological changes in this PR.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description adequately explains the motivation, current revenue streams, fee structure, and the rationale for the FT valuation approach, providing sufficient context for the changes. However, it does not follow the required template sections.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@llamabutler
Copy link

The termmax.ts adapter exports:

> adapters@1.0.0 test
> ts-node --transpile-only cli/testAdapter.ts fees termmax.ts

🦙 Running TERMMAX.TS adapter 🦙
---------------------------------------------------
Start Date:	Sun, 01 Feb 2026 04:09:53 GMT
End Date:	Mon, 02 Feb 2026 04:09:53 GMT
---------------------------------------------------

chain     | Daily user fees | Daily fees | Daily revenue | Daily protocol revenue | Start Time
---       | ---             | ---        | ---           | ---                    | ---       
ethereum  | 0.00            | 0.00       | 0.00          | 0.00                   | 27/3/2025 
arbitrum  | 0.00            | 0.00       | 0.00          | 0.00                   | 27/3/2025 
bsc       | 47.00           | 47.00      | 47.00         | 47.00                  | 28/5/2025 
berachain | 0.00            | 0.00       | 0.00          | 0.00                   | 8/7/2025  
Aggregate | 47.00           | 47.00      | 47.00         | 47.00                  |           

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@fees/termmax.ts`:
- Around line 83-86: The getLogs call in fees/termmax.ts that populates the logs
variable is missing parseLog: true, but later code accesses log.args.from and
log.args.value in the loop and other places (e.g., where froms/addresses are
built and fee parsing occurs), which will throw if logs are unparsed; update the
getLogs invocation (the call that sets const logs = await getLogs({...})) to
include parseLog: true alongside the existing entireLog and noTarget options so
the logs contain the args property and subsequent accesses to
log.args.from/log.args.value succeed.
- Around line 150-167: The fetch function in termmax.ts currently calls
dailyRevenue.add(dailyUserFees) but both dailyRevenue and dailyUserFees are
Balances objects; replace the call with dailyRevenue.addBalances(dailyUserFees)
to use the standard Balances merge method; update the invocation in the fetch
function (where dailyRevenue and dailyUserFees are defined and dailyRevenue is
mutated) to call addBalances instead of add for consistency with other helpers.
🧹 Nitpick comments (1)
fees/termmax.ts (1)

169-204: Consider reducing methodology duplication.

All four categories (Fees, UserFees, Revenue, ProtocolRevenue) have identical descriptions. You could define the strings once and reuse them:

const feeDescription = "Protocol fees (FT valued at underlying 1:1)...";
const methodology = {
  Fees: feeDescription,
  UserFees: feeDescription,
  Revenue: feeDescription,
  ProtocolRevenue: feeDescription,
};

This is a minor readability improvement - the current approach works fine.

Comment on lines +83 to +86
for (const log of logs) {
froms.push(log.args.from);
addresses.push(log.address);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n -A 5 'entireLog:\s*true' --type=ts | head -150

Repository: DefiLlama/dimension-adapters

Length of output: 7175


🏁 Script executed:

# Also search for getLogs usage generally
rg -n 'getLogs' --type=ts -B 2 -A 3 | head -200

Repository: DefiLlama/dimension-adapters

Length of output: 11892


🏁 Script executed:

# Search for the log structure - what properties are typically accessed on logs
rg -n 'log\.args\.' --type=ts | head -100

Repository: DefiLlama/dimension-adapters

Length of output: 4119


🏁 Script executed:

# Search for log.address usage
rg -n 'log\.address' --type=ts | head -100

Repository: DefiLlama/dimension-adapters

Length of output: 3881


🏁 Script executed:

cat -n fees/termmax.ts | sed -n '55,150p'

Repository: DefiLlama/dimension-adapters

Length of output: 4210


🏁 Script executed:

rg -n "parseLog:\s*true" --type=ts -B 2 -A 2 | head -80

Repository: DefiLlama/dimension-adapters

Length of output: 2074


🏁 Script executed:

# Check if there are any Transfer event handlers without parseLog that still access log.args
rg -n "Transfer.*event" --type=ts -A 10 | grep -A 10 "entireLog" | head -60

Repository: DefiLlama/dimension-adapters

Length of output: 54


Add parseLog: true to the getLogs call at line 61.

The code accesses log.args.from (line 84) and log.args.value (lines 107, 141) but the getLogs call doesn't include parseLog: true. Without it, logs are unparsed and lack the args property, causing runtime errors. Add parseLog: true to the getLogs configuration:

Diff
  const logs = await getLogs({
    topics: [
      "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
      from as any,
      to as any,
    ],
    fromBlock,
    toBlock,
+   parseLog: true,
    entireLog: true,
    noTarget: true,
  });
🤖 Prompt for AI Agents
In `@fees/termmax.ts` around lines 83 - 86, The getLogs call in fees/termmax.ts
that populates the logs variable is missing parseLog: true, but later code
accesses log.args.from and log.args.value in the loop and other places (e.g.,
where froms/addresses are built and fee parsing occurs), which will throw if
logs are unparsed; update the getLogs invocation (the call that sets const logs
= await getLogs({...})) to include parseLog: true alongside the existing
entireLog and noTarget options so the logs contain the args property and
subsequent accesses to log.args.from/log.args.value succeed.

Comment on lines +150 to +167
const fetch = async (options: FetchOptions) => {
const dailyRevenue = options.createBalances();

const [fromBlock, toBlock] = await Promise.all([
options.getFromBlock(),
options.getToBlock(),
]);
const logs = await getTransfers(options, null, TREASURY, fromBlock, toBlock);
const { dailyUserFees } = await handleLogs(options, logs);
dailyRevenue.add(dailyUserFees);

return {
dailyFees,
dailyUserFees,
dailyFees: dailyUserFees,
dailyRevenue,
dailyProtocolRevenue: dailyRevenue,
dailySupplySideRevenue,
}
}
};
};
Copy link
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:

# Find the Balances class/type definition
fd -e ts -e tsx | xargs rg -l "class Balances|interface Balances|type Balances" | head -20

Repository: DefiLlama/dimension-adapters

Length of output: 97


🏁 Script executed:

# Search for add method within Balances
rg -n "add\(" fees/termmax.ts -B3 -A3

Repository: DefiLlama/dimension-adapters

Length of output: 1271


🏁 Script executed:

# Look for Balances definition and add method
rg -n "add\(" --type=ts -B2 -A5 | grep -A7 "Balances\|\.add" | head -100

Repository: DefiLlama/dimension-adapters

Length of output: 5643


🏁 Script executed:

# Find Balances class/interface definition
rg -n "class Balances|interface Balances|type Balances" --type=ts -A15 | head -100

Repository: DefiLlama/dimension-adapters

Length of output: 857


🏁 Script executed:

# Look for the add method implementation
rg -n "add\s*\(" --type=ts -B3 -A10 | grep -B5 -A10 "Balances" | head -80

Repository: DefiLlama/dimension-adapters

Length of output: 4213


🏁 Script executed:

# Check the helper functions directory for Balances
fd -e ts -e tsx helpers/ | xargs rg -l "Balances" | head -20

Repository: DefiLlama/dimension-adapters

Length of output: 945


🏁 Script executed:

# Search for sdk import and definition
rg -n "@defillama/sdk|from.*sdk|import.*sdk" --type=ts fees/termmax.ts | head -5

Repository: DefiLlama/dimension-adapters

Length of output: 54


🏁 Script executed:

# Look for sdk.Balances or Balances class definition in node_modules or src
rg -n "class Balances|\.add\(" --type=ts -B2 -A8 | grep -B4 -A8 "add.*Balances\|Balances.*add" | head -60

Repository: DefiLlama/dimension-adapters

Length of output: 3221


🏁 Script executed:

# Search for type definition of add method
rg -n "add\s*\(\s*\w+\s*:\s*Balances" --type=ts -A5

Repository: DefiLlama/dimension-adapters

Length of output: 54


🏁 Script executed:

# Check capybara-perp.ts around line 26 to see what type is being used
sed -n '15,35p' open-interest/capybara-perp.ts

Repository: DefiLlama/dimension-adapters

Length of output: 764


🏁 Script executed:

# Look for more context on how these Balances objects are created
rg -n "addBalances\|\.add\(" fees/termmax.ts -B2 -A2

Repository: DefiLlama/dimension-adapters

Length of output: 54


🏁 Script executed:

# Search for addBalances method definition in codebase
rg -n "addBalances" --type=ts -B2 -A5 | head -80

Repository: DefiLlama/dimension-adapters

Length of output: 4646


Use .addBalances() instead of .add() to merge Balances objects.

Line 159 calls dailyRevenue.add(dailyUserFees) where both are Balances objects. Throughout the codebase, the standard method for merging Balances objects is .addBalances() (see usages in helpers/gmx.ts, helpers/token.ts, helpers/compoundV2.ts, options/thales/index.ts, etc.). Update this to dailyRevenue.addBalances(dailyUserFees) for consistency.

🤖 Prompt for AI Agents
In `@fees/termmax.ts` around lines 150 - 167, The fetch function in termmax.ts
currently calls dailyRevenue.add(dailyUserFees) but both dailyRevenue and
dailyUserFees are Balances objects; replace the call with
dailyRevenue.addBalances(dailyUserFees) to use the standard Balances merge
method; update the invocation in the fetch function (where dailyRevenue and
dailyUserFees are defined and dailyRevenue is mutated) to call addBalances
instead of add for consistency with other helpers.

@treeoflife2 treeoflife2 self-assigned this Feb 3, 2026
Copy link
Member

@treeoflife2 treeoflife2 left a comment

Choose a reason for hiding this comment

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

.

const { log } = tuples[i];
const tokens = allTokens[i];
if (tokens && tokens[4]) {
// FT contract -> Protocol fee (valued in underlying token)
Copy link
Member

Choose a reason for hiding this comment

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

FT trades at a discount. The actual value at fee collection time is lower than the underlying, this would be incorrect as price could move drastic until it unlocks, so it would be wrong to price it as 1:1 even tho it's discounted, maybe we could track the fees at when it matures?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants