All incidents

BonqDAO ALBT Oracle Manipulation via TellorFlex

Share
Feb 01, 2023 18:29 UTCAttackLoss: 100,000,000 BEUR, 113,813,998.37 ALBTPending manual check1 exploit txWindow: Atomic
Estimated Impact
100,000,000 BEUR, 113,813,998.37 ALBT
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Feb 01, 2023 18:29 UTC → Feb 01, 2023 18:29 UTC

Exploit Transactions

TX 1Polygon
0x31957ecc43774d19f54d9968e95c69c882468b46860f921668f2c55fadd51b19
Feb 01, 2023 18:29 UTCExplorer

Victim Addresses

0x3bB7fFD08f46620beA3a9Ae7F096cF2b213768B3Polygon
0x20D50159aff262f953C8913Ec859Cac13A010b8aPolygon

Loss Breakdown

100,000,000BEUR
113,813,998.37ALBT

Similar Incidents

Root Cause Analysis

BonqDAO ALBT Oracle Manipulation via TellorFlex

1. Incident Overview TL;DR

BonqDAO accepted a permissionless TellorFlex SpotPrice(albt, usd) report inside the live ALBT collateral price path. In Polygon transaction 0x31957ecc43774d19f54d9968e95c69c882468b46860f921668f2c55fadd51b19, the adversary cluster staked 10 TRB, submitted an ALBT/USD value of 5e27, opened a fresh ALBT trove with only 0.1 ALBT, and minted 100,000,000 BEUR. In Polygon transaction 0xa02d0c3d16d6ee0e0b6a42c3cc91997c2b40c87d777136dedebe8ee0f47f32b1, the same cluster submitted 1e11 for the same Tellor query and then liquidated Bonq troves that became unhealthy under the manipulated price.

The root cause is a direct oracle-trust failure. Bonq's solvency logic treated raw Tellor reporter output as authoritative ALBT pricing without bounds checks, freshness validation, dispute delay, or trusted-source aggregation. That made both borrow() and liquidate() depend on an attacker-controlled oracle value.

2. Key Background

Bonq's ALBT collateral flow matters because the protocol uses the current token price to decide whether a trove may borrow more BEUR or must be liquidated. On-chain validation in the current validator rerun at block 38792977 confirms:

  • BonqProxy / OriginalTroveFactory at 0x3bb7ffd08f46620bea3a9ae7f096cf2b213768b3 points to TokenToPriceFeed at 0x20D50159aff262f953C8913Ec859Cac13A010b8a.
  • TokenToPriceFeed.tokenPriceFeed(ALBT) returns 0x7D4c36c79b89E1f3eA63A38C1DdB16EF8c394bc8.
  • TokenToPriceFeed.mcr(ALBT) returns 2000000000000000000, so ALBT troves must remain at or above 200% collateralization.
  • TokenToPriceFeed.tokenPrice(ALBT) returns 97690490106405379 before the exploit transaction.

The configured ALBT feed is not a local Bonq calculation. It is a converted feed that multiplies one price source by another conversion leg:

contract ConvertedPriceFeed is IPriceFeed, Constants {
  IPriceFeed public immutable priceFeed;
  IPriceFeed public immutable conversionPriceFeed;

  function price() public view override returns (uint256) {
    return (priceFeed.price() * DECIMAL_PRECISION) / conversionPriceFeed.price();
  }
}

The ALBT-specific priceFeed leg resolves to Tellor-backed pricing. TellorFlex itself is permissionless: any address that stakes the required TRB can submit a report for any query ID as long as it respects the reporting lock. The relevant TellorFlex logic is:

function depositStake(uint256 _amount) external {
    ...
    require(token.transferFrom(msg.sender, address(this), _amount));
    _updateStakeAndPayRewards(msg.sender, _stakedBalance + _amount);
}

function submitValue(
    bytes32 _queryId,
    bytes calldata _value,
    uint256 _nonce,
    bytes calldata _queryData
) external {
    ...
    require(_staker.stakedBalance >= stakeAmount, "balance must be greater than stake amount");
    require(_queryId == keccak256(_queryData), "query id must be hash of query data");
    ...
    _report.valueByTimestamp[block.timestamp] = _value;
}

Validator-side RPC checks confirm that TellorFlex.getStakeAmount() returned 10000000000000000000, so the exploit prerequisite was the public 10 TRB stake and nothing more.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK-class oracle-manipulation failure in BonqDAO's collateral solvency path. Bonq's TokenToPriceFeed contract simply forwards token pricing to an external feed, and the ALBT feed chain eventually reaches TellorPriceFeed. TellorPriceFeed.price() converts the current Tellor report directly into a uint256 and returns it without validating reporter identity, value range, report age, or dispute status. Trove accounting then multiplies ALBT collateral by that oracle output and compares the resulting collateralization ratio against mcr(). Because the oracle value is attacker-controlled after a public Tellor stake and report submission, Bonq accepts an otherwise undercollateralized borrow and later also accepts liquidations created by the opposite price move. The protocol therefore violated a core solvency invariant: borrowing and liquidation should depend on manipulation-resistant collateral prices rather than on a single permissionless reporter's current output.

4. Detailed Root Cause Analysis

4.1 Code-level breakpoint

Bonq's first critical step is the price lookup itself:

function tokenPrice(address _token) public view override returns (uint256) {
  return IPriceFeed(tokens[_token].priceFeed).price();
}

The Tellor-backed feed then exposes the raw value:

function price() public view virtual override returns (uint256) {
  return uint256(bytes32(oracle.getCurrentValue(queryId)));
}

That raw price is consumed by Bonq's trove solvency logic:

function collateralValue() public view override returns (uint256) {
  return (normalisedDecimals(collateral()) * factory.tokenToPriceFeed().tokenPrice(address(token))) / DECIMAL_PRECISION;
}

function _collateralization() private view returns (uint256) {
  if (_debt > 0) {
    return (normalisedDecimals(recordedCollateral) * factory.tokenToPriceFeed().tokenPrice(address(token))) / _debt;
  } else {
    return MAX_INT;
  }
}

function insertTrove(address _newNextTrove) private {
  require(_collateralization() >= mcr(), "41670 TCR must be > MCR");
}

function liquidate() public {
  _updateCollateral();
  require(_collateralization() < mcr(), "454f4 CR must lt MCR");
}

This is the concrete breakpoint. Once the Tellor value moves, Bonq's borrow and liquidation predicates move with it.

4.2 Upward manipulation and unbacked BEUR mint

The collector trace for tx 0x31957ecc43774d19f54d9968e95c69c882468b46860f921668f2c55fadd51b19 shows the exploit sequence directly:

TellorFlex::depositStake(10000000000000000000)
TellorFlex::submitValue(
  0x12906c5e9178631dba86f1f750f7ab7451c61e6357160eb890029b9eac1fb235,
  0x00000000000000000000000000000000000000001027e72f1f12813088000000,
  0,
  SpotPrice("albt","usd")
)
OriginalTroveFactory::createTrove(WrappedToken: [0x35b2ECE5B1eD6a7a99b83508F8ceEAB8661E0632])
Trove::increaseCollateral(0, 0x0000000000000000000000000000000000000000)
Trove::borrow(0xED596991ac5F1Aa1858Da66c67f7CFA76e54B5f1, 100000000000000000000000000, 0x0000000000000000000000000000000000000000)

The query ID in that trace is the Tellor hash for SpotPrice("albt","usd"). The submitted value bytes decode to 5000000000000000000000000000 (5e27). Validator-side RPC checks show the effect immediately:

  • Before block 38792978, TokenToPriceFeed.tokenPrice(ALBT) = 97690490106405379.
  • After tx 0x31957ecc..., TokenToPriceFeed.tokenPrice(ALBT) = 4579375226679073720614149170.

Bonq therefore saw ALBT as orders of magnitude more valuable than it was in the pre-state. The root cause's ACT success predicate is satisfied because the same transaction minted BEUR from a trove that would be below the required mcr() under the baseline price.

The balance diff makes the economic realization deterministic:

{
  "token": "0x338eb4d394a4327e5db80d08628fa56ea2fd4b81",
  "holder": "0xed596991ac5f1aa1858da66c67f7cfa76e54b5f1",
  "before": "0",
  "after": "100000000000000000000000000",
  "delta": "100000000000000000000000000"
}

That same balance diff also shows the Tellor stake movement into TellorFlex and the 0.1 ALBT movement into the newly created trove.

4.3 Downward manipulation and liquidation wave

The second seed transaction uses the same primitive in the opposite direction. The collector trace for tx 0xa02d0c3d16d6ee0e0b6a42c3cc91997c2b40c87d777136dedebe8ee0f47f32b1 shows:

TellorFlex::depositStake(10000000000000000000)
TellorFlex::submitValue(
  0x12906c5e9178631dba86f1f750f7ab7451c61e6357160eb890029b9eac1fb235,
  0x000000000000000000000000000000000000000000000000000000174876e800,
  0,
  SpotPrice("albt","usd")
)
0x454C40FC7C61B2153728871D11bEA6C56F99FE77::liquidate()
0x6e237d5fB96D9C7aEdfD37e679017849dc845502::liquidate()
0x605778C9B0938fd60634FcE0f73b908500ACb8AA::liquidate()
...

Validator-side RPC checks show the ALBT price collapsing to 91594383432 after this transaction. At that point Bonq's require(_collateralization() < mcr()) liquidation branch becomes true for many ALBT troves.

The second balance diff shows the liquidation harvest landing in the attacker path, starting with:

{
  "token": "0x35b2ece5b1ed6a7a99b83508f8ceeab8661e0632",
  "holder": "0xeec2266a0ea9d1970fe692273839dbbb6a9ae598",
  "before": "0",
  "after": "858342997708983721666464",
  "delta": "858342997708983721666464"
}

and continuing across many liquidated troves. The root cause artifact summarizes the total ALBT realization from this phase as 113813998.369826208354681311 ALBT.

4.4 Deterministic ACT framing

This is an ACT opportunity because every dependency is public:

  • the Tellor stake amount is public and permissionless;
  • the Tellor query is public and can be derived from the visible query data;
  • Bonq's trove creation, collateral increase, borrow, and liquidation entrypoints are public;
  • the relevant pre-state is observable from public RPC and verified source.

No private key, governance privilege, or non-public order flow is required.

5. Adversary Flow Analysis

The adversary cluster identified in the root cause is:

  • 0xcacf2d28b2a5309e099f0c6e8c60ec3ddf656642: EOA that sent both seed exploit transactions.
  • 0xed596991ac5f1aa1858da66c67f7cfa76e54b5f1: attacker entry contract that receives the BEUR and ALBT gains.
  • 0xbaf48429b4d30bdfad488508d3b528033331fe8a: first Tellor reporter helper used in tx 0x31957ecc....
  • 0xb5c0ba8ed0f4fb9a31fccf84b9fb3da639a1ede5: second Tellor reporter helper used in tx 0xa02d0c3d....

Their execution flow is straightforward:

  1. Acquire or route 10 TRB into a fresh helper contract.
  2. Stake into TellorFlex and submit SpotPrice("albt","usd") with a manipulated value.
  3. While the manipulated value is live, call Bonq trove functions.
  4. First realization: create a fresh ALBT trove, add 0.1 ALBT, and mint 100,000,000 BEUR.
  5. Second realization: submit the low ALBT value and execute repeated liquidations against Bonq troves that now fail the MCR check.

Bonq victim-facing addresses relevant to this path are:

  • 0x3bb7ffd08f46620bea3a9ae7f096cf2b213768b3 (BonqProxy / OriginalTroveFactory)
  • 0x20d50159aff262f953c8913ec859cac13a010b8a (TokenToPriceFeed)
  • 0x35b2ece5b1ed6a7a99b83508f8ceeab8661e0632 (ALBT WrappedToken)

6. Impact & Losses

The incident created two deterministic impacts:

  • 100,000,000 BEUR was minted into the attacker path in tx 0x31957ecc43774d19f54d9968e95c69c882468b46860f921668f2c55fadd51b19.
  • 113,813,998.369826208354681311 ALBT was accumulated during the liquidation phase summarized by tx 0xa02d0c3d16d6ee0e0b6a42c3cc91997c2b40c87d777136dedebe8ee0f47f32b1.

The non-monetary exploit predicate is equally important: Bonq accepted a borrow from a newly created ALBT trove that would be undercollateralized under the pre-manipulation price. That is the core solvency break. The profit fields in root_cause.json now quantify the first BEUR realization deterministically: attacker entry contract 0xed596991ac5f1aa1858da66c67f7cfa76e54b5f1 moved from 0 to 100000000000000000000000000 BEUR, while BEUR-denominated fees remained 0 because gas was paid in POL by the attacker EOA.

7. References

  • Seed exploit tx 1: 0x31957ecc43774d19f54d9968e95c69c882468b46860f921668f2c55fadd51b19
  • Seed exploit tx 2: 0xa02d0c3d16d6ee0e0b6a42c3cc91997c2b40c87d777136dedebe8ee0f47f32b1
  • Bonq factory / proxy: 0x3bb7ffd08f46620bea3a9ae7f096cf2b213768b3
  • TokenToPriceFeed: 0x20D50159aff262f953C8913Ec859Cac13A010b8a
  • Converted ALBT feed: 0x7D4c36c79b89E1f3eA63A38C1DdB16EF8c394bc8
  • TellorPriceFeed: 0xa1620Af6138D2754F7250299DC9024563bd1a5D6
  • TellorFlex: 0x8f55D884CAD66B79e1a131f6bCB0e66f4fD84d5B
  • ALBT token: 0x35b2ECE5B1eD6a7a99b83508F8ceEAB8661E0632
  • BEUR token: 0x338Eb4d394a4327E5dB80d08628fa56EA2FD4B81
  • Tellor query ID used in both exploit phases: 0x12906c5e9178631dba86f1f750f7ab7451c61e6357160eb890029b9eac1fb235
  • Collector evidence used for validation:
    • seed trace and balance diff for tx 0x31957ecc43774d19f54d9968e95c69c882468b46860f921668f2c55fadd51b19
    • seed trace and balance diff for tx 0xa02d0c3d16d6ee0e0b6a42c3cc91997c2b40c87d777136dedebe8ee0f47f32b1