Silo Finance WETH Donated-Liquidity Utilization Inflation Near Miss (2023-04-28)

  • Name: Silo Finance WETH donated-liquidity utilization inflation near miss
  • Date: 2023-04-28
  • Protocol: Silo Finance
  • Chain: Ethereum
  • Category: utilization-boundary failure / off-ledger liquidity interest explosion / donation-driven collateral inflation / whitehat near miss
  • Loss: no user funds lost; the reporting whitehat estimated roughly $3M of Ethereum-side pool value was exposed
  • Summary: A whitehat found that Silo’s market accounting could be split in two. deposit() updated assetStorage[_asset].totalDeposits, but raw ERC-20 transfers changed the contract’s live balance without minting deposit shares. In an empty WETH side of market 0xcB3B879aB11F825885d5aDD8Bf3672596d35197C, an attacker could deposit dust WETH, donate 1 WETH directly to the market, then use a second address to borrow that donated WETH against LINK collateral. On the next block, accrueInterest(WETH) would see totalBorrowAmount > totalDeposits, push utilization above 100%, and drive the interest model into an extreme region. In Immunefi’s published proof of concept, that made the attacker’s tiny initial WETH deposit price as collateral worth more than 5,000 ETH on paper, enough to borrow out deep-liquidity assets such as XAI.
  • Trigger / entry point:
    • Silo let unsolicited ERC-20 transfers increase live market liquidity without increasing totalDeposits.
    • liquidity(_asset) was derived from ERC20(_asset).balanceOf(address(this)) - collateralOnlyDeposits, so donated WETH became borrowable.
    • _accrueInterest(_asset) and the interest-rate model still priced the market from totalBorrowAmount / totalDeposits.
    • The target WETH side was empty, so a dust initial deposit gave the attacker almost all depositor exposure before the utilization blow-up.
  • Root cause:
    • Silo used two incompatible truths for the same market: live token balance for borrowable liquidity and internal booked deposits for utilization and interest accrual.
    • The protocol did not enforce or safely handle the invariant that totalBorrowAmount should never exceed totalDeposits.
    • Once utilization moved above 100%, the interest model could produce extreme accrued value that Silo then capitalized into depositor collateral.
  • Exploit path:
    1. The attacker finds a Silo market whose WETH side has 0 booked deposits.
    2. They deposit a dust amount of WETH through deposit(); Immunefi’s PoC uses 1e5 wei so the attacker owns effectively all WETH depositor-share state.
    3. They transfer 1 WETH directly to the market contract, increasing live liquidity but leaving assetStorage[WETH].totalDeposits near zero.
    4. From a second controlled address, they deposit about 545 LINK as collateral and borrow the donated 1 WETH.
    5. On the next block, accrueInterest(WETH) computes utilization from debt divided by the tiny booked deposits base, so utilization is far above 100%.
    6. The interest model produces an enormous accrued-interest figure; in the public PoC, the resulting collateral repricing makes the first account’s tiny WETH deposit appear worth more than 5,000 ETH.
    7. The first account then borrows out assets such as XAI from the rest of the market stack before the protocol can react.
  • What the attacker needed to be true:
    • The target asset had to have zero or near-zero booked deposits so a dust initial deposit would control most depositor exposure.
    • Direct token transfers had to increase liquidity() without minting deposit shares or raising totalDeposits.
    • Another asset in the market had to be accepted as collateral so a second account could borrow the donated WETH.
    • The interest model and collateral valuation path had to remain dangerous rather than fail closed once utilization exceeded 100%.
    • The market needed enough borrowable value elsewhere, especially XAI or bridge assets, to monetize the printed collateral.
  • Technical anatomy worth remembering:
    • The affected state split was between assetStorage[_asset].totalDeposits and the live ERC-20 balance read inside liquidity().
    • The critical functions were deposit(), _borrow(), liquidity(), and _accrueInterest(), plus the InterestRateModel.getCompoundInterestRateAndUpdate() path that priced supercritical utilization.
    • Certora’s later review usefully decomposed the exploit into two separate failures: first, Silo allowed totalBorrowAmount > totalDeposits; second, the interest model still behaved dangerously when utilization moved above 100% instead of failing closed or staying safely bounded.
    • This was not a flash-loan-in-one-transaction exploit. Immunefi’s PoC explicitly required two blocks: the state was prepared on block 17139470, then the interest explosion was harvested on block 17139471.
    • The core profit bridge was not steal WETH directly; it was use broken WETH interest accounting to print collateral, then borrow some other stronger asset.
  • Representative contracts, assets, and replay points:
    • Public PoC market: 0xcB3B879aB11F825885d5aDD8Bf3672596d35197C
    • WETH: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    • LINK: 0x514910771AF9Ca656af840dff83E8264EcF986CA
    • XAI bridge asset used in the PoC drain step: 0xd7C9F0e536dC865Ae858b0C0453Fe76D13c3bEAc
    • Public fork blocks used in Immunefi’s Forge reproduction: 17139470 and 17139471
  • Response / mitigation:
    • The whitehat reported the issue to Silo Finance through Immunefi on 2023-04-28.
    • Silo remediated the bug before public disclosure; no user funds were lost.
    • Certora’s later review says Silo bounded utilization and changed the interest calculation so the model stayed safe even if utilization rose above 100%.
    • Certora also preserved two concrete verification hooks worth reusing elsewhere: an invariant that getAssetTotalDeposits(asset) >= getAssetTotalBorrowAmount(asset) and a companion rule that unbounded utilization must never produce more interest than the model’s maximum intended-rate case.
    • The whitehat received a 100,000 USDC bounty.
    • The incident also triggered follow-up formal-verification work around invariants such as getAssetTotalDeposits(asset) >= getAssetTotalBorrowAmount(asset).

Pattern tags

  • utilization-boundary-failure
  • entrypoint-only-policy-off-path-state-mutation
  • reward-recognition-presentation-and-collateral-separation
  • The preserved failure was not merely donation changed a balance; it was borrowable liquidity and utilization denominator becoming two different reserve truths for the same WETH market.
  • This is also a clean thin-market authorship case. A dust WETH depositor was still allowed to own nearly all depositor-share state, manufacture the next utilization regime, and then spend the resulting paper yield as collateral.
  • The near-miss matters because it preserves the sharper double-boundary lesson from Certora’s review: first, borrow > deposits should have been impossible; second, even if it somehow appeared, out-of-domain utilization should not have been allowed to mint immediate collateral authority.

Why the exploit worked

  • Silo split one market into two incompatible accounting objects. liquidity() trusted the contract’s live WETH balance, while _accrueInterest() and depositor accounting still trusted assetStorage[WETH].totalDeposits. A direct transfer therefore widened borrowable liquidity without widening the denominator that priced utilization and depositor yield.
  • The target WETH side was empty enough that the attacker could own essentially all meaningful depositor-share state with a dust deposit. That meant any later utilization-driven interest spike would accrue almost entirely to the attacker rather than to a broad supplier base.
  • The protocol also treated an out-of-domain utilization state as economically meaningful instead of as an emergency condition. Once the second account borrowed the donated WETH, the next accrual step could observe totalBorrowAmount > totalDeposits, push utilization above 100%, and still capitalize the resulting rate output into depositor value.
  • No second distrust boundary stopped the paper gain from becoming borrow power. The first account’s inflated WETH deposit could immediately support borrowing stronger assets like XAI even though no real WETH repayment or durable reserve growth had occurred.
  • The exploit path was therefore not steal the donated WETH and leave; it was use off-ledger WETH to manufacture a supercritical utilization regime, let that regime print collateral for the dominant depositor, then drain sibling assets whose liquidity was still real.

Detection and audit pivots

  • Compare every market’s available to borrow variable against the deposits denominator used for utilization and interest accrual. If direct transfers, rebases, bridge mints, or rescue top-ups can move the first without moving the second, the solvency split already exists.
  • Run the empty-market replay explicitly: dust-deposit the target asset, donate underlying directly, borrow it from a second address, advance one block, and inspect whether utilization rises above 100% or depositor value jumps. This family is easy to miss if testing assumes a single actor in one transaction.
  • Prove two invariants separately: borrow <= deposits for the intended ledger model, and rate / collateral output stays bounded and non-monetizable even if that first invariant is violated. Silo is useful precisely because the first guard failed and the second was still required.
  • Inspect where accrued interest becomes collateral truth. If a protocol lets unrealized depositor yield immediately increase borrow headroom, then any rate-model domain failure becomes a balance-sheet failure instead of a display-only bug.
  • Keep thin markets under stricter admission policy. An asset side where one actor can cheaply own almost all depositor shares should not also be allowed to publish full collateral truth for the rest of the lending stack.
  • Monitor production-impossible states directly: totalBorrowAmount > totalDeposits, utilization above the documented ceiling, sudden depositor APR explosions, or empty markets that become meaningfully borrowable after raw token transfers.

Failure-prevention checklist

  • Force one canonical reserve truth for borrowable liquidity, totalDeposits, and utilization-sensitive interest math. If direct transfers can widen one of those state objects without widening the others, the market is still attacker-authorable.
  • Keep unsolicited balances economically inert until an explicit recognition path adopts them. A donated WETH balance should not become borrowable reserve just because balanceOf(address(this)) can see it.
  • Encode the intended ledger invariant directly: totalBorrowAmount <= totalDeposits or an equivalent stronger condition should be continuously enforced, not merely assumed by the interest model.
  • Bound the dangerous state twice. First, stop or quarantine any path that pushes utilization above the protocol’s intended range; second, keep the rate model and collateral engine bounded and non-monetizable even if that impossible state still appears.
  • Quarantine thin or empty markets more aggressively. A side where one actor can cheaply own almost all deposit-share state should not also be allowed to publish immediate collateral truth for the rest of the market stack.
  • Separate accrued-interest recognition from borrow-side authority. If paper yield from a supercritical-rate event can instantly raise collateral value before any real repayment occurs, the protocol is lending against hypothetical value.
  • Test multi-account, multi-block bundles explicitly. Silo’s public PoC needed one address to manufacture the reserve split, another to borrow the donated WETH, and a later block to harvest the collateral inflation.
  • Monitor for production-impossible states and auto-demote aggressively: borrow > deposits, utilization above the documented ceiling, abrupt depositor-APR explosions, or empty markets made borrowable by raw transfers should all be treated as incident conditions.

Audit prompts

  • What exact variable defines available to borrow, and what exact variable defines the denominator used for utilization and depositor yield? Are they guaranteed to move together under every balance-changing path?
  • If someone transfers WETH directly to the market without calling deposit(), which subsystems change immediately: borrowable liquidity, totalDeposits, utilization, collateral value, or none of the above?
  • What exact invariant, check, or clamp prevents totalBorrowAmount > totalDeposits, and what happens if that invariant is violated anyway?
  • Does the interest model have a safe, bounded behavior when utilization exceeds its intended domain, or is the protocol still relying on that state should be unreachable as its only defense?
  • Can accrued but unpaid interest immediately increase collateral value, LTV headroom, or borrow capacity for the dominant depositor before any real reserve growth has occurred?
  • How does the protocol treat zero-deposit or near-zero-deposit asset sides? Can a dust depositor cheaply become the main beneficiary of the next utilization or rate-regime change?
  • Can one account donate or park liquidity while a second account borrows it, so the exploitable state split exists only across multiple addresses or blocks?
  • Which production alert would fire first if raw token balances, booked deposits, and utilization inputs stopped agreeing in a live market?

Lessons that should already be standard

  • Raw token balance and the deposits ledger are not interchangeable truths. If one decides borrowing power while the other decides interest and collateral repricing, the gap between them is already a solvency surface.
  • Utilization > 100% is an incident state, not a curiosity the interest model is allowed to improvise through.
  • Empty or dust-dominated markets turn rate math into value-authorship. If one actor owns almost all deposit-share state, the next extreme interest event belongs to that actor by construction.
  • Unsolicited transfers are adversarial inputs unless the protocol has explicitly reconciled them. The tokens are here is not the same thing as the tokens now back depositor economics.
  • Capitalizing unrealized interest directly into collateral value is a force multiplier. Once rate output is attacker-authored, the lending stack is borrowing against hypothetical yield rather than settled assets.
  • Verification assumptions are part of the threat model. If the proof only holds because reviewers assumed utilization never exceeds 100%, then proving that assumption is itself security work.
  • Multi-block, multi-address exploit paths deserve first-class review. A bug does not become less real just because one account prepares the state and another account harvests it later.

Why this incident matters

  • It is a clean near-miss exemplar for the utilization-boundary failure family because the decisive exploit primitive was not a classic price-oracle spoof or empty-market exchange-rate donation. The attacker would have manufactured a supercritical interest regime by making live borrowable liquidity larger than the deposits ledger that the rate model trusted.
  • It preserves a useful two-boundary lesson from Certora’s review: first, lending protocols should not casually tolerate borrow > deposits; second, even if that impossible state somehow appears, the interest model and collateral path still must not turn it into printable value.
  • It also matters as a verification lesson. Earlier formal work had effectively assumed utilization stayed within its intended range, which meant the prover never explored the broken >100% state. Silo is a good reminder that out-of-domain states are unreachable is itself a security claim that must be proved, not just used to simplify the model.
  • Because the issue was caught before theft, the note is especially useful as prevention material rather than just post-loss history: it preserves the exact invariant, PoC shape, and multi-block attack sequence other isolated-lending markets should test before they need a public exploit to learn the same lesson.
  • Key transaction hashes:
    • No public exploit transaction hashes are known because the issue was caught before an onchain theft.
    • The public reproduction path is the Immunefi Forge PoC on Ethereum mainnet fork blocks 17139470 and 17139471.
  • Comparable exploit pattern: off-ledger liquidity interest explosion.

Internal linkages

  • Core recurring pattern: utilization-boundary-failure — Silo is the clean whitehat case where raw-balance liquidity, booked deposits, and interest-model domain assumptions split apart.
  • Broader policy-boundary lens: entrypoint-only-policy-off-path-state-mutation — deposit() updated the deposits ledger, but direct transfers still changed the live-liquidity state that borrow and interest logic trusted.
  • Adjacent accounting family: supplier-index-inflation — both families can make a dust depositor position look borrowable, but Silo’s path ran through unsafe utilization and interest accrual rather than through a rebasing supplier index.
  • Adjacent donation family: donation-share-price-manipulation — both weaponize direct transfers into collateral inflation, but Silo inflated lending-market interest accounting instead of vault share price.
  • Adjacent balance-sheet family: solvency-check-bypass — the shared lesson is that helper accounting paths are part of the solvency boundary, even when they look like harmless excess balance.
  • Positive-design companion on borrow/deposit ledger coherence: borrowable-liquidity-and-deposit-ledger-coherence-discipline — useful when the sharper design question is how a lending market should keep available to borrow, the deposits denominator, utilization math, and collateral publication on one reserve story instead of letting direct-transfer inventory become borrowable before the depositor book adopted it.
  • Positive-design companion on utilization-domain containment: utilization-domain-enforcement-and-supercritical-rate-quarantine — useful when the sharper design question is what should happen once the market has already reached utilization > 100%: whether interest output should clamp, quarantine, treasury-route, or otherwise lose collateral authority instead of being capitalized straight into depositor value.
  • Positive-design companion on reserve-path consistency: helper-path-invariant-reuse-and-fail-closed-discipline — useful when the sharper design question is why deposit(), liquidity(), and interest accrual were allowed to trust different reserve truths at all.
  • Positive-design companion on ambient-reserve quarantine: unexpected-balance-quarantine-and-explicit-recognition-discipline — useful when the sharper design question is where donated WETH should have lived before any borrow path or utilization-sensitive interest path was allowed to treat it as economically active reserve.
  • Positive-design companion on thin-market admission: collateral-admission-lag-and-borrow-side-quarantine — useful when the sharper design question is why a dust-dominated WETH side was still allowed to publish full collateral truth for the rest of the market stack.
  • Positive-design companion on collateral publication timing: reward-recognition-presentation-and-collateral-separation — useful when the sharper question is why paper depositor yield from an out-of-domain rate spike became immediate collateral authority before any real repayment happened.

Control surface