diff --git a/contracts/DODOGasSavingPool/GasSavingPool/impl/GSP.sol b/contracts/DODOGasSavingPool/GasSavingPool/impl/GSP.sol new file mode 100644 index 0000000..274f6f2 --- /dev/null +++ b/contracts/DODOGasSavingPool/GasSavingPool/impl/GSP.sol @@ -0,0 +1,139 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.8.16; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {GSPTrader} from "./GSPTrader.sol"; +import {GSPFunding} from "./GSPFunding.sol"; +import {GSPVault} from "./GSPVault.sol"; + +/** + * @title DODO GasSavingPool + * @author DODO Breeder + * + * @notice DODO GasSavingPool initialization + */ +contract GSP is GSPTrader, GSPFunding { + /** + * @notice Function will be called in factory, init risk should not be included. + * @param maintainer The dodo's address, who can claim mtFee and own this pool + * @param admin oracle owner address, who can set price. + * @param baseTokenAddress The base token address + * @param quoteTokenAddress The quote token address + * @param lpFeeRate The rate of lp fee, with 18 decimal + * @param mtFeeRate The rate of mt fee, with 18 decimal + * @param i The oracle price, possible to be changed only by maintainer + * @param k The swap curve parameter + * @param priceLimit The limit of the setting range of the I + * @param isOpenTWAP Useless, always false, just for compatible with old version pool + */ + function init( + address maintainer, + address admin, + address baseTokenAddress, + address quoteTokenAddress, + uint256 lpFeeRate, + uint256 mtFeeRate, + uint256 i, + uint256 k, + uint256 priceLimit, + bool isOpenTWAP + ) external { + // GSP can only be initialized once + require(!_GSP_INITIALIZED_, "GSP_INITIALIZED"); + // _GSP_INITIALIZED_ is set to true after initialization + _GSP_INITIALIZED_ = true; + // baseTokenAddress and quoteTokenAddress should not be the same + require(baseTokenAddress != quoteTokenAddress, "BASE_QUOTE_CAN_NOT_BE_SAME"); + // _BASE_TOKEN_ and _QUOTE_TOKEN_ should be valid ERC20 tokens + _BASE_TOKEN_ = IERC20(baseTokenAddress); + _QUOTE_TOKEN_ = IERC20(quoteTokenAddress); + + // i should be greater than 0 and less than 10**36 + require(i > 0 && i <= 10**36); + _I_ = i; + // k should be greater than 0 and less than 10**18 + require(k <= 10**18); + _K_ = k; + + // _LP_FEE_RATE_ is set when initialization + _LP_FEE_RATE_ = lpFeeRate; + // _MT_FEE_RATE_ is set when initialization + _MT_FEE_RATE_ = mtFeeRate; + // _MAINTAINER_ is set when initialization, the address receives the fee + _MAINTAINER_ = maintainer; + _ADMIN_ = admin; + + _PRICE_LIMIT_ = priceLimit; + // _IS_OPEN_TWAP_ is always false + _IS_OPEN_TWAP_ = false; + + + string memory connect = "_"; + string memory suffix = "GSP"; + // name of the shares is the combination of suffix, connect and string of the GSP + name = string(abi.encodePacked(suffix, connect, addressToShortString(address(this)))); + // symbol of the shares is GLP + symbol = "GLP"; + // decimals of the shares is the same as the base token decimals + decimals = IERC20Metadata(baseTokenAddress).decimals(); + // initialize DOMAIN_SEPARATOR + buildDomainSeparator(); + // ========================================================================== + } + + // ============================== Permit ==================================== + /** +     * @notice DOMAIN_SEPARATOR is used for approve by signature +     */ + function buildDomainSeparator() public returns (bytes32){ + string memory connect = "_"; + string memory suffix = "GSP"; + // name of the shares is the combination of suffix, connect and string of the GSP + string memory name = string(abi.encodePacked(suffix, connect, addressToShortString(address(this)))); + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), + 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, + keccak256(bytes(name)), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + return DOMAIN_SEPARATOR; + } + + /** + * @notice Convert the address to a shorter string + * @param _addr The address to convert + * @return A string representation of _addr in hexadecimal + */ + function addressToShortString(address _addr) public pure returns (string memory) { + bytes32 value = bytes32(uint256(uint160(_addr))); + bytes memory alphabet = "0123456789abcdef"; + + bytes memory str = new bytes(8); + for (uint256 i = 0; i < 4; i++) { + str[i * 2] = alphabet[uint8(value[i + 12] >> 4)]; + str[1 + i * 2] = alphabet[uint8(value[i + 12] & 0x0f)]; + } + return string(str); + } + + // ============ Version Control ============ + /** + * @notice Return the version of DODOGasSavingPool + * @return The current version is 1.0.1 + */ + function version() external pure returns (string memory) { + return "GSP 1.0.1"; + } +} diff --git a/contracts/DODOGasSavingPool/GasSavingPool/impl/GSPFunding.sol b/contracts/DODOGasSavingPool/GasSavingPool/impl/GSPFunding.sol new file mode 100644 index 0000000..66f69c3 --- /dev/null +++ b/contracts/DODOGasSavingPool/GasSavingPool/impl/GSPFunding.sol @@ -0,0 +1,153 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + + +pragma solidity 0.8.16; + +import {GSPVault} from "./GSPVault.sol"; +import {DecimalMath} from "../../lib/DecimalMath.sol"; +import {IDODOCallee} from "../../intf/IDODOCallee.sol"; + +/// @notice this part focus on Lp tokens, mint and burn +contract GSPFunding is GSPVault { + // ============ Events ============ + + event BuyShares(address to, uint256 increaseShares, uint256 totalShares); + + event SellShares(address payer, address to, uint256 decreaseShares, uint256 totalShares); + + // ============ Buy & Sell Shares ============ + + /// @notice User mint Lp token and deposit tokens, the result is rounded down + /// @dev User first transfer baseToken and quoteToken to GSP, then call buyShares + /// @param to The address will receive shares + /// @return shares The amount of shares user will receive + /// @return baseInput The amount of baseToken user transfer to GSP + /// @return quoteInput The amount of quoteToken user transfer to GSP + function buyShares(address to) + external + nonReentrant + returns ( + uint256 shares, + uint256 baseInput, + uint256 quoteInput + ) + { + // The balance of baseToken and quoteToken should be the balance minus the fee + uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this)) - _MT_FEE_BASE_; + uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this)) - _MT_FEE_QUOTE_; + // The reserve of baseToken and quoteToken + uint256 baseReserve = _BASE_RESERVE_; + uint256 quoteReserve = _QUOTE_RESERVE_; + + // The amount of baseToken and quoteToken user transfer to GSP + baseInput = baseBalance - baseReserve; + quoteInput = quoteBalance - quoteReserve; + + // BaseToken should be transferred to GSP before calling buyShares + require(baseInput > 0, "NO_BASE_INPUT"); + + // Round down when withdrawing. Therefore, never be a situation occuring balance is 0 but totalsupply is not 0 + // But May Happen,reserve >0 But totalSupply = 0 + if (totalSupply == 0) { + // case 1. initial supply + require(quoteBalance > 0, "ZERO_QUOTE_AMOUNT"); + // The shares will be minted to user + shares = quoteBalance < DecimalMath.mulFloor(baseBalance, _I_) + ? DecimalMath.divFloor(quoteBalance, _I_) + : baseBalance; + // The target will be updated + _BASE_TARGET_ = uint112(shares); + _QUOTE_TARGET_ = uint112(DecimalMath.mulFloor(shares, _I_)); + require(_QUOTE_TARGET_ > 0, "QUOTE_TARGET_IS_ZERO"); + // Lock 1001 shares permanently in first deposit + require(shares > 2001, "MINT_AMOUNT_NOT_ENOUGH"); + _mint(address(0), 1001); + shares -= 1001; + } else if (baseReserve > 0 && quoteReserve > 0) { + // case 2. normal case + uint256 baseInputRatio = DecimalMath.divFloor(baseInput, baseReserve); + uint256 quoteInputRatio = DecimalMath.divFloor(quoteInput, quoteReserve); + uint256 mintRatio = quoteInputRatio < baseInputRatio ? quoteInputRatio : baseInputRatio; + // The shares will be minted to user + shares = DecimalMath.mulFloor(totalSupply, mintRatio); + + // The target will be updated + _BASE_TARGET_ = uint112(uint256(_BASE_TARGET_) + (DecimalMath.mulFloor(uint256(_BASE_TARGET_), mintRatio))); + _QUOTE_TARGET_ = uint112(uint256(_QUOTE_TARGET_) + (DecimalMath.mulFloor(uint256(_QUOTE_TARGET_), mintRatio))); + } + // The shares will be minted to user + // The reserve will be updated + _mint(to, shares); + _setReserve(baseBalance, quoteBalance); + emit BuyShares(to, shares, _SHARES_[to]); + } + + /// @notice User burn their lp and withdraw their tokens, the result is rounded down + /// @dev User call sellShares, the calculated baseToken and quoteToken amount should geater than minBaseToken and minQuoteToken + /// @param shareAmount The amount of shares user want to sell + /// @param to The address will receive baseToken and quoteToken + /// @param baseMinAmount The minimum amount of baseToken user want to receive + /// @param quoteMinAmount The minimum amount of quoteToken user want to receive + /// @param data The data will be passed to callee contract + /// @param deadline The deadline of this transaction + function sellShares( + uint256 shareAmount, + address to, + uint256 baseMinAmount, + uint256 quoteMinAmount, + bytes calldata data, + uint256 deadline + ) external nonReentrant returns (uint256 baseAmount, uint256 quoteAmount) { + // The deadline should be greater than current timestamp + require(deadline >= block.timestamp, "TIME_EXPIRED"); + // The amount of shares user want to sell should be less than user's balance + require(shareAmount <= _SHARES_[msg.sender], "GLP_NOT_ENOUGH"); + + // The balance of baseToken and quoteToken should be the balance minus the fee + uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this)) - _MT_FEE_BASE_; + uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this)) - _MT_FEE_QUOTE_; + // The total shares of GSP + uint256 totalShares = totalSupply; + + // The amount of baseToken and quoteToken user will receive is calculated by the ratio of user's shares to total shares + baseAmount = baseBalance * shareAmount / totalShares; + quoteAmount = quoteBalance * shareAmount / totalShares; + + // The target will be updated + _BASE_TARGET_ = uint112(uint256(_BASE_TARGET_) - DecimalMath._divCeil((uint256(_BASE_TARGET_) * (shareAmount)), totalShares)); + _QUOTE_TARGET_ = uint112(uint256(_QUOTE_TARGET_) - DecimalMath._divCeil((uint256(_QUOTE_TARGET_) * (shareAmount)), totalShares)); + + // The calculated baseToken and quoteToken amount should geater than minBaseToken and minQuoteToken + require( + baseAmount >= baseMinAmount && quoteAmount >= quoteMinAmount, + "WITHDRAW_NOT_ENOUGH" + ); + + // The shares will be burned from user + // The baseToken and quoteToken will be transferred to user + // The reserve will be synced + _burn(msg.sender, shareAmount); + _transferBaseOut(to, baseAmount); + _transferQuoteOut(to, quoteAmount); + _sync(); + + // If the data is not empty, the callee contract will be called + if (data.length > 0) { + //Same as DVM + IDODOCallee(to).DVMSellShareCall( + msg.sender, + shareAmount, + baseAmount, + quoteAmount, + data + ); + } + + emit SellShares(msg.sender, to, shareAmount, _SHARES_[msg.sender]); + } +} diff --git a/contracts/DODOGasSavingPool/GasSavingPool/impl/GSPStorage.sol b/contracts/DODOGasSavingPool/GasSavingPool/impl/GSPStorage.sol new file mode 100644 index 0000000..cca8dcf --- /dev/null +++ b/contracts/DODOGasSavingPool/GasSavingPool/impl/GSPStorage.sol @@ -0,0 +1,148 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.8.16; + +import {DODOMath} from "../../lib/DODOMath.sol"; +import {DecimalMath} from "../../lib/DecimalMath.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {PMMPricing} from "../../lib/PMMPricing.sol"; + +/// @notice this contract is used for store state and read state +contract GSPStorage is ReentrancyGuard { + + // ============ Storage for Setup ============ + // _GSP_INITIALIZED_ will be set to true when the init function is called + bool internal _GSP_INITIALIZED_; + // GSP does not open TWAP by default + // _IS_OPEN_TWAP_ can be set to true when the init function is called + bool public _IS_OPEN_TWAP_ = false; + + // ============ Core Address ============ + // _MAINTAINER_ is the maintainer of GSP + address public _MAINTAINER_; + // _ADMIN_ can set price + address public _ADMIN_; + // _BASE_TOKEN_ and _QUOTE_TOKEN_ should be ERC20 token + IERC20 public _BASE_TOKEN_; + IERC20 public _QUOTE_TOKEN_; + // _BASE_RESERVE_ and _QUOTE_RESERVE_ are the current reserves of the GSP + uint112 public _BASE_RESERVE_; + uint112 public _QUOTE_RESERVE_; + // _BLOCK_TIMESTAMP_LAST_ is used when calculating TWAP + uint32 public _BLOCK_TIMESTAMP_LAST_; + // _BASE_PRICE_CUMULATIVE_LAST_ is used when calculating TWAP + uint256 public _BASE_PRICE_CUMULATIVE_LAST_; + + // _BASE_TARGET_ and _QUOTE_TARGET_ are recalculated when the pool state changes + uint112 public _BASE_TARGET_; + uint112 public _QUOTE_TARGET_; + // _RState_ is the current R state of the GSP + uint32 public _RState_; + + // ============ Shares (ERC20) ============ + // symbol is the symbol of the shares + string public symbol; + // decimals is the decimals of the shares + uint8 public decimals; + // name is the name of the shares + string public name; + // totalSupply is the total supply of the shares + uint256 public totalSupply; + // _SHARES_ is the mapping from account to share balance, record the share balance of each account + mapping(address => uint256) internal _SHARES_; + mapping(address => mapping(address => uint256)) internal _ALLOWED_; + + // ================= Permit ====================== + + bytes32 public DOMAIN_SEPARATOR; + // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 public constant PERMIT_TYPEHASH = + 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + mapping(address => uint256) public nonces; + + // ============ Variables for Pricing ============ + // _MT_FEE_RATE_ is the fee rate of mt fee + uint256 public _MT_FEE_RATE_; + // _LP_FEE_RATE_ is the fee rate of lp fee + uint256 public _LP_FEE_RATE_; + uint256 public _K_; + uint256 public _I_; + // _PRICE_LIMIT_ is used to limit the setting range of I + uint256 public _PRICE_LIMIT_; + + // ============ Mt Fee ============ + // _MT_FEE_BASE_ represents the mt fee in base token + uint256 public _MT_FEE_BASE_; + // _MT_FEE_QUOTE_ represents the mt fee in quote token + uint256 public _MT_FEE_QUOTE_; + // _MT_FEE_RATE_MODEL_ is useless, just for compatible with old version pool + address public _MT_FEE_RATE_MODEL_ = address(0); + + // ============ Helper Functions ============ + + /// @notice Return the PMM state of the pool from inner or outside + /// @dev B0 and Q0 are calculated in adjustedTarget + /// @return state The current PMM state + function getPMMState() public view returns (PMMPricing.PMMState memory state) { + state.i = _I_; + state.K = _K_; + state.B = _BASE_RESERVE_; + state.Q = _QUOTE_RESERVE_; + state.B0 = _BASE_TARGET_; // will be calculated in adjustedTarget + state.Q0 = _QUOTE_TARGET_; + state.R = PMMPricing.RState(_RState_); + PMMPricing.adjustedTarget(state); + } + + /// @notice Return the PMM state variables used for routeHelpers + /// @return i The price index + /// @return K The K value + /// @return B The base token reserve + /// @return Q The quote token reserve + /// @return B0 The base token target + /// @return Q0 The quote token target + /// @return R The R state of the pool + function getPMMStateForCall() + external + view + returns ( + uint256 i, + uint256 K, + uint256 B, + uint256 Q, + uint256 B0, + uint256 Q0, + uint256 R + ) + { + PMMPricing.PMMState memory state = getPMMState(); + i = state.i; + K = state.K; + B = state.B; + Q = state.Q; + B0 = state.B0; + Q0 = state.Q0; + R = uint256(state.R); + } + + /// @notice Return the adjusted mid price + /// @return midPrice The current mid price + function getMidPrice() public view returns (uint256 midPrice) { + return PMMPricing.getMidPrice(getPMMState()); + } + + /// @notice Return the total mt fee maintainer can claim + /// @dev The total mt fee is represented in two types: in base token and in quote token + /// @return mtFeeBase The total mt fee in base token + /// @return mtFeeQuote The total mt fee in quote token + function getMtFeeTotal() public view returns (uint256 mtFeeBase, uint256 mtFeeQuote) { + mtFeeBase = _MT_FEE_BASE_; + mtFeeQuote = _MT_FEE_QUOTE_; + } +} diff --git a/contracts/DODOGasSavingPool/GasSavingPool/impl/GSPTrader.sol b/contracts/DODOGasSavingPool/GasSavingPool/impl/GSPTrader.sol new file mode 100644 index 0000000..97c3f3b --- /dev/null +++ b/contracts/DODOGasSavingPool/GasSavingPool/impl/GSPTrader.sol @@ -0,0 +1,275 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.8.16; + +import {GSPVault} from "./GSPVault.sol"; +import {DecimalMath} from "../../lib/DecimalMath.sol"; +import {PMMPricing} from "../../lib/PMMPricing.sol"; +import {IDODOCallee} from "../../intf/IDODOCallee.sol"; + +/// @notice this contract deal with swap +contract GSPTrader is GSPVault { + + // ============ Events ============ + + event DODOSwap( + address fromToken, + address toToken, + uint256 fromAmount, + uint256 toAmount, + address trader, + address receiver + ); + + event DODOFlashLoan(address borrower, address assetTo, uint256 baseAmount, uint256 quoteAmount); + + event RChange(PMMPricing.RState newRState); + + // ============ Trade Functions ============ + /** + * @notice User sell base tokens, user pay tokens first. Must be used with a router + * @dev The base token balance is the actual balance minus the mt fee + * @param to The recipient of the output + * @return receiveQuoteAmount Amount of quote token received + */ + function sellBase(address to) external nonReentrant returns (uint256 receiveQuoteAmount) { + uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this)) - _MT_FEE_BASE_; + uint256 baseInput = baseBalance - uint256(_BASE_RESERVE_); + uint256 mtFee; + uint256 newBaseTarget; + PMMPricing.RState newRState; + // calculate the amount of quote token to receive and mt fee + (receiveQuoteAmount, mtFee, newRState, newBaseTarget) = querySellBase(tx.origin, baseInput); + // transfer quote token to recipient + _transferQuoteOut(to, receiveQuoteAmount); + // update mt fee in quote token + _MT_FEE_QUOTE_ = _MT_FEE_QUOTE_ + mtFee; + + + // update TARGET + if (_RState_ != uint32(newRState)) { + require(newBaseTarget <= type(uint112).max, "OVERFLOW"); + _BASE_TARGET_ = uint112(newBaseTarget); + _RState_ = uint32(newRState); + emit RChange(newRState); + } + // update reserve + _setReserve(baseBalance, _QUOTE_TOKEN_.balanceOf(address(this)) - _MT_FEE_QUOTE_); + + emit DODOSwap( + address(_BASE_TOKEN_), + address(_QUOTE_TOKEN_), + baseInput, + receiveQuoteAmount, + msg.sender, + to + ); + } + + /** + * @notice User sell quote tokens, user pay tokens first. Must be used with a router + * @param to The recipient of the output + * @return receiveBaseAmount Amount of base token received + */ + function sellQuote(address to) external nonReentrant returns (uint256 receiveBaseAmount) { + uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this)) - _MT_FEE_QUOTE_; + uint256 quoteInput = quoteBalance - uint256(_QUOTE_RESERVE_); + uint256 mtFee; + uint256 newQuoteTarget; + PMMPricing.RState newRState; + // calculate the amount of base token to receive and mt fee + (receiveBaseAmount, mtFee, newRState, newQuoteTarget) = querySellQuote( + tx.origin, + quoteInput + ); + // transfer base token to recipient + _transferBaseOut(to, receiveBaseAmount); + // update mt fee in base token + _MT_FEE_BASE_ = _MT_FEE_BASE_ + mtFee; + + // update TARGET + if (_RState_ != uint32(newRState)) { + require(newQuoteTarget <= type(uint112).max, "OVERFLOW"); + _QUOTE_TARGET_ = uint112(newQuoteTarget); + _RState_ = uint32(newRState); + emit RChange(newRState); + } + // update reserve + _setReserve((_BASE_TOKEN_.balanceOf(address(this)) - _MT_FEE_BASE_), quoteBalance); + + emit DODOSwap( + address(_QUOTE_TOKEN_), + address(_BASE_TOKEN_), + quoteInput, + receiveBaseAmount, + msg.sender, + to + ); + } + + /** + * @notice inner flashloan, pay tokens out first, call external contract and check tokens left + * @param baseAmount The base token amount user require + * @param quoteAmount The quote token amount user require + * @param assetTo The address who uses above tokens + * @param data The external contract's callData + */ + function flashLoan( + uint256 baseAmount, + uint256 quoteAmount, + address assetTo, + bytes calldata data + ) external nonReentrant { + _transferBaseOut(assetTo, baseAmount); + _transferQuoteOut(assetTo, quoteAmount); + + if (data.length > 0) + IDODOCallee(assetTo).DSPFlashLoanCall(msg.sender, baseAmount, quoteAmount, data); + + uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this)) - _MT_FEE_BASE_; + uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this)) - _MT_FEE_QUOTE_; + + // no input -> pure loss + require( + baseBalance >= _BASE_RESERVE_ || quoteBalance >= _QUOTE_RESERVE_, + "FLASH_LOAN_FAILED" + ); + + // sell quote case + // quote input + base output + if (baseBalance < _BASE_RESERVE_) { + uint256 quoteInput = quoteBalance - uint256(_QUOTE_RESERVE_); + ( + uint256 receiveBaseAmount, + uint256 mtFee, + PMMPricing.RState newRState, + uint256 newQuoteTarget + ) = querySellQuote(tx.origin, quoteInput); // revert if quoteBalance _I_ ? i - _I_ : _I_ - i; + require((offset * 1e6 / _I_) <= _PRICE_LIMIT_, "EXCEED_PRICE_LIMIT"); + _I_ = i; + + emit IChange(i); + } + + /** + * @notice Adjust mtFee rate, only for maintainer + * @dev The decimals of mtFee rate is 1e18 + * @param mtFeeRate The new mtFee rate + */ + function adjustMtFeeRate(uint256 mtFeeRate) external onlyMaintainer { + require(mtFeeRate <= 10**18, "INVALID_MT_FEE_RATE"); + _MT_FEE_RATE_ = mtFeeRate; + + emit MtFeeRateChange(mtFeeRate); + } + + /** + * @notice Adjust lpFee rate, only for maintainer + * @dev The decimals of lpFee rate is 1e18 + * @param lpFeeRate The new lpFee rate + */ + function adjustLpFeeRate(uint256 lpFeeRate) external onlyMaintainer { + require(lpFeeRate <= 10**18, "INVALID_LP_FEE_RATE"); + _LP_FEE_RATE_ = lpFeeRate; + + emit LpFeeRateChange(lpFeeRate); + } + + /** + * @notice Adjust swap curve parameter k, only for maintainer + * @dev The decimals of k is 1e18 + * @param k The new swap curve parameter k + */ + function adjustK(uint256 k) external onlyMaintainer { + require(k <= 10**18, "INVALID_K"); + _K_ = k; + + emit KChange(k); + } + + // ============ Asset Out ============ + /** + * @notice Transfer base token out, internal use only + * @param to The address of the receiver + * @param amount The amount of base token to transfer out + */ + function _transferBaseOut(address to, uint256 amount) internal { + if (amount > 0) { + _BASE_TOKEN_.safeTransfer(to, amount); + } + } + + /** + * @notice Transfer quote token out, internal use only + * @param to The address of the receiver + * @param amount The amount of quote token to transfer out + */ + function _transferQuoteOut(address to, uint256 amount) internal { + if (amount > 0) { + _QUOTE_TOKEN_.safeTransfer(to, amount); + } + } + + /// @notice Maintainer withdraw mtFee, only for maintainer + function withdrawMtFeeTotal() external nonReentrant onlyMaintainer { + uint256 mtFeeQuote = _MT_FEE_QUOTE_; + uint256 mtFeeBase = _MT_FEE_BASE_; + _MT_FEE_QUOTE_ = 0; + _transferQuoteOut(_MAINTAINER_, mtFeeQuote); + _MT_FEE_BASE_ = 0; + _transferBaseOut(_MAINTAINER_, mtFeeBase); + + emit WithdrawMtFee(address(_QUOTE_TOKEN_), mtFeeQuote); + emit WithdrawMtFee(address(_BASE_TOKEN_), mtFeeBase); + } + + // ============ Shares (ERC20) ============ + + /** + * @dev Transfer token for a specified address + * @param to The address to transfer to. + * @param amount The amount to be transferred. + */ + function transfer(address to, uint256 amount) public returns (bool) { + require(amount <= _SHARES_[msg.sender], "BALANCE_NOT_ENOUGH"); + + _SHARES_[msg.sender] = _SHARES_[msg.sender] - (amount); + _SHARES_[to] = _SHARES_[to] + amount; + emit Transfer(msg.sender, to, amount); + return true; + } + + /** + * @dev Gets the balance of the specified address. + * @param owner The address to query the the balance of. + * @return balance An uint256 representing the amount owned by the passed address. + */ + function balanceOf(address owner) external view returns (uint256 balance) { + return _SHARES_[owner]; + } + + /** + * @dev Transfer tokens from one address to another + * @param from address The address which you want to send tokens from + * @param to address The address which you want to transfer to + * @param amount uint256 the amount of tokens to be transferred + */ + function transferFrom( + address from, + address to, + uint256 amount + ) public returns (bool) { + require(amount <= _SHARES_[from], "BALANCE_NOT_ENOUGH"); + require(amount <= _ALLOWED_[from][msg.sender], "ALLOWANCE_NOT_ENOUGH"); + + _SHARES_[from] = _SHARES_[from] - amount; + _SHARES_[to] = _SHARES_[to] + amount; + _ALLOWED_[from][msg.sender] = _ALLOWED_[from][msg.sender] - amount; + emit Transfer(from, to, amount); + return true; + } + + /** + * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. + * @param spender The address which will spend the funds. + * @param amount The amount of tokens to be spent. + */ + function approve(address spender, uint256 amount) public returns (bool) { + _approve(msg.sender, spender, amount); + return true; + } + + function _approve( + address owner, + address spender, + uint256 amount + ) private { + _ALLOWED_[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Function to check the amount of tokens that an owner _ALLOWED_ to a spender. + * @param owner address The address which owns the funds. + * @param spender address The address which will spend the funds. + * @return A uint256 specifying the amount of tokens still available for the spender. + */ + function allowance(address owner, address spender) public view returns (uint256) { + return _ALLOWED_[owner][spender]; + } + + function _mint(address user, uint256 value) internal { + require(value > 1000, "MINT_AMOUNT_NOT_ENOUGH"); + _SHARES_[user] = _SHARES_[user] + value; + totalSupply = totalSupply + value; + emit Mint(user, value); + emit Transfer(address(0), user, value); + } + + function _burn(address user, uint256 value) internal { + _SHARES_[user] = _SHARES_[user] - value; + totalSupply = totalSupply - value; + emit Burn(user, value); + emit Transfer(user, address(0), value); + } + + // ============================ Permit ====================================== + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + require(deadline >= block.timestamp, "DODO_GSP_LP: EXPIRED"); + bytes32 digest = + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + PERMIT_TYPEHASH, + owner, + spender, + value, + nonces[owner]++, + deadline + ) + ) + ) + ); + + address recoveredAddress = ecrecover(digest, v, r, s); + require( + recoveredAddress != address(0) && recoveredAddress == owner, + "DODO_GSP_LP: INVALID_SIGNATURE" + ); + _approve(owner, spender, value); + } +} \ No newline at end of file diff --git a/contracts/DODOGasSavingPool/GasSavingPool/intf/IGSP.sol b/contracts/DODOGasSavingPool/GasSavingPool/intf/IGSP.sol new file mode 100644 index 0000000..7ccbe07 --- /dev/null +++ b/contracts/DODOGasSavingPool/GasSavingPool/intf/IGSP.sol @@ -0,0 +1,45 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.8.16; + +interface IGSP { + function init( + address maintainer, + address admin, + address baseTokenAddress, + address quoteTokenAddress, + uint256 lpFeeRate, + uint256 mtFeeRate, + uint256 i, + uint256 k, + uint256 priceLimit, + bool isOpenTWAP + ) external; + + function _BASE_TOKEN_() external view returns (address); + + function _QUOTE_TOKEN_() external view returns (address); + + function _I_() external view returns (uint256); + + function _MT_FEE_RATE_MODEL_() external view returns (address); // Useless, just for compatibility + + function getVaultReserve() external view returns (uint256 baseReserve, uint256 quoteReserve); + + function getUserFeeRate(address user) external view returns (uint256 lpFeeRate, uint256 mtFeeRate); + + function getMtFeeTotal() external view returns (uint256 mtFeeBase, uint256 mtFeeQuote); + + function sellBase(address to) external returns (uint256); + + function sellQuote(address to) external returns (uint256); + + function buyShares(address to) external returns (uint256 shares, uint256 baseInput, uint256 quoteInput); + + function sellShares(uint256 shareAmount, address to, uint256 baseMinAmount, uint256 quoteMinAmount, bytes calldata data, uint256 deadline) external returns (uint256 baseAmount, uint256 quoteAmount); +} diff --git a/contracts/DODOGasSavingPool/intf/IDODOCallee.sol b/contracts/DODOGasSavingPool/intf/IDODOCallee.sol new file mode 100644 index 0000000..0a19525 --- /dev/null +++ b/contracts/DODOGasSavingPool/intf/IDODOCallee.sol @@ -0,0 +1,59 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.8.16; +pragma experimental ABIEncoderV2; + +interface IDODOCallee { + function DVMSellShareCall( + address sender, + uint256 burnShareAmount, + uint256 baseAmount, + uint256 quoteAmount, + bytes calldata data + ) external; + + function DVMFlashLoanCall( + address sender, + uint256 baseAmount, + uint256 quoteAmount, + bytes calldata data + ) external; + + function DPPFlashLoanCall( + address sender, + uint256 baseAmount, + uint256 quoteAmount, + bytes calldata data + ) external; + + function DSPFlashLoanCall( + address sender, + uint256 baseAmount, + uint256 quoteAmount, + bytes calldata data + ) external; + + function CPCancelCall( + address sender, + uint256 amount, + bytes calldata data + ) external; + + function CPClaimBidCall( + address sender, + uint256 baseAmount, + uint256 quoteAmount, + bytes calldata data + ) external; + + function NFTRedeemCall( + address payable assetTo, + uint256 quoteAmount, + bytes calldata + ) external; +} diff --git a/contracts/DODOGasSavingPool/lib/DODOMath.sol b/contracts/DODOGasSavingPool/lib/DODOMath.sol new file mode 100644 index 0000000..61f9cd3 --- /dev/null +++ b/contracts/DODOGasSavingPool/lib/DODOMath.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.16; +pragma experimental ABIEncoderV2; + +import {DecimalMath} from "./DecimalMath.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +/** + * @title DODOMath + * @author DODO Breeder + * + * @notice Functions for complex calculating. Including ONE Integration and TWO Quadratic solutions + */ +library DODOMath { + using Math for uint256; + /* + Integrate dodo curve from V1 to V2 + require V0>=V1>=V2>0 + res = (1-k)i(V1-V2)+ikV0*V0(1/V2-1/V1) + let V1-V2=delta + res = i*delta*(1-k+k(V0^2/V1/V2)) + + i is the price of V-res trading pair + + support k=1 & k=0 case + + [round down] + */ + function _GeneralIntegrate( + uint256 V0, + uint256 V1, + uint256 V2, + uint256 i, + uint256 k + ) internal pure returns (uint256) { + require(V0 > 0, "TARGET_IS_ZERO"); + uint256 fairAmount = i * (V1 - V2); // i*delta + if (k == 0) { + return fairAmount / DecimalMath.ONE; + } + uint256 V0V0V1V2 = DecimalMath.divFloor(V0 * V0 / V1, V2); + uint256 penalty = DecimalMath.mulFloor(k, V0V0V1V2); // k(V0^2/V1/V2) + return (DecimalMath.ONE - k + penalty) * fairAmount / DecimalMath.ONE2; + } + + /* + Follow the integration function above + i*deltaB = (Q2-Q1)*(1-k+kQ0^2/Q1/Q2) + Assume Q2=Q0, Given Q1 and deltaB, solve Q0 + + i is the price of delta-V trading pair + give out target of V + + support k=1 & k=0 case + + [round down] + */ + function _SolveQuadraticFunctionForTarget( + uint256 V1, + uint256 delta, + uint256 i, + uint256 k + ) internal pure returns (uint256) { + if (k == 0) { + return V1 + DecimalMath.mulFloor(i, delta); + } + // V0 = V1*(1+(sqrt-1)/2k) + // sqrt = √(1+4kidelta/V1) + // premium = 1+(sqrt-1)/2k + // uint256 sqrt = (4 * k).mul(i).mul(delta).div(V1).add(DecimalMath.ONE2).sqrt(); + + if (V1 == 0) { + return 0; + } + uint256 sqrt; + uint256 ki = 4 * k * i; + + if (ki == 0) { + sqrt = DecimalMath.ONE; + } else if ((ki * delta) / ki == delta) { + sqrt =((ki * delta) / V1 + DecimalMath.ONE2).sqrt(); + } else { + sqrt = (ki / V1 * delta + DecimalMath.ONE2).sqrt(); + } + uint256 premium = + DecimalMath.divFloor(sqrt - DecimalMath.ONE, k * 2) + DecimalMath.ONE; + // V0 is greater than or equal to V1 according to the solution + return DecimalMath.mulFloor(V1, premium); + } + + /* + Follow the integration expression above, we have: + i*deltaB = (Q2-Q1)*(1-k+kQ0^2/Q1/Q2) + Given Q1 and deltaB, solve Q2 + This is a quadratic function and the standard version is + aQ2^2 + bQ2 + c = 0, where + a=1-k + -b=(1-k)Q1-kQ0^2/Q1+i*deltaB + c=-kQ0^2 + and Q2=(-b+sqrt(b^2+4(1-k)kQ0^2))/2(1-k) + note: another root is negative, abondan + + if deltaBSig=true, then Q2>Q1, user sell Q and receive B + if deltaBSig=false, then Q2 0, "TARGET_IS_ZERO"); + if (delta == 0) { + return 0; + } + + if (k == 0) { + // why v1 + return DecimalMath.mulFloor(i, delta) > V1 ? V1 : DecimalMath.mulFloor(i, delta); + } + + if (k == DecimalMath.ONE) { + // if k==1 + // Q2=Q1/(1+ideltaBQ1/Q0/Q0) + // temp = ideltaBQ1/Q0/Q0 + // Q2 = Q1/(1+temp) + // Q1-Q2 = Q1*(1-1/(1+temp)) = Q1*(temp/(1+temp)) + // uint256 temp = i.mul(delta).mul(V1).div(V0.mul(V0)); + uint256 temp; + uint256 idelta = i * (delta); + if (idelta == 0) { + temp = 0; + } else if ((idelta * V1) / idelta == V1) { + temp = (idelta * V1) / (V0 * V0); + } else { + temp = delta * (V1) / (V0) * (i) / (V0); + } + return V1 * (temp) / (temp + (DecimalMath.ONE)); + } + + // calculate -b value and sig + // b = kQ0^2/Q1-i*deltaB-(1-k)Q1 + // part1 = (1-k)Q1 >=0 + // part2 = kQ0^2/Q1-i*deltaB >=0 + // bAbs = abs(part1-part2) + // if part1>part2 => b is negative => bSig is false + // if part2>part1 => b is positive => bSig is true + uint256 part2 = k * (V0) / (V1) * (V0) + (i * (delta)); // kQ0^2/Q1-i*deltaB + uint256 bAbs = (DecimalMath.ONE - k) * (V1); // (1-k)Q1 + + bool bSig; + if (bAbs >= part2) { + bAbs = bAbs - part2; + bSig = false; + } else { + bAbs = part2 - bAbs; + bSig = true; + } + bAbs = bAbs / (DecimalMath.ONE); + + // calculate sqrt + uint256 squareRoot = DecimalMath.mulFloor((DecimalMath.ONE - k) * (4), DecimalMath.mulFloor(k, V0) * (V0)); // 4(1-k)kQ0^2 + squareRoot = Math.sqrt((bAbs * bAbs) + squareRoot); // sqrt(b*b+4(1-k)kQ0*Q0) + + // final res + uint256 denominator = (DecimalMath.ONE - k) * 2; // 2(1-k) + uint256 numerator; + if (bSig) { + numerator = squareRoot - bAbs; + if (numerator == 0) { + revert("DODOMath: should not be 0"); + } + } else { + numerator = bAbs + squareRoot; + } + + uint256 V2 = DecimalMath.divCeil(numerator, denominator); + if (V2 > V1) { + return 0; + } else { + return V1 - V2; + } + } +} diff --git a/contracts/DODOGasSavingPool/lib/DecimalMath.sol b/contracts/DODOGasSavingPool/lib/DecimalMath.sol new file mode 100644 index 0000000..fadf919 --- /dev/null +++ b/contracts/DODOGasSavingPool/lib/DecimalMath.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.16; +pragma experimental ABIEncoderV2; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +/** + * @title DecimalMath + * @author DODO Breeder + * + * @notice Functions for fixed point number with 18 decimals + */ + +library DecimalMath { + uint256 internal constant ONE = 10 ** 18; + uint256 internal constant ONE2 = 10 ** 36; + + function mul(uint256 target, uint256 d) internal pure returns (uint256) { + return target * d / (10 ** 18); + } + + function mulFloor(uint256 target, uint256 d) internal pure returns (uint256) { + return target * d / (10 ** 18); + } + + function mulCeil(uint256 target, uint256 d) internal pure returns (uint256) { + return _divCeil(target * d, 10 ** 18); + } + + function div(uint256 target, uint256 d) internal pure returns (uint256) { + return target * (10 ** 18) / d; + } + + function divFloor(uint256 target, uint256 d) internal pure returns (uint256) { + return target * (10 ** 18) / d; + } + + function divCeil(uint256 target, uint256 d) internal pure returns (uint256) { + return _divCeil(target * (10 ** 18), d); + } + + function reciprocalFloor(uint256 target) internal pure returns (uint256) { + return uint256(10 ** 36) / target; + } + + function reciprocalCeil(uint256 target) internal pure returns (uint256) { + return _divCeil(uint256(10 ** 36), target); + } + + function sqrt(uint256 target) internal pure returns (uint256) { + return Math.sqrt(target * ONE); + } + + function powFloor(uint256 target, uint256 e) internal pure returns (uint256) { + if (e == 0) { + return 10 ** 18; + } else if (e == 1) { + return target; + } else { + uint256 p = powFloor(target, e / 2); + p = p * p / (10 ** 18); + if (e % 2 == 1) { + p = p * target / (10 ** 18); + } + return p; + } + } + + function _divCeil(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 quotient = a / b; + uint256 remainder = a - quotient * b; + if (remainder > 0) { + return quotient + 1; + } else { + return quotient; + } + } +} diff --git a/contracts/DODOGasSavingPool/lib/InitializableOwnable.sol b/contracts/DODOGasSavingPool/lib/InitializableOwnable.sol new file mode 100644 index 0000000..aa402c6 --- /dev/null +++ b/contracts/DODOGasSavingPool/lib/InitializableOwnable.sol @@ -0,0 +1,58 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.8.16; +pragma experimental ABIEncoderV2; + +/** + * @title Ownable + * @author DODO Breeder + * + * @notice Ownership related functions + */ +contract InitializableOwnable { + address public _OWNER_; + address public _NEW_OWNER_; + bool internal _INITIALIZED_; + + // ============ Events ============ + + event OwnershipTransferPrepared(address indexed previousOwner, address indexed newOwner); + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + // ============ Modifiers ============ + + modifier notInitialized() { + require(!_INITIALIZED_, "DODO_INITIALIZED"); + _; + } + + modifier onlyOwner() { + require(msg.sender == _OWNER_, "NOT_OWNER"); + _; + } + + // ============ Functions ============ + + function initOwner(address newOwner) public notInitialized { + _INITIALIZED_ = true; + _OWNER_ = newOwner; + } + + function transferOwnership(address newOwner) public onlyOwner { + emit OwnershipTransferPrepared(_OWNER_, newOwner); + _NEW_OWNER_ = newOwner; + } + + function claimOwnership() public { + require(msg.sender == _NEW_OWNER_, "INVALID_CLAIM"); + emit OwnershipTransferred(_OWNER_, _NEW_OWNER_); + _OWNER_ = _NEW_OWNER_; + _NEW_OWNER_ = address(0); + } +} diff --git a/contracts/DODOGasSavingPool/lib/PMMPricing.sol b/contracts/DODOGasSavingPool/lib/PMMPricing.sol new file mode 100644 index 0000000..e6a0d01 --- /dev/null +++ b/contracts/DODOGasSavingPool/lib/PMMPricing.sol @@ -0,0 +1,266 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.8.16; +pragma experimental ABIEncoderV2; + +import {DecimalMath} from "../lib/DecimalMath.sol"; +import {DODOMath} from "../lib/DODOMath.sol"; + +/** + * @title Pricing + * @author DODO Breeder + * + * @notice DODO Pricing model + */ + +library PMMPricing { + + enum RState {ONE, ABOVE_ONE, BELOW_ONE} + + struct PMMState { + uint256 i; + uint256 K; + uint256 B; + uint256 Q; + uint256 B0; + uint256 Q0; + RState R; + } + + // ============ buy & sell ============ + /** + * @notice Inner calculation based on pmm algorithm, sell base + * @param state The current PMM state + * @param payBaseAmount The amount of base token user want to sell + * @return receiveQuoteAmount The amount of quote token user will receive + * @return newR The new R status after swap + */ + function sellBaseToken(PMMState memory state, uint256 payBaseAmount) + internal + pure + returns (uint256 receiveQuoteAmount, RState newR) + { + if (state.R == RState.ONE) { + // case 1: R=1 + // R falls below one + receiveQuoteAmount = _ROneSellBaseToken(state, payBaseAmount); + newR = RState.BELOW_ONE; + } else if (state.R == RState.ABOVE_ONE) { + uint256 backToOnePayBase = state.B0 - state.B; + uint256 backToOneReceiveQuote = state.Q - state.Q0; + // case 2: R>1 + // complex case, R status depends on trading amount + if (payBaseAmount < backToOnePayBase) { + // case 2.1: R status do not change + receiveQuoteAmount = _RAboveSellBaseToken(state, payBaseAmount); + newR = RState.ABOVE_ONE; + if (receiveQuoteAmount > 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 + receiveQuoteAmount = backToOneReceiveQuote; + } + } else if (payBaseAmount == backToOnePayBase) { + // case 2.2: R status changes to ONE + receiveQuoteAmount = backToOneReceiveQuote; + newR = RState.ONE; + } else { + // case 2.3: R status changes to BELOW_ONE + receiveQuoteAmount = backToOneReceiveQuote + ( + _ROneSellBaseToken(state, (payBaseAmount - backToOnePayBase)) + ); + newR = RState.BELOW_ONE; + } + } else { + // state.R == RState.BELOW_ONE + // case 3: R<1 + receiveQuoteAmount = _RBelowSellBaseToken(state, payBaseAmount); + newR = RState.BELOW_ONE; + } + } + + /** + * @notice Inner calculation based on pmm algorithm, sell quote + * @param state The current PMM state + * @param payQuoteAmount The amount of quote token user want to sell + * @return receiveBaseAmount The amount of base token user will receive + * @return newR The new R status after swap + */ + function sellQuoteToken(PMMState memory state, uint256 payQuoteAmount) + internal + pure + returns (uint256 receiveBaseAmount, RState newR) + { + if (state.R == RState.ONE) { + receiveBaseAmount = _ROneSellQuoteToken(state, payQuoteAmount); + newR = RState.ABOVE_ONE; + } else if (state.R == RState.ABOVE_ONE) { + receiveBaseAmount = _RAboveSellQuoteToken(state, payQuoteAmount); + newR = RState.ABOVE_ONE; + } else { + uint256 backToOnePayQuote = state.Q0 - state.Q; + uint256 backToOneReceiveBase = state.B - state.B0; + if (payQuoteAmount < backToOnePayQuote) { + receiveBaseAmount = _RBelowSellQuoteToken(state, payQuoteAmount); + newR = RState.BELOW_ONE; + if (receiveBaseAmount > backToOneReceiveBase) { + receiveBaseAmount = backToOneReceiveBase; + } + } else if (payQuoteAmount == backToOnePayQuote) { + receiveBaseAmount = backToOneReceiveBase; + newR = RState.ONE; + } else { + receiveBaseAmount = backToOneReceiveBase + ( + _ROneSellQuoteToken(state, payQuoteAmount - backToOnePayQuote) + ); + newR = RState.ABOVE_ONE; + } + } + } + + // ============ R = 1 cases ============ + + function _ROneSellBaseToken(PMMState memory state, uint256 payBaseAmount) + internal + pure + returns ( + uint256 // receiveQuoteToken + ) + { + // in theory Q2 <= targetQuoteTokenAmount + // however when amount is close to 0, precision problems may cause Q2 > targetQuoteTokenAmount + return + DODOMath._SolveQuadraticFunctionForTrade( + state.Q0, + state.Q0, + payBaseAmount, + state.i, + state.K + ); + } + + function _ROneSellQuoteToken(PMMState memory state, uint256 payQuoteAmount) + internal + pure + returns ( + uint256 // receiveBaseToken + ) + { + return + DODOMath._SolveQuadraticFunctionForTrade( + state.B0, + state.B0, + payQuoteAmount, + DecimalMath.reciprocalFloor(state.i), + state.K + ); + } + + // ============ R < 1 cases ============ + + function _RBelowSellQuoteToken(PMMState memory state, uint256 payQuoteAmount) + internal + pure + returns ( + uint256 // receiveBaseToken + ) + { + return + DODOMath._GeneralIntegrate( + state.Q0, + state.Q + payQuoteAmount, + state.Q, + DecimalMath.reciprocalFloor(state.i), + state.K + ); + } + + function _RBelowSellBaseToken(PMMState memory state, uint256 payBaseAmount) + internal + pure + returns ( + uint256 // receiveQuoteToken + ) + { + return + DODOMath._SolveQuadraticFunctionForTrade( + state.Q0, + state.Q, + payBaseAmount, + state.i, + state.K + ); + } + + // ============ R > 1 cases ============ + + function _RAboveSellBaseToken(PMMState memory state, uint256 payBaseAmount) + internal + pure + returns ( + uint256 // receiveQuoteToken + ) + { + return + DODOMath._GeneralIntegrate( + state.B0, + state.B + payBaseAmount, + state.B, + state.i, + state.K + ); + } + + function _RAboveSellQuoteToken(PMMState memory state, uint256 payQuoteAmount) + internal + pure + returns ( + uint256 // receiveBaseToken + ) + { + return + DODOMath._SolveQuadraticFunctionForTrade( + state.B0, + state.B, + payQuoteAmount, + DecimalMath.reciprocalFloor(state.i), + state.K + ); + } + + // ============ Helper functions ============ + + function adjustedTarget(PMMState memory state) internal pure { + if (state.R == RState.BELOW_ONE) { + state.Q0 = DODOMath._SolveQuadraticFunctionForTarget( + state.Q, + state.B - state.B0, + state.i, + state.K + ); + } else if (state.R == RState.ABOVE_ONE) { + state.B0 = DODOMath._SolveQuadraticFunctionForTarget( + state.B, + state.Q - state.Q0, + DecimalMath.reciprocalFloor(state.i), + state.K + ); + } + } + + function getMidPrice(PMMState memory state) internal pure returns (uint256) { + if (state.R == RState.BELOW_ONE) { + uint256 R = DecimalMath.divFloor(state.Q0 * state.Q0 / state.Q, state.Q); + R = DecimalMath.ONE - state.K + (DecimalMath.mulFloor(state.K, R)); + return DecimalMath.divFloor(state.i, R); + } else { + uint256 R = DecimalMath.divFloor(state.B0 * state.B0 / state.B, state.B); + R = DecimalMath.ONE - state.K + (DecimalMath.mulFloor(state.K, R)); + return DecimalMath.mulFloor(state.i, R); + } + } +}