Vault.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "../interfaces/IVault.sol";
import "../interfaces/IPortfolio.sol";
import "./OrderBooks.sol";
import "./TradePairs.sol";
import "../library/Bytes32Library.sol";
import "../library/StringLibrary.sol";

contract Vault is ReentrancyGuard, IVault {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;
    using StringLibrary for string;
    using Bytes32Library for bytes32;

    struct Position {
        uint256 size;
        uint256 collateral;
        uint256 averagePrice;
        uint256 entryFundingRate;
        uint256 reserveAmount;
        int256 realisedPnl;
    }

    uint256 public constant BASIS_POINTS_DIVISOR = 10000;
    uint256 public constant FUNDING_RATE_PRECISION = 1000000;
    uint256 public constant PRICE_PRECISION = 10 ** 30;
    uint256 public constant MIN_LEVERAGE = 10000; // 1x
    uint256 public constant MAX_FEE_BASIS_POINTS = 500; // 5%
    uint256 public constant MAX_LIQUIDATION_FEE_USD = 100 * PRICE_PRECISION; // 100 USD
    uint256 public constant MIN_FUNDING_RATE_INTERNAL = 1 hours;
    uint256 public constant MAX_FUNDING_RATE_FACTOR = 10000; // 1 %

    bool public isInitialized;
    bool public isMintingEnabled = false;

    address public router;

    uint256 public marginFeeBasisPoints = 10; // 0.1 %

    uint256 public maxGasPrice;
    mapping(address => uint256) public feeReserves;
    bool public includeAmmPrice = true;

    mapping (address => mapping (address => bool)) public approvedRouters;

    mapping (address => uint256) public minProfitBasisPoints;

    // tokenBalances is used only to determin _transferIn values
    mapping (address => uint256) public tokenBalances;

    mapping (address => uint256) public override cumulativeFundingRates;
    mapping (address => uint256) public lastFundingTimes;

    mapping (bytes32 => Position) public positions;
    
    event UpdatePnl(bytes32 key, bool hasProfit, uint256 delta);
    event CollectMarginFees(bytes32 tradePairId, uint256 feeUsd, uint256 feeTokens);

    event IncreasePosition(
        bytes32 key,
        address account,
        bytes32 tradePairId,
        uint256 collateralDelta,
        uint256 sizeDelta,
        bool isLong,
        uint256 price,
        uint256 fee
    );

    event DecreasePosition(
        bytes32 key,
        address account,
        address collateralToken,
        address indexToken,
        uint256 collateralDelta,
        uint256 sizeDelta,
        bool isLong,
        uint256 price,
        uint256 fee
    );

    event LiquidatePosition(
        bytes32 key,
        address account,
        address collateralToken,
        address indexToken,
        bool isLong,
        uint256 size,
        uint256 collateral,
        uint256 reserveAmount,
        int256 realisedPnl,
        uint256 markPrice
    );

    event UpdatePosition(
        bytes32 key,
        uint256 size,
        uint256 collateral,
        uint256 averagePrice,
        uint256 entryFundingRate,
        uint256 reserveAmount,
        int256 realisedPnl
    );

    event ClosePosition(
        bytes32 key,
        uint256 size,
        uint256 collateral,
        uint256 averagePrice,
        uint256 entryFundingRate,
        uint256 reserveAmount,
        int256 realisedPnl
    );
    event IncreaseReservedAmount(address token, uint256 amount);
    event DecreaseReservedAmount(address token, uint256 amount);

    OrderBooks private orderBooks;
    IPortfolio private portfolio;
    TradePairs private tradePairs;

    constructor(address _orderBooks, address _portfolio, address _tradePairs) {
        orderBooks = OrderBooks(_orderBooks);
        tradePairs = TradePairs(_tradePairs);
        portfolio = IPortfolio(_portfolio);
    }

    function increasePosition(address _account, bytes32 _tradePairId, uint256 _sizeDelta, bool _isLong) external override nonReentrant {
        bytes32 key = getPositionKey(_account, _tradePairId, _isLong);
        Position storage position = positions[key];
        (uint256 price, address collateralToken) = getIncreasePrice(_tradePairId, _isLong);
        if (position.size == 0) {
            position.averagePrice = price;
        }

        if (position.size > 0 && _sizeDelta > 0) {
            position.averagePrice = getNextAveragePrice(position.size, position.averagePrice, _isLong, price, _sizeDelta);
        }

        uint256 fee = _collectMarginFees(_tradePairId, _sizeDelta, price);
        uint256 collateralDelta = _transferIn(collateralToken);
        position.collateral = position.collateral.add(collateralDelta);
        require(position.collateral >= fee, "Vault: insufficient collateral for fees");

        position.collateral = position.collateral.sub(fee);
        feeReserves[collateralToken] = feeReserves[collateralToken].add(fee);
        // position.entryFundingRate = cumulativeFundingRates[_collateralToken];
        position.size = position.size.add(_sizeDelta);

        require(position.size > 0, "Vault: invalid position.size");
        // // reserves tokens to pay profits on the position
        uint256 reserveDelta = usdToToken(_tradePairId, _sizeDelta, price);
        position.reserveAmount = position.reserveAmount.add(reserveDelta);
    }

    function getIncreasePrice(bytes32 _tradePairId, bool _isLong) internal view returns (uint256, address) {
        bytes32 baseSymbol = tradePairs.getSymbol(_tradePairId, true);
        IERC20Upgradeable collateralToken = portfolio.getToken(baseSymbol);
        bytes32 _buyBookId = string(abi.encodePacked(_tradePairId.bytes32ToString(), '-BUYBOOK')).stringToBytes32();
        bytes32 _sellBookId = string(abi.encodePacked(_tradePairId.bytes32ToString(), '-SELLBOOK')).stringToBytes32();
        uint256 price = _isLong ? orderBooks.first(_sellBookId) : orderBooks.last(_buyBookId);
        return (price, address(collateralToken));
    }

    function getDecreasePrice(bytes32 _tradePairId, bool _isLong) internal view returns (uint256, address) {
        bytes32 baseSymbol = tradePairs.getSymbol(_tradePairId, true);
        IERC20Upgradeable collateralToken = portfolio.getToken(baseSymbol);
        bytes32 _buyBookId = string(abi.encodePacked(_tradePairId.bytes32ToString(), '-BUYBOOK')).stringToBytes32();
        bytes32 _sellBookId = string(abi.encodePacked(_tradePairId.bytes32ToString(), '-SELLBOOK')).stringToBytes32();
        uint256 price = _isLong ? orderBooks.first(_sellBookId) : orderBooks.last(_buyBookId);
        return (price, address(collateralToken));
    }


    function decreasePosition(address _account, bytes32 _tradePairId, uint256 _collateralDelta, uint256 _sizeDelta, bool _isLong, address _receiver) external override nonReentrant returns (uint256) {
        bytes32 key = getPositionKey(_account, _tradePairId, _isLong);
        Position storage position = positions[key];
        (uint256 price, address collateralToken) = getDecreasePrice(_tradePairId, _isLong);
        require(position.size > 0, "Vault: empty position");
        require(position.collateral >= _collateralDelta, "Vault: position collateral exceeded");
        {
            uint256 reserveDelta = position.reserveAmount.mul(_sizeDelta).div(position.size);
            position.reserveAmount = position.reserveAmount.sub(reserveDelta);
        }

        (uint256 usdOut, uint256 usdOutAfterFee) = _reduceCollateral(key, _tradePairId, _collateralDelta, _sizeDelta, price, _isLong);
        if (position.size != _sizeDelta) {
            position.size = position.size.sub(_sizeDelta);
        } else {
            delete positions[key];
        }
        if (usdOut > 0) {
            uint256 amountOutAfterFees = usdToToken(_tradePairId, usdOutAfterFee, price);
            _transferOut(collateralToken, amountOutAfterFees, _receiver);
            return usdOut;
        }
        return 0;
    }

    function getPositionKey(address _account, bytes32 _tradePairId, bool _isLong) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(
            _account,
            _tradePairId,
            _isLong
        ));
    }

    function getNextAveragePrice(uint256 _size, uint256 _averagePrice, bool _isLong, uint256 _nextPrice, uint256 _sizeDelta) public view returns (uint256) {
        (bool hasProfit, uint256 delta) = getDelta(_size, _averagePrice, _nextPrice, _isLong);
        uint256 nextSize = _size.add(_sizeDelta);
        uint256 divisor;
        if (_isLong) {
            divisor = hasProfit ? nextSize.add(delta) : nextSize.sub(delta);
        } else {
            divisor = hasProfit ? nextSize.sub(delta) : nextSize.add(delta);
        }
        return _nextPrice.mul(nextSize).div(divisor);
    }

    function _reduceCollateral(bytes32 _positionKey, bytes32 _tradePairId, uint256 _collateralDelta, uint256 _sizeDelta, uint256 _price, bool _isLong) private returns (uint256, uint256) {
        Position storage position = positions[_positionKey];

        uint256 fee = _collectMarginFees(_tradePairId, _sizeDelta, _price);
        bool hasProfit;
        uint256 adjustedDelta;

        // scope variables to avoid stack too deep errors
        {
            (bool _hasProfit, uint256 delta) = getDelta(position.size, position.averagePrice, _price, _isLong);
            hasProfit = _hasProfit;
            // get the proportional change in pnl
            adjustedDelta = _sizeDelta.mul(delta).div(position.size);
        }

        uint256 usdOut;
        // transfer profits out
        if (hasProfit && adjustedDelta > 0) {
            position.collateral = position.collateral + usdToToken(_tradePairId, adjustedDelta, _price);
            position.realisedPnl = position.realisedPnl + int256(adjustedDelta);

            // pay out realised profits from the pool amount for short positions
        }

        if (!hasProfit && adjustedDelta > 0) {
            if (position.collateral > usdToToken(_tradePairId, adjustedDelta, _price))
            {
                position.collateral = position.collateral - usdToToken(_tradePairId, adjustedDelta, _price);
            } else {
                position.collateral = 0;
            }
            // transfer realised losses to the pool for short positions
            // realised losses for long positions are not transferred here as
            position.realisedPnl = position.realisedPnl - int256(adjustedDelta);
        }

        // reduce the position's collateral by _collateralDelta
        // transfer _collateralDelta out
        if (_collateralDelta > 0) {
            usdOut = usdOut.add(_collateralDelta);
            position.collateral = position.collateral.sub(usdToToken(_tradePairId, _collateralDelta, _price));
        }

        // if the position will be closed, then transfer the remaining collateral out
        if (position.size == _sizeDelta) {
            usdOut = usdOut.add(tokenToUsd(_tradePairId, position.collateral, _price));
            position.collateral = 0;
        } else {
            usdOut = usdOut.add(tokenToUsd(_tradePairId, position.collateral*_sizeDelta/position.size, _price));
            position.collateral = position.collateral - position.collateral*_sizeDelta/position.size;
        }

        // if the usdOut is more than the fee then deduct the fee from the usdOut directly
        // else deduct the fee from the position's collateral
        uint256 usdOutAfterFee = usdOut;
        if (usdOut > tokenToUsd(_tradePairId, fee, _price)) {
            usdOutAfterFee = usdOut.sub(tokenToUsd(_tradePairId, fee, _price));
            position.collateral = position.collateral.sub(fee);
        } else {
            usdOutAfterFee = 0;
            position.collateral = 0;
        }

        emit UpdatePnl(_positionKey, hasProfit, adjustedDelta);

        return (usdOut, usdOutAfterFee);
    }

    function _collectMarginFees(bytes32 _tradePairId, uint256 _sizeDelta, uint256 _price) private returns (uint256) {
        uint256 feeUsd = getPositionFee(_sizeDelta);

        // uint256 fundingFee = getFundingFee(_token, _size, _entryFundingRate);
        // feeUsd = feeUsd.add(fundingFee);
        uint256 fee = usdToToken(_tradePairId, feeUsd, _price);
        emit CollectMarginFees(_tradePairId, fee, 0);
        return fee;
    }

    function getFundingFee(address _token, uint256 _size, uint256 _entryFundingRate) public view returns (uint256) {
        if (_size == 0) { return 0; }

        uint256 fundingRate = cumulativeFundingRates[_token].sub(_entryFundingRate);
        if (fundingRate == 0) { return 0; }

        return _size.mul(fundingRate).div(FUNDING_RATE_PRECISION);
    }

    function getPositionFee(uint256 _sizeDelta) public view returns (uint256) {
        if (_sizeDelta == 0) { return 0; }
        uint256 afterFeeUsd = _sizeDelta.mul(BASIS_POINTS_DIVISOR.sub(marginFeeBasisPoints)).div(BASIS_POINTS_DIVISOR);
        return _sizeDelta.sub(afterFeeUsd);
    }

    function _transferIn(address _token) private returns (uint256) {
        uint256 prevBalance = tokenBalances[_token];
        uint256 nextBalance = IERC20(_token).balanceOf(address(this));
        tokenBalances[_token] = nextBalance;

        return nextBalance.sub(prevBalance);
    }

    function _transferOut(address _token, uint256 _amount, address _receiver) private {
        IERC20(_token).safeTransfer(_receiver, _amount);
        tokenBalances[_token] = IERC20(_token).balanceOf(address(this));
    }

    function usdToToken(bytes32 _tradePairId, uint256 _amount, uint256 _price) public view returns (uint256) {
        if (_price == 0) { return 0; }
        uint8 baseDecimals = tradePairs.getDecimals(_tradePairId, true);
        return _amount * 10**baseDecimals /_price;
    }

    function tokenToUsd(bytes32 _tradePairId, uint256 _amount, uint256 _price) public view returns (uint256) {
        uint8 baseDecimals = tradePairs.getDecimals(_tradePairId, true);
        return _amount * _price / 10 ** baseDecimals;
    }

    function getDelta(uint256 _size, uint256 _averagePrice, uint256 _nextPrice, bool _isLong) public override view returns (bool, uint256){
        require(_averagePrice > 0, "Vault: invalid _averagePrice");
        uint256 priceDelta = _averagePrice > _nextPrice ? _averagePrice.sub(_nextPrice) : _nextPrice.sub(_averagePrice);
        uint256 delta = _size.mul(priceDelta).div(_averagePrice);

        bool hasProfit;

        if (_isLong) {
            hasProfit = _nextPrice > _averagePrice;
        } else {
            hasProfit = _averagePrice > _nextPrice;
        }
        return (hasProfit, delta);
    }
}

Last updated