Tender.fi tGMX GMXPriceOracle Decimal Misconfiguration Borrow Drain / Whitehat (2023-03-07)

  • Name: Tender.fi tGMX GMXPriceOracle decimal misconfiguration borrow drain / whitehat
  • Date: 2023-03-07
  • Protocol: Tender.fi
  • Chain: Arbitrum
  • Category: oracle configuration failure / decimal mismatch / wrong-scale collateral valuation
  • Loss: about $1.59M-$1.6M temporarily borrowed; funds were later returned and the whitehat received about 62.15 ETH
  • Summary: Tender.fi, a Compound-v2-style lending market on Arbitrum, shipped a custom oracle adapter for GMX ecosystem assets that returned collateral prices at the wrong scale. Comptroller expects oracle.getUnderlyingPrice() to produce 18-decimal mantissas, but the Tender GMXPriceOracle path for tGMX multiplied Chainlink’s 8-decimal GMX price by 1e20 and then by another 1e10. That made 1 GMX collateral look roughly 10^20 times more valuable than intended. A whitehat bought about 1 GMX, minted tGMX, and then borrowed roughly $1.6M of stronger assets from Tender’s pools before leaving an onchain message and later returning the funds.
  • Trigger / entry point:
    • Tender used Compound-style accounting where account liquidity is computed in Comptroller.getHypotheticalAccountLiquidityInternal() from user balances, exchange rates, collateral factors, and oracle.getUnderlyingPrice(asset).
    • Tender’s Exponential math expects prices to be expressed as Exp mantissas with 18 decimal places.
    • Tender’s official postmortem says the team deployed an upgraded price feed at about 4:56 PM on 2023-03-06 that pointed GMX pricing to a new Chainlink-backed oracle branch instead of the older TWAP path.
    • Tender’s custom GMXPriceOracle mixed multiple oracle sources with different native decimal conventions: Chainlink for GMX, GMX’s own pricing for many assets, a custom TND TWAP path, and a GLP-specific AUM path.
    • In the tGMX branch, the adapter scaled Chainlink’s already-quoted GMX price twice, so the lending core accepted a wildly inflated collateral price without any sanity bound on the final result.
  • Root cause:
    • This was an oracle configuration / normalization failure, not a market-manipulation attack against GMX or Chainlink.
    • The decisive bug was scale mismatch inside Tender’s adapter layer: getGmxPrice() multiplied the Chainlink answer by 1e20, and getUnderlyingPrice() multiplied that result by another 1e10 for tGMX.
    • Because Tender’s Compound-derived core treated the returned value as an 18-decimal Exp, the system effectively priced GMX collateral about 10^20 too high.
    • The same walkthrough also notes that Tender’s tTND path returned a 30-decimal TWAP without rescaling to 18, preserving the broader lesson that heterogeneous oracle sources become dangerous when one adapter is responsible for dimensional reconciliation.
  • Exploit path:
    1. The whitehat funded attacker account 0x896DF3759205C141c97640B2B7345FA479FEB1aB and began the exploit bundle in tx 0x38ae60739af0726831957546d9d16c92ed75164a1581d4e4e6f270917913ab9c.
    2. The attacker acquired about 1 GMX and approved Tender’s GMX cToken market.
    3. In mint tx 0xaebc68faecfcf6dcf06fed038572510091e6fb37037b5bbf525a89f502a11110, the attacker deposited that GMX into Tender’s GMX market and received tGMX collateral tokens.
    4. Tender’s Comptroller then evaluated account liquidity using the inflated tGMX oracle price, so the account appeared to have enormous excess collateral value.
    5. The attacker borrowed real pool assets through ordinary lending paths, including a representative 100 ETH borrow in tx 0xc6e90441a684f370f0a1cd845e0d639088df20548a5c2d37f770cc85ea7306f9.
    6. Public incident reporting says the full drain reached about 198 ETH, 541,700 USDC, 16 WBTC, 8,798 UNI, 50,011 DAI, 36,700 USDT, 24,975 FRAX, and 16,203 LINK.
    7. After extracting the funds, the whitehat left an onchain message telling Tender the oracle was misconfigured, negotiated a return, and later sent the assets back in exchange for a bounty.
  • What the attacker needed to be true:
    • The final oracle output needed to be accepted as authoritative borrow power without any plausibility check on GMX/USD price scale.
    • The GMX market had to be listed as collateral and connected to pools with real, borrowable liquidity in stronger assets.
    • Tender’s core accounting had to remain internally consistent enough that the bad price would not revert execution; the exploit only needed one wrong number at the oracle boundary.
    • Operators needed to discover the issue after the first borrow, not before the whitehat emptied the relevant pools.
  • Technical anatomy worth remembering:
    • This is a clean example of why oracle manipulation and oracle configuration failure should stay separate in the corpus. Nobody moved the GMX market or corrupted Chainlink; Tender mis-scaled its own adapter output.
    • The bug sat exactly at the adapter boundary between heterogeneous oracle systems. Tender combined Chainlink, GMX-native pricing, a GLP AUM path, and a custom TND TWAP path inside one normalizing contract, so dimensional mistakes in one branch became direct solvency fiction for the entire lending market.
    • The exploit also preserves the Compound lesson that the lending core cannot tell the difference between price is wrong because the market moved and price is wrong because the adapter added too many zeros. Once getUnderlyingPrice() returns nonsense, Comptroller dutifully honors it.
    • The whitehat response makes this a useful operational case too: even a benign exploit path can fully drain pool liquidity before a protocol has time to patch an adapter-layer scaling bug.
  • Representative transactions and artifacts:
    • Attacker / whitehat account: 0x896DF3759205C141c97640B2B7345FA479FEB1aB
    • Initial exploit / swap setup tx: 0x38ae60739af0726831957546d9d16c92ed75164a1581d4e4e6f270917913ab9c
    • GMX mint / collateral deposit tx: 0xaebc68faecfcf6dcf06fed038572510091e6fb37037b5bbf525a89f502a11110
    • Representative 100 ETH borrow tx: 0xc6e90441a684f370f0a1cd845e0d639088df20548a5c2d37f770cc85ea7306f9
    • Additional Arbiscan-referenced exploit activity from the same account included tx 0xdeb31360e395dcba28ec360f61c619f5ebd0db170a189b796631b2c6796e0b72
  • Response / mitigation:
    • Tender’s official postmortem says the team reverted to the previous price feed at about 2:42 AM once it identified the new oracle branch as the likely source of the problem.
    • Tender then paused borrowing across all markets at about 2:57 AM out of caution.
    • Public reporting says the attacker left an onchain message identifying the problem as an oracle misconfiguration and then negotiated return terms with the team.
    • Tender later paid the whitehat about 62.15 ETH / roughly $97k-$850k depending on reference point and reporting method, in exchange for the return of about $1.6M.
    • Reporting after the incident says Tender planned to deploy a rewritten oracle contract before unpausing borrowing and to streamline price-fetching logic rather than keep the vulnerable mixed-source adapter.

Three oracle-publication promotions this incident collapsed

  1. A new GMX price-publication branch existed and returned a syntactically valid number

    • Tender’s postmortem says the team had deployed a new Chainlink-backed GMX price feed the previous afternoon, replacing the older TWAP path for that branch.
    • At this stage the dangerous object was still only a new branch output exists; it had not yet necessarily earned the stronger claim that this output now deserves to define live borrow power.
  2. Tender promoted that misnormalized branch output into canonical Compound collateral truth

    • The decisive bug was not that Chainlink lied; it was that Tender’s own adapter returned the GMX price with about 38 decimals instead of the 18-decimal solvency object the Compound-style core expected.
    • Once getUnderlyingPrice(tGMX) published that wrong-dimensional answer, the protocol treated 1 GMX as if it were systemically rich collateral rather than a branch-local normalization mistake.
  3. That published collateral truth still retained live reserve-spending authority

    • Ordinary borrow paths against deeper sibling pools remained open long enough for the whitehat to drain about $1.6M of ETH, WBTC, stablecoins, and other assets.
    • Tender’s own incident timeline later showed the missing separation clearly: the team reverted to the prior price feed at about 2:42 AM and paused borrowing at about 2:57 AM, but before those controls landed the mispublished oracle branch still carried full borrowing authority.

Pattern tags

  • oracle-configuration-failure
  • unit-normalization-failure
  • The preserved shape is sharper than Chainlink glitch or GMX price bug: Tender’s own adapter layer was responsible for turning heterogeneous source conventions into the final Compound-style solvency object, and one extra scaling step at that boundary made ordinary 1 GMX collateral look protocol-systemically rich.
  • This is also a clean mixed-source oracle adapter became the balance sheet failure. Tender’s upstream PriceOracle interface and Compound-derived Comptroller expected one answer shape — getUnderlyingPrice() returning 1e(36 - baseUnit)-scaled collateral value — but the protocol’s custom GMX branch published the wrong dimensional object into that interface and the rest of the lending core accepted it without skepticism.

Why the exploit worked

  • Tender’s oracle stack already encoded the key trust boundary: the Agora-derived ProxyOracle simply forwards getUnderlyingPrice(cToken), and the underlying UniswapOracleTWAP family normalizes heterogeneous sources into one Compound-compatible output via factor = 1e36 / basePricePrecision and division by config.baseUnit. Once a branch returns the wrong dimensional answer, the lending core has no second chance to reinterpret it.
  • The whitehat succeeded because the GMX-specific branch broke that normalization contract before the value reached Comptroller. Public reconstructions show getGmxPrice() multiplied the Chainlink answer by 1e20, then the tGMX path applied another 1e10, so the final published getUnderlyingPrice() answer still looked syntactically valid even while it was economically absurd.
  • That made the exploit path brutally simple. No market manipulation, callback trick, or governance race was required; the protocol itself published a false collateral mark, and the ordinary borrow path monetized it against deeper-liquidity pools.
  • The mixed-source design is the part worth preserving. Tender’s oracle stack combined Uniswap TWAP logic, fixed-price branches, external-oracle branches, and LP-specific paths under one shared interface. That meant branch-local unit drift in one GMX path was enough to falsify systemwide solvency truth even though the surrounding oracle framework looked structurally mature.
  • The preserved design lesson is broader than check decimals: branch-local normalization, final published price shape, and collateral-admission authority were one security object, and Tender treated them as if adapter correctness were just implementation detail rather than balance-sheet law.

Detection and audit pivots

  • Reconstruct the exact dimensional algebra from upstream answer to published borrow truth. For Tender-style stacks that means naming the raw source decimals, the branch-local normalization step, and the final Compound-facing object getUnderlyingPrice() = ${price} * 1e(36 - underlyingDecimals) rather than only checking that a feed address exists.
  • Audit mixed-source adapters branch by branch, not by interface label. A healthy Uniswap or fixed-price path does not validate an external-oracle or custom-asset branch that might double-scale an already normalized answer.
  • Compare the live onchain collateral mark against an independent reference before collateral use. If 1 GMX suddenly publishes as borrow power for protocol-scale ETH, WBTC, and stablecoin draws, the system should fail closed long before users can call the ordinary borrow path.
  • Treat market listing plus collateral activation as part of oracle review. Even when the price bug is adapter-local, the blast radius is determined by whether the affected market can immediately borrow from deeper sibling pools without caps, quarantine, or plausibility checks.
  • Review configuration-emitting code and deployment structure as security artifacts too. Tender inherited a generic oracle framework whose submodule config flow (_setConfigs, baseUnit, priceSource, branch-local oracle selection) made branch-local dimensional mistakes especially dangerous because the final output interface flattened them into one trusted solvency number.

Failure-prevention checklist

  • Write the final Tender-facing solvency object down branch by branch before activation: for tGMX, the answer had to be GMX / USD, expressed in the exact Compound-compatible scale Comptroller expects, not merely a number returned by the GMX branch.
  • Keep a branch-local unit table for every mixed-source adapter path. If one branch starts from an 8-decimal Chainlink answer, another from GMX-native pricing, another from GLP AUM math, and another from a custom TWAP, each branch should prove its own normalization before all of them collapse into one shared getUnderlyingPrice() interface.
  • Put plausible-magnitude checks at the publication boundary. A branch that makes about 1 GMX collateral appear rich enough to borrow protocol-scale ETH, WBTC, and stables should fail closed even if the output is syntactically well-formed.
  • Separate new oracle branch returns a value from new oracle branch may underwrite live borrowing. Fresh mixed-source branches should clear a canary or low-cap stage before they become full collateral authority against deeper sibling pools.
  • Review emitted config and deployment artifacts, not only source code. baseUnit, priceSource, branch selector, and any branch-local scale constants are balance-sheet inputs once the lending core honors them without reinterpretation.
  • Keep a fast emergency path close to the oracle boundary. Borrow freezes, collateral-factor zeroing, or per-market caps should be able to contain an adapter-level dimensional mistake faster than operators can fully redeploy or rewrite the oracle stack.
  • Test the exact boundary the core relies on, not just upstream feeds in isolation. The meaningful invariant is Comptroller receives the right final object, because the lending core cannot rescue a branch that already published the wrong dimension as money-good truth.

Audit prompts

  • What exact object does the tGMX branch publish into Tender’s borrow logic: GMX / USD, GMX / ETH, a GMX-native mark, or some partially normalized intermediary?
  • Starting from the raw upstream answer, which line in the adapter accounts for each decimal exponent, and where could one extra multiplication or omitted division survive as a syntactically valid but economically absurd final price?
  • Do all mixed-source branches return the same economic object, or does the shared getUnderlyingPrice() interface hide branch-local differences in source decimals, quote currency, or semantic meaning?
  • If the upstream feed is perfectly honest, what concrete branch-local mistake could still make the published Tender price wrong: wrong exponent, wrong quote leg, wrong baseUnit, wrong branch selector, or double normalization?
  • Which controls stop a newly added or modified oracle branch from immediately borrowing against deeper sibling markets before anyone reconciles it against an independent reference?
  • What range or reference checks would have made 1 GMX priced at protocol-systemic collateral scale unpublishable rather than merely surprising after the first borrow?
  • Which config-emitting scripts, registry writes, or deployment-time constants deserve the same review weight as oracle contract code because they decide the exact value Comptroller will later trust?
  • If this branch went wrong in production, how quickly could Tender freeze tGMX borrow relevance without waiting for a full oracle rewrite?

Lessons that should already be standard

  • Mixed-source adapters are balance-sheet code, not convenience glue. Once one branch misnormalizes a feed, the whole lending core inherits that fiction.
  • Upstream oracle was fine is not a defense. Tender still published a false collateral object because its own branch-local scaling logic changed the meaning of a valid source answer.
  • A shared interface can hide dangerous semantic drift. Several branches returning through one getUnderlyingPrice() surface only help if every branch proves the same asset, quote, and scale.
  • Plausibility checks belong at the final publication boundary. The core cannot tell double-scaled answer from real price after the adapter has flattened both into one trusted number.
  • Borrow-side quarantine matters for oracle changes too. A locally new or newly rewritten branch should not get instant full authority over deeper sibling pools just because it returns a value.
  • Deployment and configuration artifacts are oracle surfaces. Branch selectors, source ids, scale constants, and base units can be as dangerous as bad math inside the contract body.
  • Whitehat outcomes do not make the design failure smaller. Tender still demonstrated that one branch-local dimensional error could empty real pools before the team had time to react.

Why this incident matters

  • It is a compact, concrete exemplar of oracle configuration failure caused by wrong decimal normalization, not wrong market data.
  • It preserves a recurring adapter-layer lesson: a protocol can combine individually reputable oracle sources and still publish nonsense collateral values if the normalizer does not reduce them to one dimensional standard.
  • It is also a useful Compound-family case study because the exploit path was just deposit tiny collateral -> borrow everything once the adapter misreported value; no flash loan, governance action, or callback trick was required.

Internal linkages

Control surface