OnChain Transaction Debugging - Lesson 6: Write Your Own PoC (Reentrancy)
Author: gbaleeee
Translation: Spark
Community Discord
This work was also published in XREX | WTF Academy
In this article, we will learn reentrancy by demonstrating a real-world attack and using Foundry to conduct tests and reproduce the attack.
Prerequisite
Understand common attack vectors in the smart contract. DeFiVulnLabs is a great resource to get started.
Know how the basic Defi model work and how smart contracts interact with others.
What Is Reentrancy Attack
Source from: Reentrancy by Consensys.
Reentrancy Attack is a popular attack vector. It almost happens every month if we look into the DeFiHackLabs database. For more information, there is another great repo that maintains a collection of reentrancy-attacks,
In short, if one function invokes an untrusted external call, there could be a risk of the reentrancy attack.
Reentrancy Attacks can be mainly identified into three types:
Single Function Reentrancy
Cross-Function Reentrancy
Cross-Contract Reentrancy
Hands-on PoC - DFX Finance
Source:Pckshield alert 11/11/2022
It seems @DFXFinance's DEX pool (named Curve) is hacked (w/ loss of 3000 ETH or $~4M) due to the lack of proper reentrancy protection. Here comes an example tx: https://etherscan.io/tx/0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7. The stolen funds are being deposited into @TornadoCash
Transaction Overview
Based on the transaction above, we can observe limited info from etherscan. It includes information about the sender (exploiter), the exploiter's contract, events during the transaction, etc. The transaction is labeled as an "MEV Transaction" and "Flashbots," indicating that the exploiter attempted to evade the impact of front-run bots.
Transaction Analysis We can use Phalcon from Blocksec to do the further investigation.
Balance Analysis In the Balance Changes section, we can see the alteration in funds with this transaction. The attack contract(receiver) collected a large amount of
USDC
, andXIDR
tokens as profit, and the contract nameddfx-xidr-v2
lost a large amount ofUSDC
andXIDR
tokens. At the same time, the address starting with0x27e8
also obtained someUSDC
andXIDR
tokens. According to the investigation of this address, this is the DFX Finance: Governance Multi-Signature wallet address.
Based on the aforementioned observations, the victim is DFX Finance's dfx-xidr-v2
contract and loss assets are USDC
and XIDR
tokens. The DFX multi-signature address also receives some tokens during the process. Based on our experience, it should relate to the fee logic.
Asset Stream Analysis We can use another tool from Blocksec called metasleuth to analyze the asset flow.
Based on the graph above, the exploiter borrowed a large amount of USDC
,XIDR
tokens from the victim contract in step [1] and [2]. In step [3] and [4], the borrowed assets were sent back to the victim contract. After that, dfx-xidr-v2
token are minted to the exploiter in step [5] and the DFX multi-sig wallet receives the fee in both USDC
and XIDR
in step [6] and [7]. In the end, dfx-xidr-v2
tokens are burned from the exploiter's address.
As a summary, the asset stream is:
The attacker borrowed
USDC
,XIDR
tokens from the victim contract.The attacker sent the
USDC
,XIDR
tokens back to the victim contract.The attacker minted
dfx-xidr-v2
tokens.DFX multi-sig wallet received
USDC
,XIDR
tokens.The attacker burned
dfx-xidr-v2
tokens.
This information can be verified with the following trace analysis.
Trace Analysis
Let's observe the transaction under expand level 2.
The complete attack transaction's function execution flow can be viewed as:
The attacker invoked function
0xb727281f
for the attack.The attacker called
viewDeposit
indfx-xidr-v2
contract viastaticcall
.The attacker triggered
flash
function indfx-xidr-v2
contract withcall
. It is worth noting that in this trace, the function0xc3924ed6
in the attack contract was used as a callback.
4. The attacker called withdraw
function in dfx-xidr-v2
contract.
Detail Analysis
The attacker's intention of calling the viewDeposit function in the first step can be found in the comment for
viewDeposit
function. The exploiter wants to obtain the number ofUSDC
,XIDR
tokens to mint 200_000 * 1e18dfx-xidr-v2
token.
And at the next step attack using the return value from viewDeposit
function as a similar value for the input of flash
function invocation(the value is not exactly the same, more details later)
The attacker invokes flash
function in the victim contract as the second step. We can get some insight from the code:
As you can see, the flash
function is similar to the flash loan in Uniswap V2. User can borrow assets via this function. And the flash
function has a callback function for the user. The code is:
IFlashCallback(msg.sender).flashCallback(fee0, fee1, data);
This invocation corresponds to the callback function in the attacker's contract in the previous trace analysis section. If we do the 4Bytes Hash verification, it is 0xc3924ed6
The last step is calling withdraw
function, and it will burn the stable token(dfx-xidr-v2
) and withdraw paired assets(USDC
,XIDR
).
POC Implementation
Based on the analysis above we can implement the PoC skeleton below:
contract EXP {
uint256 amount;
function testExploit() public{
uint[] memory XIDR_USDC = new uint[](2);
XIDR_USDC[0] = 0;
XIDR_USDC[1] = 0;
( , XIDR_USDC) = dfx.viewDeposit(200_000 * 1e18);
dfx.flash(address(this), XIDR_USDC[0] * 995 / 1000, XIDR_USDC[1] * 995 / 1000, new bytes(1)); // 5% fee
dfx.withdraw(amount, block.timestamp + 60);
}
function flashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external{
/*
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
*/
}
}
It is likely to raise the question of how an attacker steals assets with withdraw
function in a flash loan. Obviously, this is the only part the attacker can work on. Now let's dive into the callback function:
As you can see, the attacker called deposit
function in the victim contract and it will receive the numeraire assets that the pool supports and mint curves token. As mentioned in the graph above, USDC
and XIDR
are sent to the victim via transferFrom
.
At this point, it is known that the completion of the flash loan is determined by checking whether the corresponding token assets in the contract are greater than or equal to the state before the execution of the flash loan callback. And depoit
function will make this validation complete.
require(balance0Before.add(fee0) <= balance0After, 'Curve/insufficient-token0-returned');
require(balance1Before.add(fee1) <= balance1After, 'Curve/insufficient-token1-returned');
It should be noticed that the attacker prepared some USDC
and XIDR
tokens for the flash loan fee mechanism before the attack. This is why the attacker's deposit is relatively higher than the borrowed amount. So the total amount for deposit
invocation is the amount borrowed with flash loan plus the fee. The validation in the flash
function can be passed with this.
As a result, the attacker invoked deposit
in the callback function, bypassed the validation in the flash loan and left the record for deposit. After all these operations, attacker withdrew tokens.
In summary, the whole attack flow is:
Prepare some
USDC
andXIDR
tokens in advance.Using
viewDeposit()
to get the number of assets for laterdeposit()
.Flash
USDC
andXIDR
tokens based on the return value in step 2.Invoke
deposit()
function in the flash loan callback .Since we have a deposit record in the previous step, now withdraw tokens.
The full PoC implementation:
contract EXP {
uint256 amount;
function testExploit() public{
uint[] memory XIDR_USDC = new uint[](2);
XIDR_USDC[0] = 0;
XIDR_USDC[1] = 0;
( , XIDR_USDC) = dfx.viewDeposit(200_000 * 1e18);
dfx.flash(address(this), XIDR_USDC[0] * 995 / 1000, XIDR_USDC[1] * 995 / 1000, new bytes(1)); // 5% fee
dfx.withdraw(amount, block.timestamp + 60);
}
function flashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external{
(amount, ) = dfx.deposit(200_000 * 1e18, block.timestamp + 60);
}
}
More detailed codebase can be found in the DefiHackLabs repo: DFX_exp.sol
Verify Fund Flow
Now, we can verify the asset stream graph with token events during the transaction.
At the end of the deposit
function, dfx-xidr-v2
tokens were minted to the exploiter.
In the flash
function, the transfer event shows the fee collection(USDC
and XIDR
) for the DFX multi-sig wallet.
The withdraw
function burned dfx-xidr-v2
tokens that were minted in the previous steps.
Summary
DFX Finance reentrancy attack is a typical cross-function reentrancy attack, where the attacker completes the reentrancy by calling the
deposit
function in the flash loan callback function.It is worth mentioning that the technique of this attack corresponds exactly to the fourth question in CTF damnvulnerabledefi [Side Entrance. If the project developers had done it carefully before, perhaps this attack would not have happened 🤣. In December of the same year, the Deforst project was also attacked due to a similar issue.
Learning Material
Reentrancy Attacks on Smart Contracts Distilled
C.R.E.A.M. Finance Post Mortem: AMP Exploit
Cross-Contract Reentrancy Attack
Sherlock Yield Strategy Bug Bounty Post-Mortem
Decoding $220K Read-only Reentrancy Exploit | QuillAudits