Skip to content
Permalink
master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
0 contributors

Users who have contributed to this file

This document just tracks my notes when attempting to recreate this attack.

Step 1 - Reverse engineer the transaction

The attacker transaction has 70 logs. Each log is an emitted event, but there is no call stack.

It's a little hard to parse the event logs, because events are emitted only during certain functions. Lots of reading is required to figure out how this happens. I won't list the events from first to last, instead I'll note the most important ones in the way that makes sense.

How to read logs

The "Address" is the address at which the event was triggered. For example, Event 2's address is 0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e, which is the address of the USDC token, so we know that USDC was transferred.

The "Name" is the name of the event.

The first "Topic" is a keccak256 hash of the event name itself. For example, for the Transfer event, it would be:

  • keccak256("Transfer(address,address,uin256)") = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef

This is especially important to remember because some events don't have names. You have to look at the contract emitting it, and hash each event to see which one matches the first Topic.

All the topics after the first are each parameter to the event. Sometimes the event log will decode a parameter and show it under Data.

Events 2 and 71

In Event 2, this proxy contract (implementation contract here) transfers 51,000,000 USDC to the attacker. This proxy contract is for the token aAvaUSDC.

In order to figure out why, we look at the very last event, 71, which is a flashLoan event emitted by this contract (implementation here) through the _handleFlashLoanRepayment() method inside FlashLoanLogic.sol in the implementation contract above. This function is called by executeFlashLoanSimple(), which is called by flashLoanSimple() inside Pool.sol.

This tells us that the majority of the attack happens in the flash loan callback, as the very last event is the repayment of the flash loan, while the first event is the transfer of the tokens from the call to transferUnderlyingTo() inside executeFlashLoanSimple(). Here, transferUnderlyingTo() will call aAvaUSDC's transferUnderlyingTo() function, which will transfer the underlying token (in this case, USDC) to us.

Events 3 to 9

In all of these events, the attacker contract is approving other contracts to spend each token on the attacker's behalf. These tokens (and corresponding spenders) are:

  1. USDC - Trader Joe Router
  2. USDC.e - Curve.fi Factory Plain Pool: USD Coin
  3. USDC.e - Trader Joe Router
  4. WAVAX - Trader Joe Router
  5. JLP - DegenBox contract
  6. NXUSD - Trader Joe Router
  7. NXUSD - A Curve.fi contract. Unsure what this contract does exactly just yet

Event 10

The attacker contract now calls the DegenBox contract's setMasterContractApproval() to approve this CauldronV2.sol contract to access the attacker contract's funds.

DegenBox seems to be another version of SushiSwap's BentoBox, which you can find documentation for here. No idea what CauldronV2.sol does at this point.

Events 11 to 14

In order to understand whats going on here, we have to work backwards. The reasoning is because in event 11, the attacker sends 280,000 USDC to the JLP Token Contract, and in event 12, the JLP Token Contract sends back 14735.962350184152 WAVAX, and we have no idea why this contract would do that unless we look at the next two events.

Event 13 and 14 are a Sync and a Swap in order, both being emitted from the JLP Token Contract. Looking at the code, the swap() function will call _update() at the end before emitting the Swap event, and the _update() function emits a Sync event. We can also see that token0 and token1 are WAVAX and USDC respectively.

We can now infer that the attacker swapped USDC for WAVAX in these events.

Events 15 to 20

Let's see what happens:

15 - Attacker transfers 260,000 USDC to the JLP Token Contract 16 - Attacker transfers 13401.980954596283 WAVAX to the JLP Token Contract 17 - JLP Token Contract transfers 0.000012652872819651 JLP tokens to this MoneyMaker contract 18 - JLP Token Contract transfers 0.04533097793130507 JLP tokens to the attacker 19 - JLP Token Contract syncs its reserves 20 - JLP Token Contract mints 260,000 USDC and 13401.980954596283 WAVAX to the Trader Joe Router

So, what exactly happened here? Well, let's find why JLP Token Contract would mint anything at all to the Trader Joe Router.

In the mint() function, the Mint event is emitted at the end, and the msg.sender in this case is the Trader Joe Router.

Looking at the Trader Joe Router code, a JLP Pair token's mint() function is called in addLiquidity() and addLiquidityAVAX().

The attacker does not call addLiquidityAVAX() because, just by following the code, we can tell that this function will emit a Deposit event which we don't see.

On the flip side, addLiquidity() emits two Transfer events to the JLP Pair token before calling mint(). This is exactly what we see: The attacker transfers USDC and WAVAX to the pair token.

Where does this MoneyMaker contract come in (event 17)? Well, looking at the mint() function in the JLP Pair token, it calls _mintFee(), which gets the JoeFactory contract's feeTo storage variable, and then sends 0.05% of the LP tokens that it's about to mint. This feeTo variable happens to point at the MoneyMaker contract.

Finally, the Mint event is emitted. Note these tokens aren't actually sent to the Trader Joe Router, it's just the event's sender is the router itself.

So, based on all of this, we know the attacker added liquidity to this pair by calling the router's addLiquidity() function.

Events 21 to 24

Looks familiar. The attacker swaps 50,460,000 USDC for 505213.7502091872 WAVAX tokens.

Events 25 to 30

This one was actually a very difficult one to figure out. First thing to know is that each log has a topic, and the topic is a 32 byte word that describes an event.

This 32 byte word is the keccak256 hash of the event itself.

For example, for the LogBorrow event inside the CauldronV2 contract where event 25 is coming from, it's defined as:

event LogBorrow(address indexed from, address indexed to, uint256 amount, uint256 part, address collateral);

Therefore, the topic for this event would be:

keccak256("LogBorrow(address,address,uint256,uint256,address)")
=
0xa7a43160f40531d706d40a466a7d0e9ab2b6725f705d28ac6f4dd6280b940d25

Why is this important? Well, events 25, 26, and 28 have no name! I don't know why, but we can tell that they're coming from the the aforementioned Cauldron contract. Looking at the events in it and doing hashing each one, we know the following:

25 - LogExchangeRate 26 - LogAccrue 27 - LogTransfer from the DegenBox contract 28 - LogBorrow 29 - Transfer 30 - LogWithdraw from the DegenBox contract

Ok, so whats happening? LogExchangeRate comes from a call to updateExchangeRate(), which uses an oracle to update the exchange rate between the collateral and the asset. In this case, the collateral is the WAVAX/USDC JLP Token pair, easily confirmed by attempting to read the collateral storage variable.

Checking the exchange rate before and after experimentally, we have 88793190826 vs 32701350550 (in wei). Now, calculating how much the attacker can borrow with the 0.04533097793130507 WAVAX/USDC LP tokens they have, we get:

  1. Before - 0.04533097793130507 / (88793190826 / 1e18) = 510523.132569209 NXUSD
  2. After - 0.04533097793130507 / (32701350550 / 1e18) = 1386211.186048555 NXUSD

So.. The attacker put in ~520,000 worth of USDC into the liquidity pool to get this much LP tokens. Now they can use the same amount of LP tokens as collateral to borrow ~1.38m dollars worth of NXUSD tokens!

Now, obviously collateral is used to borrow something else, so what are we borrowing? Well, the LogAccrue event comes from the accrue() function, and the LogBorrow event comes from the _borrow() internal function. Looking at the external borrow() function:

function borrow(address to, uint256 amount) public solvent virtual returns (uint256 part, uint256 share) {
    accrue();
    (part, share) = _borrow(to, amount);
}

Pretty convenient, this must be where events 26-28 are coming from.

accrue() is used to update some storage variables to account for interest on all tokens that have been currently borrowed from this contract, namely the totalBorrow storage variable.

borrow() is now called. The LogTransfer event in this function doesn't actually mean the tokens were transferred to the attacker yet. It only logs that the attacker "borrowed" the tokens, but until we see a Transfer event to the attacker's account, we can assume the attacker doesn't have the tokens yet. This LogTransfer event is followed by a LogBorrow event.

One quick quirk here is that when the borrow() function calls the DegenBox contract's transfer() function, there is a modifier on the function that checks of the from address matches msg.sender. This would only be the case if the CauldronV2 contract is calling on behalf of us (or if someone is being phished I guess). In this case, it checks if the CauldronV2's master contract has approval to do things on our behalf. That's why the attacker had to make that master contract approval at the beginning.

Why couldn't the attacker use the master CauldronV2? Well, because the master CauldronV2 doesn't have an oracle set! The slave one does though, so that allows the attacker to updateExchangeRate() after pumping the price.

After all of that, we see a Transfer followed by a LogWithdraw. This comes from the withdraw() function in the DegenBox contract. This time the attacker calls the function directly without going through the CauldronV2 contract. The function just transfers the user the NXUSD tokens (all 998,000 of them).. Why? I have no idea

Events 31 to 34

The attacker deposits their entire balance of WAVAX/USDC JLP Pair tokens by calling the deposit() function on the DegenBox contract.

Then, a LogTransfer followed by another unnamed event comes from the CauldronV2 contract. This time, it's the LogAddCollateral event. The addCollateral() function is called

These two steps essentially:

  1. Transfer the JLP Pair tokens to the DegenBox contract.
  2. Tracks them as collateral that the attacker has provided to the CauldronV2 contract.

Events 35 to 38

The attacker swaps 506,547.7316047751 WAVAX for 50,426,896.250037 USDC

Right now, the attacker has 998,000 NXUSD + 50,426,896 USDC. Assuming both are equal, the attacker now has 51,424,896 USDC, when they started off with a flash loan of 51 million. The premium in this case is 25,500, so the attacker can get away with almost 400,000 USDC profit here.

Past event 39

Past this point, it was pretty hard to figure out what the attacker was doing.

Looking at the transaction itself (not the logs), I decided to find a similar transaction in the same contract the attacker transfers his NXUSD into, and I found a very similar looking transaction where the output is avUSDC, just like in the attacker's transaction (the attacker later on goes on to convert the avUSDC to USDC.e, but we'll look at that later).

That transaction in question is this one. You can see the similarities between the attacker's transaction and this one.

The function being called here is exchange_underlying(), with the pool being this pool

Following this function, it calls the pool's exchange_underlying() function, which emits TokenExchangeUnderlying event. Looking for this event in the logs, we see it in event 50. With this, we can verify that the attacker called this function with i = 0 (NXUSD token index), and j = 2 (which ends up being the avUsdc token, because they access the j-1 index inside the underlying_coins[] array).

The avUsdc token is wrapped into USDC.e, and then sent back to the attacker. From here, the attacker can just use the Trader Joe Router to swap USDC.e for USDC, and the exploit is complete.