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