newland-rise 关键契约: Goblin and Strategy
2021-06-08

Strategy

简介

newland-rise定义了三种金融策略来管理用户的资产。newland-rise这三种Strategy均面向借钱投资场景。

类似从花呗借钱买基金...

MdxStrategyAddTwoSidesOptimal 双边最优策略

简介

该Strategy继承自Piloit项目的理财策略。
理解起来比较简单,即Strategy将拥有的Token0和Token1余额进行调整。使其比例刚好是当前市场价格。以这样的比例AddLiquidity效果最佳。

optimalDeposit


计算最优Deposit数量

calAndSwap


通过optimalDeposit函数获取需要调整的数量。
把strategy中所持有的t0和t1 数量进行调整,调整后的t0和t1价值相等。
如果此时addLiquidity,即是最优方案。

这里价值相等的定义是Mdx交易所的pool配比。

safeUnWrapperAndAllSend

这个函数就是简单的转账。如果要转账的token是WHT,就先withdraw成HT再转。
HT转账的时候,调用的函数是safeTransferETH,存在重入风险。
不过所有调用了safeUnWrapperAndAllSend函数的地方都有nonReentrant修饰符。
非常强力。

execute 核心函数

调用execute时,输入参数如下

  • user 受益人,在newland项目中,该值从Bank合约一路传下来,指的就是tx.origin
  • borrowToken tx.origin借款币种
  • borrow tx.origin借款数量
  • debt 未使用
  • data 额外参数,解析成如下。用户想要进行理财的输入资金,和最小LPToken输出数量
    • _token0
    • _token1
    • token0Amount
    • token1Amount
    • _minLPAmount

      函数先将用户输入的t0 t1做一次预处理,处理成ERC20token格式,使用calAndSwap函数进行调平。
      调平后AddLiquidity。
      最后
  • 将获取的lptoken转给msg.sender
  • 将剩余的borrowToken转给msg.sender
  • 将剩余的另一种代币直接转给user

MdxStrategyWithdrawMinimizeTrading 还一部分,获益一部分

简介

该策略的输入是LPToken和debt。

  • LPToken 是 用户托管于Goblin的凭证
  • debt 是用户借了多少钱来投资

Strategy将lptoken全部removeLiquidity为token0和token1。
由于token0和token1中有一部分钱是用户借来的,还需要偿还debt。
最终剩下的token,便是用户的收益。

swapIfNeed

该函数用于判断,是否需要通过swap操作来有足够的钱偿还贷款。

    /// swap if need.
    function swapIfNeed(address borrowToken, address tokenRelative, uint256 debt) internal {
        uint256 borrowTokenAmount = borrowToken.myBalance();
        if (debt > borrowTokenAmount) {
            tokenRelative.safeApprove(address(router), 0);
            tokenRelative.safeApprove(address(router), uint256(-1));

            uint256 remainingDebt = debt.sub(borrowTokenAmount);
            address[] memory path = new address[](2);
            path[0] = tokenRelative;
            path[1] = borrowToken;
            router.swapTokensForExactTokens(remainingDebt, tokenRelative.myBalance(), path, address(this), now); 
        }
    }

execute 核心函数

    /// @dev Execute worker strategy. Take LP tokens. Return debt token + token want back.
    /// @param user User address to withdraw liquidity.
    /// @param borrowToken The token user borrow from bank.
    /// @param debt User's debt amount.
    /// @param data Extra calldata information passed along to this strategy.
    function execute(address user, address borrowToken, uint256 /* borrow */, uint256 debt, bytes calldata data)
    external
    override
    payable
    nonReentrant
    {
        // 1. Find out lpToken and liquidity.
        // whichWantBack: 0:token0;1:token1;2:token what surplus.
        (address token0, address token1, uint whichWantBack) = abi.decode(data, (address, address, uint));

        // is borrowToken is ht.
        bool isBorrowHt = borrowToken == address(0);
        borrowToken = isBorrowHt ? wht : borrowToken;

        // the relative token when token0 or token1 is ht.
        address htRelative = address(0);
        {
            if (token0 == address(0)){
                token0 = wht;
                htRelative = token1;
            }
            if (token1 == address(0)){
                token1 = wht;
                htRelative = token0;
            }
        }
        require(borrowToken == token0 || borrowToken == token1, "borrowToken not token0 and token1");
        require(whichWantBack == uint(0) || whichWantBack == uint(1) || whichWantBack == uint(2),
            "whichWantBack not in (0,1,2)");

        address tokenUserWant = whichWantBack == uint(0) ? token0 : token1;

        IMdexPair lpToken = IMdexPair(factory.getPair(token0, token1));
        token0 = lpToken.token0();
        token1 = lpToken.token1();

        {
            lpToken.approve(address(router), uint256(-1));
            router.removeLiquidity(token0, token1, lpToken.balanceOf(address(this)), 0, 0, address(this), now);
        }
        {
            address tokenRelative = borrowToken == token0 ? token1 : token0;

            swapIfNeed(borrowToken, tokenRelative, debt);

            if (isBorrowHt) {
                IWHT(wht).withdraw(debt);
                SafeToken.safeTransferETH(msg.sender, debt);
            } else {
                SafeToken.safeTransfer(borrowToken, msg.sender, debt);
            }
        }

        // 2. swap remaining token to what user want.
        if (whichWantBack != uint(2)) {
            address tokenAnother = tokenUserWant == token0 ? token1 : token0;
            uint256 anotherAmount = tokenAnother.myBalance();
            if(anotherAmount > 0){
                tokenAnother.safeApprove(address(router), 0);
                tokenAnother.safeApprove(address(router), uint256(-1));

                address[] memory path = new address[](2);
                path[0] = tokenAnother;
                path[1] = tokenUserWant;
                router.swapExactTokensForTokens(anotherAmount, 0, path, address(this), now);
            }
        }

        // 3. send all tokens back.
        if (htRelative == address(0)) {
            token0.safeTransfer(user, token0.myBalance());
            token1.safeTransfer(user, token1.myBalance());
        } else {
            safeUnWrapperAndAllSend(wht, user);
            safeUnWrapperAndAllSend(htRelative, user);
        }
    }

调用execute时,输入参数如下

  • user 受益人 tx.origin
  • borrowToken tx.origin借款币种
  • borrow 未使用
  • debt 借款借了多少
  • data 额外参数,解析成如下。
    • _token0
    • _token1
    • whichWantBack (0, 1, 2) 三选一,选择0或1即只要其中某种代币;选择2就是不需要进行整合,直接分别获取token0和token1。

函数先将LPToken removeLiquidity成token0 和 token1
再通过SwapIfNeed 还清贷款。
最终将剩余的token汇总成whichWantBack token 并转给user。

MdxLiqStrategy 全部收益拿来还钱

简介

MdxLiqStrategy策略是newland-rise在Pilot上新增的。
该策略与MdxStrategyWithdrawMinimizeTrading类似,输入LPToken,removeLiquidity为token0和token1。
但在MdxLiqStrategy中,Strategy将全部的token0和token1整合成borrowToken并归还Goblin。
并没有在Strategy层分发用户的收益。

execute 核心函数

function execute(address /* user */, address borrowToken, uint256 /* borrow */, uint256 /* debt */, bytes calldata data)
    external
    override
    payable
    nonReentrant
    {
        IMdexPair lpToken = IMdexPair(abi.decode(data, (address)));
        address token0 = lpToken.token0();
        address token1 = lpToken.token1();

        // is borrowToken is ht.
        bool isBorrowHt = borrowToken == address(0);
        borrowToken = isBorrowHt ? wht : borrowToken;

        require(borrowToken == token0 || borrowToken == token1, "borrowToken not token0 and token1");

        {
            lpToken.approve(address(router), uint256(-1));
            router.removeLiquidity(token0, token1, lpToken.balanceOf(address(this)), 0, 0, address(this), now);
        }
        {
            address tokenRelative = borrowToken == token0 ? token1 : token0;
            swapToBorrowToken(borrowToken, tokenRelative);
            if (isBorrowHt) {
                IWHT(wht).withdraw(borrowToken.myBalance());
                SafeToken.safeTransferETH(msg.sender, borrowToken.myBalance());
            } else {
                SafeToken.safeTransfer(borrowToken, msg.sender, borrowToken.myBalance());
            }
        }
    }

调用execute时,输入参数如下

  • borrowToken tx.origin借款币种
  • data 额外参数,解析成如下。
    • lpToken

函数先将LPToken removeLiquidity成token0 和 token1
把所有token汇总成borrowToken, 转给Goblin

Goblin

简介

在newland-rise项目中,只有Goblin合约调用了Strategy。
Goblin是newland-rise项目中用户资金的管理者。
用户通过Bank借钱,把借到的钱 和 自己的本金 交给Goblin进行投资。

health && healthOracle

health和healthOracle函数,其目的都是判断用户的仓位状态是否正常,如果不正常即可被清算。

    /// @dev Return the amount of debt token to receive if we are to liquidate the given position.
    /// @param id The position ID to perform health check.
    /// @param borrowToken The token this position had debt.
    function healthOracle(uint256 id, address borrowToken) external override view returns (uint256) { // 哥布林把手里的钱计算成 borrowToken为单位的价格
        bool isDebtHt = borrowToken == address(0);
        require(borrowToken == token0 || borrowToken == token1 || isDebtHt, "borrowToken not token0 and token1");

        // 1. Get the position's LP balance and LP total supply.
        uint256 lpBalance = posLPAmount[id];
        uint256 lpSupply = lpToken.totalSupply();
        // Ignore pending mintFee as it is insignificant
        // 2. Get the pool's total supply of token0 and token1.
        (uint256 totalAmount0, uint256 totalAmount1,) = lpToken.getReserves();

        // 3. Convert the position's LP tokens to the underlying assets.
        uint256 userToken0 = lpBalance.mul(totalAmount0).div(lpSupply);
        uint256 userToken1 = lpBalance.mul(totalAmount1).div(lpSupply);
        uint256 userAmtProduct = userToken0.mul(userToken1);

        if (isDebtHt) {
            borrowToken = token0 == wht ? token0 : token1;
        }

        (int token0Price,) = oracle.getPrice(token0);
        (int token1Price,) = oracle.getPrice(token1);
        require(token0Price > 0, 'oracle token0Price invalid');
        require(token1Price > 0, 'oracle token1Price invalid');
        uint256 priceProduct = uint(token0Price).mul(uint(token1Price));
        uint totalAmt = Math.sqrt(userAmtProduct.mul(priceProduct)).mul(2);
        // 4. Convert all farming tokens to debtToken and return total amount.
        if (borrowToken == token0) {
            return totalAmt.div(uint(token0Price));
        } else {
            return totalAmt.div(uint(token1Price));
        }
    }

Goblin先获取输入id对应的仓位里储存的LPToken

        uint256 lpBalance = posLPAmount[id];
        uint256 lpSupply = lpToken.totalSupply();

再将LPToken用borrowToken作为单位来计算价值。

        // 3. Convert the position's LP tokens to the underlying assets.
        uint256 userToken0 = lpBalance.mul(totalAmount0).div(lpSupply);
        uint256 userToken1 = lpBalance.mul(totalAmount1).div(lpSupply);
        uint256 userAmtProduct = userToken0.mul(userToken1);

        if (isDebtHt) {
            borrowToken = token0 == wht ? token0 : token1;
        }

        (int token0Price,) = oracle.getPrice(token0);
        (int token1Price,) = oracle.getPrice(token1);
        require(token0Price > 0, 'oracle token0Price invalid');
        require(token1Price > 0, 'oracle token1Price invalid');
        uint256 priceProduct = uint(token0Price).mul(uint(token1Price));
        uint totalAmt = Math.sqrt(userAmtProduct.mul(priceProduct)).mul(2);
        // 4. Convert all farming tokens to debtToken and return total amount.
        if (borrowToken == token0) {
            return totalAmt.div(uint(token0Price));
        } else {
            return totalAmt.div(uint(token1Price));
        }

上述代码转化成数学公式为

healthOracle=2t0P0t1P1P0healthOracle = \frac{2\sqrt{t_0 * P_0 * t_1 * P_1}}{P_0}

可见是计算了一下几何平均数,几何平均数相对于算数平均数更加安全。因为它将t0和t1相乘,即得到K。又由于K值守恒,使攻击者无法通过闪电贷等方法操纵t0和t1的比例,进而无法操纵币价。

work函数

work函数
Goblin接受Bank的指令,调取Strategy进行相关动作。把收到的borrowToken向上层传递。
如果是加仓操作,Goblin会跟Chef进行交互,实现farming同时存farming凭证的功能。