Skip to content

Conversation

@RohanNero
Copy link
Contributor

@RohanNero RohanNero commented Feb 2, 2026

May resolve #17849

  • adds common addUnderlyingBalances() helper function to reduce duplicated code
  • tracks borrowed export using the evault's totalBorrows() view function

Alternatively, using maxRelease() on collateral vaults was returning an identical value for borrowed:

async function borrowed(api) {
    const collateralVaults = await getCollateralVaults(api);
    const creditReserved = await api.multiCall({
        calls: collateralVaults,
        abi: 'function maxRelease() view returns (uint256)',
        permitFailure: true,
    });
    await addUnderlyingBalances(api, collateralVaults, creditReserved);
}

Summary by CodeRabbit

  • New Features

    • Introduced Twyne TVL module for Ethereum to track total value locked across evaults and collateral vaults.
    • Added calculation and reporting of outstanding borrowed amounts.
    • Implemented conversion from receipt tokens to underlying assets for accurate vault valuations.
  • Documentation

    • Added an on-chain methodology description for the new Ethereum TVL and borrowed metrics.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 2, 2026

📝 Walkthrough

Walkthrough

Adds a new Ethereum TVL module for Twyne that discovers EVaults and Collateral Vaults from on‑chain events, resolves issuer/underlying assets via multicall, converts receipt-token amounts to underlying balances, and exposes tvl and borrowed handlers plus a methodology string.

Changes

Cohort / File(s) Summary
Twyne TVL Module
projects/twyne/index.js
New module adding vault discovery (listening to ProxyCreated and T_CollateralVaultCreated from a START_BLOCK), helper functions (getTwyneEVaults, getCollateralVaults, addUnderlyingBalances), multicall flows to resolve issuer & underlying assets and call convertToAssets, and exported ethereum object with tvl and borrowed, plus methodology string.

Sequence Diagram

sequenceDiagram
    participant API as API (adapter)
    participant Factory as Factory Contracts
    participant Vault as Vault Contracts
    participant MC as MultiCall
    participant Agg as Aggregator

    API->>Factory: fetch events (ProxyCreated, T_CollateralVaultCreated) from START_BLOCK
    Factory-->>API: return vault addresses
    API->>Vault: read receipt-token totals (totalAssets / totalAssetsDepositedOrReserved)
    Vault-->>API: return receipt-token amounts
    API->>MC: batch -> resolve issuer vaults & underlying assets
    MC-->>API: issuer addresses, underlying token addresses
    API->>MC: batch -> call convertToAssets on issuer vaults (permitFailure)
    MC-->>API: underlying asset amounts
    API->>Agg: aggregate balances (sum assets, subtract borrows)
    Agg-->>API: final balances object
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through logs and traced each vault's song,
I chased the receipts and turned tokens along,
With multicall hops and balances in sight,
Twyne's vaults now counted under moonlight,
A little rabbit cheers for the code tonight.

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.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 'twyne tvl refactor' is vague and generic, using the term 'refactor' without describing the specific scope or main change. Clarify the title to specify the main change, such as 'Add borrowed tracking and helper function to Twyne adapter' or 'Refactor Twyne adapter with common helper function and borrowed export'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description addresses the PR's technical changes (helper function, borrowed tracking) but uses the template for new protocol listings, which is not applicable for this refactor PR.
Linked Issues check ✅ Passed The PR addresses #17849 requirements: adds TVL tracking, computes borrowed values, and provides the necessary adapter exports; the implementation matches the linked issue's objectives.
Out of Scope Changes check ✅ Passed All changes are directly related to Twyne TVL implementation as specified in #17849; no out-of-scope modifications detected.

✏️ 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

Important

Action Needed: IP Allowlist Update

If your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:

  • 136.113.208.247/32 (new)
  • 34.170.211.100/32
  • 35.222.179.152/32

Reviews will stop working after February 8, 2026 if the new IP is not added to your allowlist.


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 adapter at projects/twyne exports TVL:

ethereum                  908.91 k
ethereum-borrowed         27.83 k
borrowed                  27.83 k

total                    908.91 k 

@llamabutler
Copy link

The adapter at projects/twyne exports TVL:

ethereum                  850.66 k
ethereum-borrowed         26.87 k
borrowed                  26.87 k

total                    850.66 k 

@llamabutler
Copy link

The adapter at projects/twyne exports TVL:

ethereum                  842.99 k
ethereum-borrowed         26.63 k
borrowed                  26.63 k

total                    842.99 k 

@llamabutler
Copy link

The adapter at projects/twyne exports TVL:

ethereum                  852.55 k
ethereum-borrowed         26.93 k
borrowed                  26.93 k

total                    852.55 k 

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 `@projects/twyne/index.js`:
- Around line 83-84: adjustedAmounts can be negative (BigInt(eVaultAmounts) -
BigInt(totalBorrows)) which will break convertToAssets; before calling
addUnderlyingBalances, coalesce missing eVaultAmounts/totalBorrows to zero,
compute each adjusted amount as max(BigInt(eVaultAmounts[i]) -
BigInt(totalBorrows[i] || 0), 0n) and pass that non-negative BigInt array to
addUnderlyingBalances (referencing adjustedAmounts, eVaultAmounts, totalBorrows,
convertToAssets, addUnderlyingBalances, twyneVaults).
- Around line 34-59: The current logic assumes every vault is ERC-4626 nested
(calls asset() on issuerVaults then convertToAssets()), which breaks for
CollateralVaults and mis-handles EVaults; update addUnderlyingBalances to branch
by vault type: for EVaults (Credit Vaults) call asset() once on the vault listed
in vaults/issuerVaults and then call convertToAssets(amount) on that same EVault
(use convertCalls built from issuerVaults and amounts), and for CollateralVaults
do NOT call asset()/convertToAssets but instead fetch the vault's collateral
holdings directly (e.g., query the vault's collateral/balance/totalAssets
method) and map those amounts to underlyingBalances; adjust the construction of
validIndices/convertCalls and the multiCall that creates underlyingAmounts so
EVault conversion and CollateralVault direct-balance queries are performed
separately using issuerVaults, underlyingAssets, convertToAssets, and vaults as
reference points.

Comment on lines +34 to +59
// Get issuer vaults (which issued the receipt tokens)
const issuerVaults = await api.multiCall({
calls: vaults,
abi: 'address:asset',
});

// Get underlying asset addresses from issuer vaults
const underlyingAssets = await api.multiCall({
calls: issuerVaults,
abi: 'address:asset',
});

const validIndices = [];
const convertCalls = [];
vaults.forEach((_, i) => {
if (issuerVaults[i] && amounts[i]) {
validIndices.push(i);
convertCalls.push({ target: issuerVaults[i], params: [amounts[i]] });
}
});

// Convert receipt token amounts to underlying amounts
const underlyingAmounts = await api.multiCall({
calls: convertCalls,
abi: 'function convertToAssets(uint256) view returns (uint256)',
});
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:

#!/bin/bash
# Search for Twyne vault ABIs/interfaces and ERC4626 usage
rg -n "EVault|CollateralVault|convertToAssets|totalAssetsDepositedOrReserved|T_CollateralVaultCreated|ProxyCreated|asset\(" -g '*.{js,ts,json,md}' --max-count=100

Repository: DefiLlama/DefiLlama-Adapters

Length of output: 10764


🏁 Script executed:

cat -n projects/twyne/index.js

Repository: DefiLlama/DefiLlama-Adapters

Length of output: 4437


🏁 Script executed:

# Search for Twyne documentation, ABI files, or contract details
find . -type f -name "*.md" -o -name "*.json" -o -name "*.ts" | xargs rg -l "Twyne|twyne" 2>/dev/null | head -20

Repository: DefiLlama/DefiLlama-Adapters

Length of output: 54


🏁 Script executed:

# Also search for any references to EVault or collateral vault interfaces
rg -i "evault.*asset|collateral.*vault" -g '*.{md,txt,json}' -A 2 -B 2

Repository: DefiLlama/DefiLlama-Adapters

Length of output: 3082


🏁 Script executed:

# Look for any Twyne ABI files or contract directories
find . -path "*/twyne/*" -type f

Repository: DefiLlama/DefiLlama-Adapters

Length of output: 97


🏁 Script executed:

# Check if there's a README or documentation for Twyne
find . -name "README*" -o -name "*.md" | xargs grep -l "Twyne\|twyne" 2>/dev/null

Repository: DefiLlama/DefiLlama-Adapters

Length of output: 56


🌐 Web query:

Twyne protocol vault structure EVault CollateralVault ERC4626

💡 Result:

Twyne vault structure (Euler EVK EVault, Twyne CollateralVault, ERC‑4626)

  • Credit Vault (lender-facing) = Euler EVK “EVault” (ERC‑4626)

    • Twyne’s Credit Vault is “unmodified Euler EVault” code and behaves like a standard ERC‑4626 tokenized vault for Credit‑LP deposits/withdrawals. (twyne.gitbook.io)
    • Functionally, it’s a passive lending pool: it extends credit (lends) to Twyne collateral vaults and charges them interest via an interest-rate model. (twyne.gitbook.io)
  • Collateral Vault (borrower-facing) = Twyne-specific, per-owner vault (NOT ERC‑4626)

    • In Twyne v1, each Collateral Vault has a single owner, and many functions are owner-only. (twyne.gitbook.io)
    • It is not ERC20 / not ERC4626 (shares would need to be non-transferable due to single-owner design). (twyne.gitbook.io)
    • It holds the borrower’s collateral (often Euler yield-bearing/wrapped collateral assets) and can deposit either the wrapped collateral asset directly or deposit underlying and convert/register it as collateral. (twyne.gitbook.io)
    • Each collateral vault can borrow only one target asset (the debt asset) and includes flows for liquidation and handling external liquidation on the underlying lending market. (twyne.gitbook.io)
  • How this ties back to Euler EVK

    • Euler EVK “credit vaults” are ERC‑4626 vaults with added borrowing/lending accounting, where borrowing is allowed against collateral deposited in other vaults (as permitted by the liability vault). (docs.euler.finance)
    • Twyne composes this by using an EVK EVault as the pooled Credit Vault, while borrower positions live in Twyne’s CollateralVault wrapper/manager contract that interfaces with the underlying market and Twyne’s internal risk/liquidation logic. (twyne.gitbook.io)

Citations:


Branch addUnderlyingBalances to handle EVault and CollateralVault separately.

CollateralVaults are not ERC-4626 compliant and do not support the nested asset() + convertToAssets() pattern. EVaults (Credit Vaults) implement ERC-4626 but may not be nested—their asset() likely returns the credit token directly, not another issuer vault. The current implementation will fail or zero balances on CollateralVault processing (line 90) and potentially mishandle EVault conversion (line 84).

Create separate handlers:

  • For EVaults: Call asset() once and use convertToAssets() to convert total credits to underlying
  • For CollateralVaults: Query the vault's collateral holdings directly without assuming asset() or nested ERC-4626 structure
🤖 Prompt for AI Agents
In `@projects/twyne/index.js` around lines 34 - 59, The current logic assumes
every vault is ERC-4626 nested (calls asset() on issuerVaults then
convertToAssets()), which breaks for CollateralVaults and mis-handles EVaults;
update addUnderlyingBalances to branch by vault type: for EVaults (Credit
Vaults) call asset() once on the vault listed in vaults/issuerVaults and then
call convertToAssets(amount) on that same EVault (use convertCalls built from
issuerVaults and amounts), and for CollateralVaults do NOT call
asset()/convertToAssets but instead fetch the vault's collateral holdings
directly (e.g., query the vault's collateral/balance/totalAssets method) and map
those amounts to underlyingBalances; adjust the construction of
validIndices/convertCalls and the multiCall that creates underlyingAmounts so
EVault conversion and CollateralVault direct-balance queries are performed
separately using issuerVaults, underlyingAssets, convertToAssets, and vaults as
reference points.

Comment on lines +83 to +84
let adjustedAmounts = eVaultAmounts.map((amount, i) => BigInt(amount) - BigInt(totalBorrows[i]));
await addUnderlyingBalances(api, twyneVaults, adjustedAmounts);
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

Guard against negative adjustedAmounts before convertToAssets.

BigInt(totalAssets) - BigInt(totalBorrows) can go negative if borrows exceed assets (or if totalAssets excludes borrows), which will break convertToAssets(uint256) and revert. Clamp at zero (and coalesce missing values) to keep TVL stable.

🛠️ Suggested fix
-    let adjustedAmounts = eVaultAmounts.map((amount, i) => BigInt(amount) - BigInt(totalBorrows[i]));
+    let adjustedAmounts = eVaultAmounts.map((amount, i) => {
+        const assets = BigInt(amount ?? 0);
+        const borrows = BigInt(totalBorrows[i] ?? 0);
+        return assets > borrows ? assets - borrows : 0n;
+    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let adjustedAmounts = eVaultAmounts.map((amount, i) => BigInt(amount) - BigInt(totalBorrows[i]));
await addUnderlyingBalances(api, twyneVaults, adjustedAmounts);
let adjustedAmounts = eVaultAmounts.map((amount, i) => {
const assets = BigInt(amount ?? 0);
const borrows = BigInt(totalBorrows[i] ?? 0);
return assets > borrows ? assets - borrows : 0n;
});
await addUnderlyingBalances(api, twyneVaults, adjustedAmounts);
🤖 Prompt for AI Agents
In `@projects/twyne/index.js` around lines 83 - 84, adjustedAmounts can be
negative (BigInt(eVaultAmounts) - BigInt(totalBorrows)) which will break
convertToAssets; before calling addUnderlyingBalances, coalesce missing
eVaultAmounts/totalBorrows to zero, compute each adjusted amount as
max(BigInt(eVaultAmounts[i]) - BigInt(totalBorrows[i] || 0), 0n) and pass that
non-negative BigInt array to addUnderlyingBalances (referencing adjustedAmounts,
eVaultAmounts, totalBorrows, convertToAssets, addUnderlyingBalances,
twyneVaults).

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