/* Copyright 2020 DODO ZOO. SPDX-License-Identifier: Apache-2.0 */ pragma solidity 0.6.9; pragma experimental ABIEncoderV2; import {SafeMath} from "../lib/SafeMath.sol"; import {DecimalMath} from "../lib/DecimalMath.sol"; import {Types} from "../lib/Types.sol"; import {Storage} from "./Storage.sol"; import {Pricing} from "./Pricing.sol"; import {Settlement} from "./Settlement.sol"; /** * @title Trader * @author DODO Breeder * * @notice Functions for trader operations */ contract Trader is Storage, Pricing, Settlement { using SafeMath for uint256; // ============ Events ============ event SellBaseToken(address indexed seller, uint256 payBase, uint256 receiveQuote); event BuyBaseToken(address indexed buyer, uint256 receiveBase, uint256 payQuote); event MaintainerFee(bool isBaseToken, uint256 amount); // ============ Modifiers ============ modifier tradeAllowed() { require(_TRADE_ALLOWED_, "TRADE_NOT_ALLOWED"); _; } modifier gasPriceLimit() { require(tx.gasprice <= _GAS_PRICE_LIMIT_, "GAS_PRICE_EXCEED"); _; } // ============ Trade Functions ============ function sellBaseToken(uint256 amount, uint256 minReceiveQuote) external tradeAllowed gasPriceLimit preventReentrant returns (uint256) { // query price ( uint256 receiveQuote, uint256 lpFeeQuote, uint256 mtFeeQuote, Types.RStatus newRStatus, uint256 newQuoteTarget, uint256 newBaseTarget ) = _querySellBaseToken(amount); require(receiveQuote >= minReceiveQuote, "SELL_BASE_RECEIVE_NOT_ENOUGH"); // settle assets _baseTokenTransferIn(msg.sender, amount); _quoteTokenTransferOut(msg.sender, receiveQuote); _quoteTokenTransferOut(_MAINTAINER_, mtFeeQuote); // update TARGET _TARGET_QUOTE_TOKEN_AMOUNT_ = newQuoteTarget; _TARGET_BASE_TOKEN_AMOUNT_ = newBaseTarget; _R_STATUS_ = newRStatus; _donateQuoteToken(lpFeeQuote); emit SellBaseToken(msg.sender, amount, receiveQuote); emit MaintainerFee(false, mtFeeQuote); return receiveQuote; } function buyBaseToken(uint256 amount, uint256 maxPayQuote) external tradeAllowed gasPriceLimit preventReentrant returns (uint256) { // query price ( uint256 payQuote, uint256 lpFeeBase, uint256 mtFeeBase, Types.RStatus newRStatus, uint256 newQuoteTarget, uint256 newBaseTarget ) = _queryBuyBaseToken(amount); require(payQuote <= maxPayQuote, "BUY_BASE_COST_TOO_MUCH"); // settle assets _quoteTokenTransferIn(msg.sender, payQuote); _baseTokenTransferOut(msg.sender, amount); _baseTokenTransferOut(_MAINTAINER_, mtFeeBase); // update TARGET _TARGET_QUOTE_TOKEN_AMOUNT_ = newQuoteTarget; _TARGET_BASE_TOKEN_AMOUNT_ = newBaseTarget; _R_STATUS_ = newRStatus; _donateBaseToken(lpFeeBase); emit BuyBaseToken(msg.sender, amount, payQuote); emit MaintainerFee(true, mtFeeBase); return payQuote; } // ============ Query Functions ============ function querySellBaseToken(uint256 amount) external view returns (uint256 receiveQuote) { (receiveQuote, , , , , ) = _querySellBaseToken(amount); return receiveQuote; } function queryBuyBaseToken(uint256 amount) external view returns (uint256 payQuote) { (payQuote, , , , , ) = _queryBuyBaseToken(amount); return payQuote; } function _querySellBaseToken(uint256 amount) internal view returns ( uint256 receiveQuote, uint256 lpFeeQuote, uint256 mtFeeQuote, Types.RStatus newRStatus, uint256 newQuoteTarget, uint256 newBaseTarget ) { (newBaseTarget, newQuoteTarget) = getExpectedTarget(); uint256 sellBaseAmount = amount; if (_R_STATUS_ == Types.RStatus.ONE) { // case 1: R=1 // R falls below one receiveQuote = _ROneSellBaseToken(sellBaseAmount, newQuoteTarget); newRStatus = Types.RStatus.BELOW_ONE; } else if (_R_STATUS_ == Types.RStatus.ABOVE_ONE) { uint256 backToOnePayBase = newBaseTarget.sub(_BASE_BALANCE_); uint256 backToOneReceiveQuote = _QUOTE_BALANCE_.sub(newQuoteTarget); // case 2: R>1 // complex case, R status depends on trading amount if (sellBaseAmount < backToOnePayBase) { // case 2.1: R status do not change receiveQuote = _RAboveSellBaseToken(sellBaseAmount, _BASE_BALANCE_, newBaseTarget); newRStatus = Types.RStatus.ABOVE_ONE; if (receiveQuote > backToOneReceiveQuote) { // [Important corner case!] may enter this branch when some precision problem happens. And consequently contribute to negative spare quote amount // to make sure spare quote>=0, mannually set receiveQuote=backToOneReceiveQuote receiveQuote = backToOneReceiveQuote; } } else if (sellBaseAmount == backToOnePayBase) { // case 2.2: R status changes to ONE receiveQuote = backToOneReceiveQuote; newRStatus = Types.RStatus.ONE; } else { // case 2.3: R status changes to BELOW_ONE receiveQuote = backToOneReceiveQuote.add( _ROneSellBaseToken(sellBaseAmount.sub(backToOnePayBase), newQuoteTarget) ); newRStatus = Types.RStatus.BELOW_ONE; } } else { // _R_STATUS_ == Types.RStatus.BELOW_ONE // case 3: R<1 receiveQuote = _RBelowSellBaseToken(sellBaseAmount, _QUOTE_BALANCE_, newQuoteTarget); newRStatus = Types.RStatus.BELOW_ONE; } // count fees lpFeeQuote = DecimalMath.mul(receiveQuote, _LP_FEE_RATE_); mtFeeQuote = DecimalMath.mul(receiveQuote, _MT_FEE_RATE_); receiveQuote = receiveQuote.sub(lpFeeQuote).sub(mtFeeQuote); return (receiveQuote, lpFeeQuote, mtFeeQuote, newRStatus, newQuoteTarget, newBaseTarget); } function _queryBuyBaseToken(uint256 amount) internal view returns ( uint256 payQuote, uint256 lpFeeBase, uint256 mtFeeBase, Types.RStatus newRStatus, uint256 newQuoteTarget, uint256 newBaseTarget ) { (newBaseTarget, newQuoteTarget) = getExpectedTarget(); // charge fee from user receive amount lpFeeBase = DecimalMath.mul(amount, _LP_FEE_RATE_); mtFeeBase = DecimalMath.mul(amount, _MT_FEE_RATE_); uint256 buyBaseAmount = amount.add(lpFeeBase).add(mtFeeBase); if (_R_STATUS_ == Types.RStatus.ONE) { // case 1: R=1 payQuote = _ROneBuyBaseToken(buyBaseAmount, newBaseTarget); newRStatus = Types.RStatus.ABOVE_ONE; } else if (_R_STATUS_ == Types.RStatus.ABOVE_ONE) { // case 2: R>1 payQuote = _RAboveBuyBaseToken(buyBaseAmount, _BASE_BALANCE_, newBaseTarget); newRStatus = Types.RStatus.ABOVE_ONE; } else if (_R_STATUS_ == Types.RStatus.BELOW_ONE) { uint256 backToOnePayQuote = newQuoteTarget.sub(_QUOTE_BALANCE_); uint256 backToOneReceiveBase = _BASE_BALANCE_.sub(newBaseTarget); // case 3: R<1 // complex case, R status may change if (buyBaseAmount < backToOneReceiveBase) { // case 3.1: R status do not change // no need to check payQuote because spare base token must be greater than zero payQuote = _RBelowBuyBaseToken(buyBaseAmount, _QUOTE_BALANCE_, newQuoteTarget); newRStatus = Types.RStatus.BELOW_ONE; } else if (buyBaseAmount == backToOneReceiveBase) { // case 3.2: R status changes to ONE payQuote = backToOnePayQuote; newRStatus = Types.RStatus.ONE; } else { // case 3.3: R status changes to ABOVE_ONE payQuote = backToOnePayQuote.add( _ROneBuyBaseToken(buyBaseAmount.sub(backToOneReceiveBase), newBaseTarget) ); newRStatus = Types.RStatus.ABOVE_ONE; } } return (payQuote, lpFeeBase, mtFeeBase, newRStatus, newQuoteTarget, newBaseTarget); } }