diff --git a/contracts/fund/FundV5.sol b/contracts/fund/FundV5.sol new file mode 100644 index 00000000..ee85ce47 --- /dev/null +++ b/contracts/fund/FundV5.sol @@ -0,0 +1,1043 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/math/Math.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +import "../utils/SafeDecimalMath.sol"; +import "../utils/CoreUtility.sol"; + +import "../interfaces/IPrimaryMarketV3.sol"; +import "../interfaces/IFundV5.sol"; +import "../interfaces/IFundForPrimaryMarketV4.sol"; +import "../interfaces/IFundForStrategyV2.sol"; +import "../interfaces/IShareV2.sol"; +import "../interfaces/ITwapOracleV2.sol"; +import "../interfaces/IAprOracle.sol"; +import "../interfaces/IVotingEscrow.sol"; + +import "./FundRolesV2.sol"; + +contract FundV5 is + IFundV5, + IFundForPrimaryMarketV4, + IFundForStrategyV2, + Ownable, + ReentrancyGuard, + FundRolesV2, + CoreUtility +{ + using Math for uint256; + using SafeMath for uint256; + using SafeDecimalMath for uint256; + using SafeERC20 for IERC20; + + event ProfitReported(uint256 profit, uint256 totalFee, uint256 totalFeeQ, uint256 strategyFeeQ); + event LossReported(uint256 loss); + event TwapOracleUpdated(address newTwapOracle); + event AprOracleUpdated(address newAprOracle); + event FeeCollectorUpdated(address newFeeCollector); + event ActivityDelayTimeUpdated(uint256 delayTime); + event SplitRatioUpdated(uint256 newSplitRatio); + event TotalDebtUpdated(uint256 newTotalDebt); + event Frozen(); + + uint256 private constant UNIT = 1e18; + uint256 private constant MAX_INTEREST_RATE = 0.001e18; // 0.1% daily + + uint256 public immutable override weightB; + uint256 public immutable settlementPeriod; + + /// @notice Address of the underlying token. + address public immutable override tokenUnderlying; + + /// @notice A multipler that normalizes an underlying balance to 18 decimal places. + uint256 public immutable override underlyingDecimalMultiplier; + + /// @notice TwapOracle address for the underlying asset. + ITwapOracleV2 public override twapOracle; + + /// @notice AprOracle address. + IAprOracle public aprOracle; + + /// @notice Fee Collector address. + address public override feeCollector; + + /// @notice End timestamp of the current trading day. + /// A trading day starts at UTC time `SETTLEMENT_TIME` of a day (inclusive) + /// and ends at the same time of the next day (exclusive). + uint256 public override currentDay; + + /// @notice The amount of BISHOP received by splitting one QUEEN. + /// This ratio changes on every rebalance. + uint256 public override splitRatio; + + /// @dev Mapping of rebalance version => splitRatio. + mapping(uint256 => uint256) private _historicalSplitRatio; + + /// @notice Start timestamp of the current activity window. + uint256 public override fundActivityStartTime; + + uint256 public activityDelayTimeAfterRebalance; + + bool public override frozen = false; + + /// @dev Historical rebalances. Rebalances are often accessed in loops with bounds checking. + /// So we store them in a fixed-length array, in order to make compiler-generated + /// bounds checking on every access cheaper. The actual length of this array is stored in + /// `_rebalanceSize` and should be explicitly checked when necessary. + Rebalance[65535] private _rebalances; + + /// @dev Historical rebalance count. + uint256 private _rebalanceSize; + + /// @dev Total share supply of the three tranches. They are always rebalanced to the latest + /// version. + uint256[TRANCHE_COUNT] private _totalSupplies; + + /// @dev Mapping of account => share balance of the three tranches. + /// Rebalance versions are stored in a separate mapping `_balanceVersions`. + mapping(address => uint256[TRANCHE_COUNT]) private _balances; + + /// @dev Rebalance version mapping for `_balances`. + mapping(address => uint256) private _balanceVersions; + + /// @dev Mapping of owner => spender => share allowance of the three tranches. + /// Rebalance versions are stored in a separate mapping `_allowanceVersions`. + mapping(address => mapping(address => uint256[TRANCHE_COUNT])) private _allowances; + + /// @dev Rebalance version mapping for `_allowances`. + mapping(address => mapping(address => uint256)) private _allowanceVersions; + + /// @dev Mapping of trading day => NAV of BISHOP. + mapping(uint256 => uint256) private _historicalNavB; + + /// @dev Mapping of trading day => NAV of ROOK. + mapping(uint256 => uint256) private _historicalNavR; + + /// @notice Mapping of trading day => equivalent BISHOP supply. + /// + /// Key is the end timestamp of a trading day. Value is the total supply of BISHOP, + /// as if all QUEEN are split. + mapping(uint256 => uint256) public override historicalEquivalentTotalB; + + /// @notice Mapping of trading day => underlying assets in the fund. + /// + /// Key is the end timestamp of a trading day. Value is the underlying assets in + /// the fund after settlement of that trading day. + mapping(uint256 => uint256) public override historicalUnderlying; + + /// @notice Mapping of trading week => interest rate of BISHOP. + /// + /// Key is the end timestamp of a trading day. Value is the interest rate captured + /// after settlement of that day, which will be effective until the next settlement. + mapping(uint256 => uint256) public historicalInterestRate; + + /// @dev Amount of redemption underlying that the fund owes the primary market + uint256 private _totalDebt; + + uint256 private _strategyUnderlying; + + struct ConstructorParameters { + uint256 weightB; + uint256 settlementPeriod; + address tokenUnderlying; + uint256 underlyingDecimals; + address tokenQ; + address tokenB; + address tokenR; + address primaryMarket; + address strategy; + address twapOracle; + address aprOracle; + address feeCollector; + } + + constructor( + ConstructorParameters memory params + ) + public + Ownable() + FundRolesV2( + params.tokenQ, + params.tokenB, + params.tokenR, + params.primaryMarket, + params.strategy + ) + { + weightB = params.weightB; + require(params.settlementPeriod % 1 days == 0); + settlementPeriod = params.settlementPeriod; + tokenUnderlying = params.tokenUnderlying; + require(params.underlyingDecimals <= 18, "Underlying decimals larger than 18"); + underlyingDecimalMultiplier = 10 ** (18 - params.underlyingDecimals); + _updateTwapOracle(params.twapOracle); + _updateAprOracle(params.aprOracle); + _updateFeeCollector(params.feeCollector); + _updateActivityDelayTime(30 minutes); + } + + function initialize( + uint256 newSplitRatio, + uint256 lastNavB, + uint256 lastNavR, + uint256 strategyUnderlying + ) external onlyOwner { + require(splitRatio == 0 && currentDay == 0, "Already initialized"); + require(newSplitRatio != 0 && lastNavB >= UNIT, "Invalid parameters"); + splitRatio = newSplitRatio; + _historicalSplitRatio[0] = newSplitRatio; + emit SplitRatioUpdated(newSplitRatio); + uint256 lastDay = endOfDay(block.timestamp) - 1 days; + currentDay = lastDay + settlementPeriod; + uint256 lastDayPrice = twapOracle.getTwap(lastDay); + require(lastDayPrice != 0, "Price not available"); // required to do the first creation + _historicalNavB[lastDay] = lastNavB; + _historicalNavR[lastDay] = lastNavR; + _strategyUnderlying = strategyUnderlying; + uint256 lastInterestRate = _updateInterestRate(lastDay); + historicalInterestRate[lastDay] = lastInterestRate; + emit Settled(lastDay, lastNavB, lastNavR, lastInterestRate); + fundActivityStartTime = lastDay; + } + + /// @notice UTC time of a day when the fund settles. + function settlementTime() external pure returns (uint256) { + return SETTLEMENT_TIME; + } + + /// @notice Return end timestamp of the trading day containing a given timestamp. + /// + /// A trading day starts at UTC time `SETTLEMENT_TIME` of a day (inclusive) + /// and ends at the same time of the next day (exclusive). + /// @param timestamp The given timestamp + /// @return End timestamp of the trading day. + function endOfDay(uint256 timestamp) public pure override returns (uint256) { + return ((timestamp.add(1 days) - SETTLEMENT_TIME) / 1 days) * 1 days + SETTLEMENT_TIME; + } + + /// @notice Return end timestamp of the trading week containing a given timestamp. + /// + /// A trading week starts at UTC time `SETTLEMENT_TIME` on a Thursday (inclusive) + /// and ends at the same time of the next Thursday (exclusive). + /// @param timestamp The given timestamp + /// @return End timestamp of the trading week. + function endOfWeek(uint256 timestamp) external pure returns (uint256) { + return _endOfWeek(timestamp); + } + + function tokenQ() external view override returns (address) { + return _tokenQ; + } + + function tokenB() external view override returns (address) { + return _tokenB; + } + + function tokenR() external view override returns (address) { + return _tokenR; + } + + function tokenShare(uint256 tranche) external view override returns (address) { + return _getShare(tranche); + } + + function primaryMarket() external view override returns (address) { + return _primaryMarket; + } + + function primaryMarketUpdateProposal() external view override returns (address, uint256) { + return (_proposedPrimaryMarket, _proposedPrimaryMarketTimestamp); + } + + function strategy() external view override returns (address) { + return _strategy; + } + + function strategyUpdateProposal() external view override returns (address, uint256) { + return (_proposedStrategy, _proposedStrategyTimestamp); + } + + /// @notice Return the status of the fund contract. + /// @param timestamp Timestamp to assess + /// @return True if the fund contract is active + function isFundActive(uint256 timestamp) public view override returns (bool) { + return timestamp >= fundActivityStartTime; + } + + function getTotalUnderlying() public view override returns (uint256) { + uint256 hot = IERC20(tokenUnderlying).balanceOf(address(this)); + return hot.add(_strategyUnderlying).sub(_totalDebt); + } + + function getStrategyUnderlying() external view override returns (uint256) { + return _strategyUnderlying; + } + + /// @notice Get the amount of redemption underlying that the fund owes the primary market. + function getTotalDebt() external view override returns (uint256) { + return _totalDebt; + } + + /// @notice Equivalent ROOK supply, as if all QUEEN are split. + function getEquivalentTotalR() public view override returns (uint256) { + return _totalSupplies[TRANCHE_Q].multiplyDecimal(splitRatio).add(_totalSupplies[TRANCHE_R]); + } + + /// @notice Equivalent BISHOP supply, as if all QUEEN are split. + function getEquivalentTotalB() public view override returns (uint256) { + return getEquivalentTotalR().mul(weightB); + } + + /// @notice Equivalent QUEEN supply, as if all BISHOP and ROOK are merged. + function getEquivalentTotalQ() public view override returns (uint256) { + return _totalSupplies[TRANCHE_R].divideDecimal(splitRatio).add(_totalSupplies[TRANCHE_Q]); + } + + /// @notice Return the rebalance matrix at a given index. A zero struct is returned + /// if `index` is out of bound. + /// @param index Rebalance index + /// @return A rebalance matrix + function getRebalance(uint256 index) external view override returns (Rebalance memory) { + return _rebalances[index]; + } + + /// @notice Return timestamp of the transaction triggering the rebalance at a given index. + /// Zero is returned if `index` is out of bound. + /// @param index Rebalance index + /// @return Timestamp of the rebalance + function getRebalanceTimestamp(uint256 index) external view override returns (uint256) { + return _rebalances[index].timestamp; + } + + /// @notice Return the number of historical rebalances. + function getRebalanceSize() external view override returns (uint256) { + return _rebalanceSize; + } + + function getSettledDay() public view override returns (uint256) { + return currentDay - settlementPeriod; + } + + /// @notice Return split ratio at a given version. + /// Zero is returned if `version` is invalid. + /// @param version Rebalance version + /// @return Split ratio of the version + function historicalSplitRatio(uint256 version) external view override returns (uint256) { + return _historicalSplitRatio[version]; + } + + /// @notice Return NAV of BISHOP and ROOK of the given trading day. + /// @param day End timestamp of a trading day + /// @return navB NAV of BISHOP + /// @return navR NAV of ROOK + function historicalNavs( + uint256 day + ) external view override returns (uint256 navB, uint256 navR) { + return (_historicalNavB[day], _historicalNavR[day]); + } + + /// @notice Estimate the current NAV of all tranches, considering underlying price change, + /// and accrued interest since the previous settlement. + /// + /// The extrapolation uses simple interest instead of daily compound interest in + /// calculating BISHOP's interest. There may be significant error + /// in the returned values when `timestamp` is far beyond the last settlement. + /// @param price Price of the underlying asset (18 decimal places) + /// @return navSum Sum of navB * weightB and the estimated NAV of ROOK + /// @return navB Estimated NAV of BISHOP + /// @return navROrZero Estimated NAV of ROOK, or zero if the NAV is negative + function extrapolateNav( + uint256 price + ) external view override returns (uint256 navSum, uint256 navB, uint256 navROrZero) { + uint256 settledDay = getSettledDay(); + uint256 underlying = getTotalUnderlying(); + return + _extrapolateNav(block.timestamp, settledDay, price, getEquivalentTotalR(), underlying); + } + + function _extrapolateNav( + uint256 timestamp, + uint256 settledDay, + uint256 price, + uint256 equivalentTotalR, + uint256 underlying + ) private view returns (uint256 navSum, uint256 navB, uint256 navROrZero) { + navB = _historicalNavB[settledDay]; + if (equivalentTotalR > 0) { + navSum = price.mul(underlying.mul(underlyingDecimalMultiplier)).div(equivalentTotalR); + navB = navB.multiplyDecimal( + historicalInterestRate[settledDay].mul(timestamp - settledDay).div(1 days).add(UNIT) + ); + navROrZero = navSum >= navB.mul(weightB) ? navSum - navB * weightB : 0; + } else { + // If the fund is empty, use NAV in the last day + navROrZero = _historicalNavR[settledDay]; + navSum = navB.mul(weightB) + navROrZero; + } + } + + /// @notice Transform share amounts according to the rebalance at a given index. + /// This function performs no bounds checking on the given index. A non-existent + /// rebalance transforms anything to a zero vector. + /// @param amountQ Amount of QUEEN before the rebalance + /// @param amountB Amount of BISHOP before the rebalance + /// @param amountR Amount of ROOK before the rebalance + /// @param index Rebalance index + /// @return newAmountQ Amount of QUEEN after the rebalance + /// @return newAmountB Amount of BISHOP after the rebalance + /// @return newAmountR Amount of ROOK after the rebalance + function doRebalance( + uint256 amountQ, + uint256 amountB, + uint256 amountR, + uint256 index + ) public view override returns (uint256 newAmountQ, uint256 newAmountB, uint256 newAmountR) { + Rebalance storage rebalance = _rebalances[index]; + newAmountQ = amountQ.add(amountB.multiplyDecimal(rebalance.ratioB2Q)).add( + amountR.multiplyDecimal(rebalance.ratioR2Q) + ); + uint256 ratioBR = rebalance.ratioBR; // Gas saver + newAmountB = amountB.multiplyDecimal(ratioBR); + newAmountR = amountR.multiplyDecimal(ratioBR); + } + + /// @notice Transform share amounts according to rebalances in a given index range, + /// This function performs no bounds checking on the given indices. The original amounts + /// are returned if `fromIndex` is no less than `toIndex`. A zero vector is returned + /// if `toIndex` is greater than the number of existing rebalances. + /// @param amountQ Amount of QUEEN before the rebalance + /// @param amountB Amount of BISHOP before the rebalance + /// @param amountR Amount of ROOK before the rebalance + /// @param fromIndex Starting of the rebalance index range, inclusive + /// @param toIndex End of the rebalance index range, exclusive + /// @return newAmountQ Amount of QUEEN after the rebalance + /// @return newAmountB Amount of BISHOP after the rebalance + /// @return newAmountR Amount of ROOK after the rebalance + function batchRebalance( + uint256 amountQ, + uint256 amountB, + uint256 amountR, + uint256 fromIndex, + uint256 toIndex + ) external view override returns (uint256 newAmountQ, uint256 newAmountB, uint256 newAmountR) { + for (uint256 i = fromIndex; i < toIndex; i++) { + (amountQ, amountB, amountR) = doRebalance(amountQ, amountB, amountR, i); + } + newAmountQ = amountQ; + newAmountB = amountB; + newAmountR = amountR; + } + + /// @notice Transform share balance to a given rebalance version, or to the latest version + /// if `targetVersion` is zero. + /// @param account Account of the balance to rebalance + /// @param targetVersion The target rebalance version, or zero for the latest version + function refreshBalance(address account, uint256 targetVersion) external override { + if (targetVersion > 0) { + require(targetVersion <= _rebalanceSize, "Target version out of bound"); + } + _refreshBalance(account, targetVersion); + } + + /// @notice Transform allowance to a given rebalance version, or to the latest version + /// if `targetVersion` is zero. + /// @param owner Owner of the allowance to rebalance + /// @param spender Spender of the allowance to rebalance + /// @param targetVersion The target rebalance version, or zero for the latest version + function refreshAllowance( + address owner, + address spender, + uint256 targetVersion + ) external override { + if (targetVersion > 0) { + require(targetVersion <= _rebalanceSize, "Target version out of bound"); + } + _refreshAllowance(owner, spender, targetVersion); + } + + function trancheBalanceOf( + uint256 tranche, + address account + ) external view override returns (uint256) { + uint256 latestVersion = _rebalanceSize; + uint256 userVersion = _balanceVersions[account]; + if (userVersion == latestVersion) { + // Fast path + return _balances[account][tranche]; + } + + uint256 amountQ = _balances[account][TRANCHE_Q]; + uint256 amountB = _balances[account][TRANCHE_B]; + uint256 amountR = _balances[account][TRANCHE_R]; + for (uint256 i = userVersion; i < latestVersion; i++) { + (amountQ, amountB, amountR) = doRebalance(amountQ, amountB, amountR, i); + } + if (tranche == TRANCHE_Q) { + return amountQ; + } else if (tranche == TRANCHE_B) { + return amountB; + } else if (tranche == TRANCHE_R) { + return amountR; + } else { + revert("Invalid tranche"); + } + } + + /// @notice Return all three share balances transformed to the latest rebalance version. + /// @param account Owner of the shares + function trancheAllBalanceOf( + address account + ) external view override returns (uint256, uint256, uint256) { + uint256 amountQ = _balances[account][TRANCHE_Q]; + uint256 amountB = _balances[account][TRANCHE_B]; + uint256 amountR = _balances[account][TRANCHE_R]; + + uint256 size = _rebalanceSize; // Gas saver + for (uint256 i = _balanceVersions[account]; i < size; i++) { + (amountQ, amountB, amountR) = doRebalance(amountQ, amountB, amountR, i); + } + + return (amountQ, amountB, amountR); + } + + function trancheBalanceVersion(address account) external view override returns (uint256) { + return _balanceVersions[account]; + } + + function trancheAllowance( + uint256 tranche, + address owner, + address spender + ) external view override returns (uint256) { + uint256 allowance = _allowances[owner][spender][tranche]; + if (tranche != TRANCHE_Q) { + uint256 size = _rebalanceSize; // Gas saver + for (uint256 i = _allowanceVersions[owner][spender]; i < size; i++) { + allowance = _rebalanceAllowanceBR(allowance, i); + } + } + return allowance; + } + + function trancheAllowanceVersion( + address owner, + address spender + ) external view override returns (uint256) { + return _allowanceVersions[owner][spender]; + } + + function trancheTransfer( + uint256 tranche, + address recipient, + uint256 amount, + uint256 version + ) external override onlyCurrentVersion(version) { + _refreshBalance(msg.sender, version); + if (tranche != TRANCHE_Q) { + _refreshBalance(recipient, version); + } + _transfer(tranche, msg.sender, recipient, amount); + } + + function trancheTransferFrom( + uint256 tranche, + address sender, + address recipient, + uint256 amount, + uint256 version + ) external override onlyCurrentVersion(version) { + _refreshBalance(sender, version); + if (tranche != TRANCHE_Q) { + _refreshAllowance(sender, msg.sender, version); + _refreshBalance(recipient, version); + } + uint256 newAllowance = _allowances[sender][msg.sender][tranche].sub( + amount, + "ERC20: transfer amount exceeds allowance" + ); + _approve(tranche, sender, msg.sender, newAllowance); + _transfer(tranche, sender, recipient, amount); + } + + function trancheApprove( + uint256 tranche, + address spender, + uint256 amount, + uint256 version + ) external override onlyCurrentVersion(version) { + if (tranche != TRANCHE_Q) { + _refreshAllowance(msg.sender, spender, version); + } + _approve(tranche, msg.sender, spender, amount); + } + + function trancheTotalSupply(uint256 tranche) external view override returns (uint256) { + return _totalSupplies[tranche]; + } + + function primaryMarketMint( + uint256 tranche, + address account, + uint256 amount, + uint256 version + ) external override onlyPrimaryMarket onlyCurrentVersion(version) { + if (tranche != TRANCHE_Q) { + _refreshBalance(account, version); + } + _mint(tranche, account, amount); + } + + function primaryMarketBurn( + uint256 tranche, + address account, + uint256 amount, + uint256 version + ) external override onlyPrimaryMarket onlyCurrentVersion(version) { + // Unlike `primaryMarketMint()`, `_refreshBalance()` is required even if we are burning + // QUEEN tokens, because a rebalance may increase the user's QUEEN balance if the user + // owns BISHOP or ROOK tokens beforehand. + _refreshBalance(account, version); + _burn(tranche, account, amount); + } + + function shareTransfer(address sender, address recipient, uint256 amount) public override { + uint256 tranche = _getTranche(msg.sender); + if (tranche != TRANCHE_Q) { + require(isFundActive(block.timestamp), "Transfer is inactive"); + _refreshBalance(recipient, _rebalanceSize); + } + _refreshBalance(sender, _rebalanceSize); + _transfer(tranche, sender, recipient, amount); + } + + function shareTransferFrom( + address spender, + address sender, + address recipient, + uint256 amount + ) external override returns (uint256 newAllowance) { + uint256 tranche = _getTranche(msg.sender); + shareTransfer(sender, recipient, amount); + if (tranche != TRANCHE_Q) { + _refreshAllowance(sender, spender, _rebalanceSize); + } + newAllowance = _allowances[sender][spender][tranche].sub( + amount, + "ERC20: transfer amount exceeds allowance" + ); + _approve(tranche, sender, spender, newAllowance); + } + + function shareApprove(address owner, address spender, uint256 amount) external override { + uint256 tranche = _getTranche(msg.sender); + if (tranche != TRANCHE_Q) { + _refreshAllowance(owner, spender, _rebalanceSize); + } + _approve(tranche, owner, spender, amount); + } + + function shareIncreaseAllowance( + address sender, + address spender, + uint256 addedValue + ) external override returns (uint256 newAllowance) { + uint256 tranche = _getTranche(msg.sender); + if (tranche != TRANCHE_Q) { + _refreshAllowance(sender, spender, _rebalanceSize); + } + newAllowance = _allowances[sender][spender][tranche].add(addedValue); + _approve(tranche, sender, spender, newAllowance); + } + + function shareDecreaseAllowance( + address sender, + address spender, + uint256 subtractedValue + ) external override returns (uint256 newAllowance) { + uint256 tranche = _getTranche(msg.sender); + if (tranche != TRANCHE_Q) { + _refreshAllowance(sender, spender, _rebalanceSize); + } + newAllowance = _allowances[sender][spender][tranche].sub(subtractedValue); + _approve(tranche, sender, spender, newAllowance); + } + + function _transfer(uint256 tranche, address sender, address recipient, uint256 amount) private { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + _balances[sender][tranche] = _balances[sender][tranche].sub( + amount, + "ERC20: transfer amount exceeds balance" + ); + _balances[recipient][tranche] = _balances[recipient][tranche].add(amount); + IShareV2(_getShare(tranche)).fundEmitTransfer(sender, recipient, amount); + } + + function _mint(uint256 tranche, address account, uint256 amount) private { + require(account != address(0), "ERC20: mint to the zero address"); + _totalSupplies[tranche] = _totalSupplies[tranche].add(amount); + _balances[account][tranche] = _balances[account][tranche].add(amount); + IShareV2(_getShare(tranche)).fundEmitTransfer(address(0), account, amount); + } + + function _burn(uint256 tranche, address account, uint256 amount) private { + require(account != address(0), "ERC20: burn from the zero address"); + _balances[account][tranche] = _balances[account][tranche].sub( + amount, + "ERC20: burn amount exceeds balance" + ); + _totalSupplies[tranche] = _totalSupplies[tranche].sub(amount); + IShareV2(_getShare(tranche)).fundEmitTransfer(account, address(0), amount); + } + + function _approve(uint256 tranche, address owner, address spender, uint256 amount) private { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + _allowances[owner][spender][tranche] = amount; + IShareV2(_getShare(tranche)).fundEmitApproval(owner, spender, amount); + } + + /// @notice Settle the current trading day. Settlement includes the following changes + /// to the fund. + /// + /// 1. Settle all pending creations and redemptions from the primary market. + /// 2. Calculate NAV of the day and trigger rebalance if necessary. + /// 3. Capture new interest rate for BISHOP. + function settle() external nonReentrant onlyNotFrozen { + uint256 day = currentDay; + require(day != 0, "Not initialized"); + require(block.timestamp >= day, "The current trading year does not end yet"); + uint256 price = twapOracle.getTwap(day); + require(price != 0, "Underlying price for settlement is not ready yet"); + + IPrimaryMarketV3(_primaryMarket).settle(day); + + // Calculate NAV + uint256 underlying = getTotalUnderlying(); + (uint256 navSum, uint256 navB, uint256 navR) = _extrapolateNav( + day, + day - settlementPeriod, + price, + getEquivalentTotalR(), + underlying + ); + require(navR > 0, "To be frozen"); + + uint256 newSplitRatio = splitRatio.multiplyDecimal(navSum) / (weightB + 1); + _triggerRebalance(day, navSum, navB, navR, newSplitRatio); + navB = UNIT; + navR = UNIT; + fundActivityStartTime = day + activityDelayTimeAfterRebalance; + + historicalEquivalentTotalB[day] = getEquivalentTotalB(); + historicalUnderlying[day] = underlying; + _historicalNavB[day] = navB; + _historicalNavR[day] = navR; + uint256 interestRate = _updateInterestRate(day); + historicalInterestRate[day] = interestRate; + currentDay = day + settlementPeriod; + + emit Settled(day, navB, navR, interestRate); + } + + function transferToStrategy(uint256 amount) external override onlyStrategy { + _strategyUnderlying = _strategyUnderlying.add(amount); + IERC20(tokenUnderlying).safeTransfer(_strategy, amount); + } + + function transferFromStrategy(uint256 amount) external override onlyStrategy { + _strategyUnderlying = _strategyUnderlying.sub(amount); + IERC20(tokenUnderlying).safeTransferFrom(_strategy, address(this), amount); + } + + function primaryMarketTransferUnderlying( + address recipient, + uint256 amount, + uint256 feeQ + ) external override onlyPrimaryMarket { + IERC20(tokenUnderlying).safeTransfer(recipient, amount); + _mint(TRANCHE_Q, feeCollector, feeQ); + } + + function primaryMarketAddDebtAndFee( + uint256 amount, + uint256 feeQ + ) external override onlyPrimaryMarket { + _mint(TRANCHE_Q, feeCollector, feeQ); + _updateTotalDebt(_totalDebt.add(amount)); + } + + function primaryMarketPayDebt(uint256 amount) external override onlyPrimaryMarket { + _updateTotalDebt(_totalDebt.sub(amount)); + IERC20(tokenUnderlying).safeTransfer(msg.sender, amount); + } + + function reportProfit( + uint256 profit, + uint256 totalFee, + uint256 strategyFee + ) external override onlyStrategy returns (uint256 strategyFeeQ) { + require(profit >= totalFee && totalFee >= strategyFee, "Fee cannot exceed profit"); + _strategyUnderlying = _strategyUnderlying.add(profit); + uint256 equivalentTotalQ = getEquivalentTotalQ(); + uint256 totalUnderlyingAfterFee = getTotalUnderlying() - totalFee; + uint256 totalFeeQ = totalFee.mul(equivalentTotalQ).div(totalUnderlyingAfterFee); + strategyFeeQ = strategyFee.mul(equivalentTotalQ).div(totalUnderlyingAfterFee); + _mint(TRANCHE_Q, feeCollector, totalFeeQ.sub(strategyFeeQ)); + _mint(TRANCHE_Q, msg.sender, strategyFeeQ); + emit ProfitReported(profit, totalFee, totalFeeQ, strategyFeeQ); + } + + function reportLoss(uint256 loss) external override onlyStrategy { + _strategyUnderlying = _strategyUnderlying.sub(loss); + emit LossReported(loss); + } + + function proposePrimaryMarketUpdate(address newPrimaryMarket) external onlyOwner { + _proposePrimaryMarketUpdate(newPrimaryMarket); + } + + function applyPrimaryMarketUpdate(address newPrimaryMarket) external onlyOwner { + require( + IPrimaryMarketV3(_primaryMarket).canBeRemovedFromFund(), + "Cannot update primary market" + ); + _applyPrimaryMarketUpdate(newPrimaryMarket); + } + + function proposeStrategyUpdate(address newStrategy) external onlyOwner { + _proposeStrategyUpdate(newStrategy); + } + + function applyStrategyUpdate(address newStrategy) external onlyOwner { + require(_totalDebt == 0, "Cannot update strategy with debt"); + _applyStrategyUpdate(newStrategy); + } + + function _updateTwapOracle(address newTwapOracle) private { + twapOracle = ITwapOracleV2(newTwapOracle); + emit TwapOracleUpdated(newTwapOracle); + } + + function updateTwapOracle(address newTwapOracle) external onlyOwner { + _updateTwapOracle(newTwapOracle); + } + + function _updateAprOracle(address newAprOracle) private { + aprOracle = IAprOracle(newAprOracle); + emit AprOracleUpdated(newAprOracle); + } + + function updateAprOracle(address newAprOracle) external onlyOwner { + _updateAprOracle(newAprOracle); + } + + function _updateFeeCollector(address newFeeCollector) private { + feeCollector = newFeeCollector; + emit FeeCollectorUpdated(newFeeCollector); + } + + function updateFeeCollector(address newFeeCollector) external onlyOwner { + _updateFeeCollector(newFeeCollector); + } + + function _updateActivityDelayTime(uint256 delayTime) private { + require( + delayTime >= 30 minutes && delayTime <= 12 hours, + "Exceed allowed delay time range" + ); + activityDelayTimeAfterRebalance = delayTime; + emit ActivityDelayTimeUpdated(delayTime); + } + + function updateActivityDelayTime(uint256 delayTime) external onlyOwner { + _updateActivityDelayTime(delayTime); + } + + /// @dev Create a new rebalance that resets NAV of all tranches to 1. Total supplies are + /// rebalanced immediately. + /// @param day Trading day that triggers this rebalance + /// @param navB BISHOP's NAV before this rebalance + /// @param navR ROOK's NAV before this rebalance + /// @param newSplitRatio The new split ratio after this rebalance + function _triggerRebalance( + uint256 day, + uint256 navSum, + uint256 navB, + uint256 navR, + uint256 newSplitRatio + ) private { + Rebalance memory rebalance = _calculateRebalance(navB, navR, newSplitRatio); + uint256 oldSize = _rebalanceSize; + splitRatio = newSplitRatio; + _historicalSplitRatio[oldSize + 1] = newSplitRatio; + emit SplitRatioUpdated(newSplitRatio); + _rebalances[oldSize] = rebalance; + _rebalanceSize = oldSize + 1; + emit RebalanceTriggered( + oldSize, + day, + navSum, + navB, + navR, + rebalance.ratioB2Q, + rebalance.ratioR2Q, + rebalance.ratioBR + ); + + ( + _totalSupplies[TRANCHE_Q], + _totalSupplies[TRANCHE_B], + _totalSupplies[TRANCHE_R] + ) = doRebalance( + _totalSupplies[TRANCHE_Q], + _totalSupplies[TRANCHE_B], + _totalSupplies[TRANCHE_R], + oldSize + ); + _refreshBalance(address(this), oldSize + 1); + } + + /// @dev Create a new rebalance matrix that resets given NAVs to (1, 1). + /// @param navB BISHOP's NAV before the rebalance + /// @param navR ROOK's NAV before the rebalance + /// @param newSplitRatio The new split ratio after this rebalance + /// @return The rebalance matrix + function _calculateRebalance( + uint256 navB, + uint256 navR, + uint256 newSplitRatio + ) private view returns (Rebalance memory) { + uint256 ratioBR; + uint256 ratioB2Q; + uint256 ratioR2Q; + if (navR <= navB) { + ratioBR = navR; + ratioB2Q = (navB - navR).divideDecimal(newSplitRatio) / (weightB + 1); + ratioR2Q = 0; + } else { + ratioBR = navB; + ratioB2Q = 0; + ratioR2Q = (navR - navB).divideDecimal(newSplitRatio) / (weightB + 1); + } + return + Rebalance({ + ratioB2Q: ratioB2Q, + ratioR2Q: ratioR2Q, + ratioBR: ratioBR, + timestamp: block.timestamp + }); + } + + function _updateInterestRate(uint256) private returns (uint256) { + uint256 rate = MAX_INTEREST_RATE.min(aprOracle.capture()); + emit InterestRateUpdated(rate, 0); + + return rate; + } + + function _updateTotalDebt(uint256 newTotalDebt) private { + _totalDebt = newTotalDebt; + emit TotalDebtUpdated(newTotalDebt); + } + + /// @dev Transform share balance to a given rebalance version, or to the latest version + /// if `targetVersion` is zero. This function does no bound check on `targetVersion`. + /// @param account Account of the balance to rebalance + /// @param targetVersion The target rebalance version, or zero for the latest version + function _refreshBalance(address account, uint256 targetVersion) private { + if (targetVersion == 0) { + targetVersion = _rebalanceSize; + } + uint256 oldVersion = _balanceVersions[account]; + if (oldVersion >= targetVersion) { + return; + } + + uint256[TRANCHE_COUNT] storage balanceTuple = _balances[account]; + uint256 balanceQ = balanceTuple[TRANCHE_Q]; + uint256 balanceB = balanceTuple[TRANCHE_B]; + uint256 balanceR = balanceTuple[TRANCHE_R]; + _balanceVersions[account] = targetVersion; + + if (balanceB == 0 && balanceR == 0) { + // Fast path for zero BISHOP and ROOK balance + return; + } + + for (uint256 i = oldVersion; i < targetVersion; i++) { + (balanceQ, balanceB, balanceR) = doRebalance(balanceQ, balanceB, balanceR, i); + } + balanceTuple[TRANCHE_Q] = balanceQ; + balanceTuple[TRANCHE_B] = balanceB; + balanceTuple[TRANCHE_R] = balanceR; + + emit BalancesRebalanced(account, targetVersion, balanceQ, balanceB, balanceR); + } + + /// @dev Transform allowance to a given rebalance version, or to the latest version + /// if `targetVersion` is zero. This function does no bound check on `targetVersion`. + /// @param owner Owner of the allowance to rebalance + /// @param spender Spender of the allowance to rebalance + /// @param targetVersion The target rebalance version, or zero for the latest version + function _refreshAllowance(address owner, address spender, uint256 targetVersion) private { + if (targetVersion == 0) { + targetVersion = _rebalanceSize; + } + uint256 oldVersion = _allowanceVersions[owner][spender]; + if (oldVersion >= targetVersion) { + return; + } + + uint256[TRANCHE_COUNT] storage allowanceTuple = _allowances[owner][spender]; + uint256 allowanceB = allowanceTuple[TRANCHE_B]; + uint256 allowanceR = allowanceTuple[TRANCHE_R]; + _allowanceVersions[owner][spender] = targetVersion; + + if (allowanceB == 0 && allowanceR == 0) { + // Fast path for empty BISHOP and ROOK allowance + return; + } + + for (uint256 i = oldVersion; i < targetVersion; i++) { + allowanceB = _rebalanceAllowanceBR(allowanceB, i); + allowanceR = _rebalanceAllowanceBR(allowanceR, i); + } + allowanceTuple[TRANCHE_B] = allowanceB; + allowanceTuple[TRANCHE_R] = allowanceR; + + emit AllowancesRebalanced( + owner, + spender, + targetVersion, + allowanceTuple[TRANCHE_Q], + allowanceB, + allowanceR + ); + } + + function _rebalanceAllowanceBR( + uint256 allowance, + uint256 index + ) private view returns (uint256) { + Rebalance storage rebalance = _rebalances[index]; + /// @dev using saturating arithmetic to avoid unconscious overflow revert + return allowance.saturatingMultiplyDecimal(rebalance.ratioBR); + } + + modifier onlyCurrentVersion(uint256 version) { + require(_rebalanceSize == version, "Only current version"); + _; + } + + modifier onlyNotFrozen() { + require(!frozen, "Frozen"); + _; + } +} diff --git a/contracts/fund/PrimaryMarketV5.sol b/contracts/fund/PrimaryMarketV5.sol new file mode 100644 index 00000000..0bb92cb3 --- /dev/null +++ b/contracts/fund/PrimaryMarketV5.sol @@ -0,0 +1,505 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/math/Math.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "../utils/SafeDecimalMath.sol"; + +import "../interfaces/IPrimaryMarketV5.sol"; +import "../interfaces/IFundV5.sol"; +import "../interfaces/IFundForPrimaryMarketV4.sol"; +import "../interfaces/ITrancheIndexV2.sol"; +import "../interfaces/IWrappedERC20.sol"; + +contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, Ownable { + event Created(address indexed account, uint256 underlying, uint256 outQ); + event Redeemed(address indexed account, uint256 inQ, uint256 underlying, uint256 feeQ); + event Split(address indexed account, uint256 inQ, uint256 outB, uint256 outR); + event Merged( + address indexed account, + uint256 outQ, + uint256 inB, + uint256 inR, + uint256 feeUnderlying + ); + event RedemptionQueued(address indexed account, uint256 index, uint256 underlying); + event RedemptionPopped(uint256 count, uint256 newHead, uint256 requiredUnderlying); + event RedemptionClaimed(address indexed account, uint256 index, uint256 underlying); + event FundCapUpdated(uint256 newCap); + event RedemptionFeeRateUpdated(uint256 newRedemptionFeeRate); + event MergeFeeRateUpdated(uint256 newMergeFeeRate); + + using SafeMath for uint256; + using SafeDecimalMath for uint256; + using SafeERC20 for IERC20; + + struct QueuedRedemption { + address account; + uint256 underlying; + uint256 previousPrefixSum; + } + + uint256 private constant MAX_REDEMPTION_FEE_RATE = 0.01e18; + uint256 private constant MAX_MERGE_FEE_RATE = 0.01e18; + + address public immutable override fund; + bool public immutable redemptionFlag; + uint256 private immutable _weightB; + IERC20 private immutable _tokenUnderlying; + + uint256 public redemptionFeeRate; + uint256 public mergeFeeRate; + + /// @notice The upper limit of underlying that the fund can hold. This contract rejects + /// creations that may break this limit. + /// @dev This limit can be bypassed if the fund has multiple primary markets. + /// + /// Set it to uint(-1) to skip the check and save gas. + uint256 public fundCap; + + /// @notice Queue of redemptions that cannot be claimed yet. Key is a sequential index + /// starting from zero. Value is a tuple of user address, redeemed underlying and + /// prefix sum before this entry. + mapping(uint256 => QueuedRedemption) public queuedRedemptions; + + /// @notice Total underlying tokens of claimable queued redemptions. + uint256 public claimableUnderlying; + + /// @notice Index of the redemption queue head. All redemptions with index smaller than + /// this value can be claimed now. + uint256 public redemptionQueueHead; + + /// @notice Index of the redemption following the last entry of the queue. The next queued + /// redemption will be written at this index. + uint256 public redemptionQueueTail; + + constructor( + address fund_, + uint256 redemptionFeeRate_, + uint256 mergeFeeRate_, + uint256 fundCap_, + bool redemptionFlag_ + ) public Ownable() { + fund = fund_; + _tokenUnderlying = IERC20(IFundV3(fund_).tokenUnderlying()); + _updateRedemptionFeeRate(redemptionFeeRate_); + _updateMergeFeeRate(mergeFeeRate_); + _updateFundCap(fundCap_); + _weightB = IFundV5(fund_).weightB(); + redemptionFlag = redemptionFlag_; + } + + /// @notice Calculate the result of a creation. + /// @param underlying Underlying amount spent for the creation + /// @return outQ Created QUEEN amount + function getCreation(uint256 underlying) public view override returns (uint256 outQ) { + uint256 fundUnderlying = IFundV3(fund).getTotalUnderlying(); + uint256 fundEquivalentTotalQ = IFundV3(fund).getEquivalentTotalQ(); + require(fundUnderlying.add(underlying) <= fundCap, "Exceed fund cap"); + if (fundEquivalentTotalQ == 0) { + outQ = underlying.mul(IFundV3(fund).underlyingDecimalMultiplier()); + uint256 splitRatio = IFundV3(fund).splitRatio(); + require(splitRatio != 0, "Fund is not initialized"); + uint256 settledDay = IFundV5(fund).getSettledDay(); + uint256 underlyingPrice = IFundV3(fund).twapOracle().getTwap(settledDay); + (uint256 navB, uint256 navR) = IFundV3(fund).historicalNavs(settledDay); + outQ = outQ.mul(underlyingPrice).div(splitRatio).divideDecimal( + navB.mul(_weightB).add(navR) + ); + } else { + require( + fundUnderlying != 0, + "Cannot create QUEEN for fund with shares but no underlying" + ); + outQ = underlying.mul(fundEquivalentTotalQ).div(fundUnderlying); + } + } + + function _getRedemption(uint256 inQ) private view returns (uint256 underlying) { + uint256 fundUnderlying = IFundV3(fund).getTotalUnderlying(); + uint256 fundEquivalentTotalQ = IFundV3(fund).getEquivalentTotalQ(); + underlying = inQ.mul(fundUnderlying).div(fundEquivalentTotalQ); + } + + /// @notice Calculate the result of a redemption. + /// @param inQ QUEEN amount spent for the redemption + /// @return underlying Redeemed underlying amount + /// @return feeQ QUEEN amount charged as redemption fee + function getRedemption( + uint256 inQ + ) public view override returns (uint256 underlying, uint256 feeQ) { + feeQ = inQ.multiplyDecimal(redemptionFeeRate); + underlying = _getRedemption(inQ - feeQ); + } + + /// @notice Calculate the result of a split. + /// @param inQ QUEEN amount to be split + /// @return outB Received BISHOP amount + /// @return outR Received ROOK amount + function getSplit(uint256 inQ) public view override returns (uint256 outB, uint256 outR) { + outR = inQ.multiplyDecimal(IFundV5(fund).splitRatio()); + outB = outR.mul(_weightB); + } + + /// @notice Calculate the result of a merge. + /// @param inB Spent BISHOP amount + /// @return inR Spent ROOK amount + /// @return outQ Received QUEEN amount + /// @return feeQ QUEEN amount charged as merge fee + function getMerge( + uint256 inB + ) public view override returns (uint256 inR, uint256 outQ, uint256 feeQ) { + uint256 splitRatio = IFundV5(fund).splitRatio(); + uint256 outQBeforeFee = inB.divideDecimal(splitRatio.mul(_weightB)); + feeQ = outQBeforeFee.multiplyDecimal(mergeFeeRate); + outQ = outQBeforeFee.sub(feeQ); + if (IFundV5(fund).frozen()) { + inR = 0; + } else { + inR = outQBeforeFee.multiplyDecimal(splitRatio); + } + } + + /// @notice Return index of the first queued redemption that cannot be claimed now. + /// Users can use this function to determine which indices can be passed to + /// `claimRedemptions()`. + /// @return Index of the first redemption that cannot be claimed now + function getNewRedemptionQueueHead() external view returns (uint256) { + uint256 available = _tokenUnderlying.balanceOf(fund); + uint256 l = redemptionQueueHead; + uint256 r = redemptionQueueTail; + uint256 startPrefixSum = queuedRedemptions[l].previousPrefixSum; + // overflow is desired + if (queuedRedemptions[r].previousPrefixSum - startPrefixSum <= available) { + return r; + } + // Iteration count is bounded by log2(tail - head), which is at most 256. + while (l + 1 < r) { + uint256 m = (l + r) / 2; + if (queuedRedemptions[m].previousPrefixSum - startPrefixSum <= available) { + l = m; + } else { + r = m; + } + } + return l; + } + + /// @notice Search in the redemption queue. + /// @param account Owner of the redemptions, or zero address to return all redemptions + /// @param startIndex Redemption index where the search starts, or zero to start from the head + /// @param maxIterationCount Maximum number of redemptions to be scanned, or zero for no limit + /// @return indices Indices of found redemptions + /// @return underlying Total underlying of found redemptions + function getQueuedRedemptions( + address account, + uint256 startIndex, + uint256 maxIterationCount + ) external view returns (uint256[] memory indices, uint256 underlying) { + uint256 head = redemptionQueueHead; + uint256 tail = redemptionQueueTail; + if (startIndex == 0) { + startIndex = head; + } else { + require(startIndex >= head && startIndex <= tail, "startIndex out of bound"); + } + uint256 endIndex = tail; + if (maxIterationCount != 0 && tail - startIndex > maxIterationCount) { + endIndex = startIndex + maxIterationCount; + } + indices = new uint256[](endIndex - startIndex); + uint256 count = 0; + for (uint256 i = startIndex; i < endIndex; i++) { + if (account == address(0) || queuedRedemptions[i].account == account) { + indices[count] = i; + underlying += queuedRedemptions[i].underlying; + count++; + } + } + if (count != endIndex - startIndex) { + // Shrink the array + assembly { + mstore(indices, count) + } + } + } + + /// @notice Return whether the fund can change its primary market to another contract. + function canBeRemovedFromFund() external view override returns (bool) { + return redemptionQueueHead == redemptionQueueTail; + } + + /// @notice Create QUEEN using underlying tokens. This function should be called by + /// a smart contract, which transfers underlying tokens to this contract + /// in the same transaction. + /// @param recipient Address that will receive created QUEEN + /// @param minOutQ Minimum QUEEN amount to be received + /// @param version The latest rebalance version + /// @return outQ Received QUEEN amount + function create( + address recipient, + uint256 minOutQ, + uint256 version + ) external override nonReentrant returns (uint256 outQ) { + uint256 underlying = _tokenUnderlying.balanceOf(address(this)).sub(claimableUnderlying); + outQ = getCreation(underlying); + require(outQ >= minOutQ && outQ > 0, "Min QUEEN created"); + IFundForPrimaryMarketV4(fund).primaryMarketMint(TRANCHE_Q, recipient, outQ, version); + _tokenUnderlying.safeTransfer(fund, underlying); + emit Created(recipient, underlying, outQ); + } + + /// @notice Redeem QUEEN to get underlying tokens back. Revert if there are still some + /// queued redemptions that cannot be claimed now. + /// @param recipient Address that will receive redeemed underlying tokens + /// @param inQ Spent QUEEN amount + /// @param minUnderlying Minimum amount of underlying tokens to be received + /// @param version The latest rebalance version + /// @return underlying Received underlying amount + function redeem( + address recipient, + uint256 inQ, + uint256 minUnderlying, + uint256 version + ) external override nonReentrant returns (uint256 underlying) { + underlying = _redeem(recipient, inQ, minUnderlying, version); + } + + /// @notice Redeem QUEEN to get native currency back. The underlying must be wrapped token + /// of the native currency. Revert if there are still some queued redemptions that + /// cannot be claimed now. + /// @param recipient Address that will receive redeemed underlying tokens + /// @param inQ Spent QUEEN amount + /// @param minUnderlying Minimum amount of underlying tokens to be received + /// @param version The latest rebalance version + /// @return underlying Received underlying amount + function redeemAndUnwrap( + address recipient, + uint256 inQ, + uint256 minUnderlying, + uint256 version + ) external override nonReentrant returns (uint256 underlying) { + underlying = _redeem(address(this), inQ, minUnderlying, version); + IWrappedERC20(address(_tokenUnderlying)).withdraw(underlying); + (bool success, ) = recipient.call{value: underlying}(""); + require(success, "Transfer failed"); + } + + function _redeem( + address recipient, + uint256 inQ, + uint256 minUnderlying, + uint256 version + ) private allowRedemption returns (uint256 underlying) { + uint256 feeQ; + (underlying, feeQ) = getRedemption(inQ); + IFundForPrimaryMarketV4(fund).primaryMarketBurn(TRANCHE_Q, msg.sender, inQ, version); + _popRedemptionQueue(0); + require(underlying >= minUnderlying && underlying > 0, "Min underlying redeemed"); + // Redundant check for user-friendly revert message. + require(underlying <= _tokenUnderlying.balanceOf(fund), "Not enough underlying in fund"); + IFundForPrimaryMarketV4(fund).primaryMarketTransferUnderlying(recipient, underlying, feeQ); + emit Redeemed(recipient, inQ, underlying, feeQ); + } + + /// @notice Redeem QUEEN and wait in the redemption queue. Redeemed underlying tokens will + /// be claimable when the fund has enough balance to pay this redemption and all + /// previous ones in the queue. + /// @param recipient Address that will receive redeemed underlying tokens + /// @param inQ Spent QUEEN amount + /// @param minUnderlying Minimum amount of underlying tokens to be received + /// @param version The latest rebalance version + /// @return underlying Received underlying amount + /// @return index Index of the queued redemption + function queueRedemption( + address recipient, + uint256 inQ, + uint256 minUnderlying, + uint256 version + ) external override nonReentrant allowRedemption returns (uint256 underlying, uint256 index) { + uint256 feeQ; + (underlying, feeQ) = getRedemption(inQ); + IFundForPrimaryMarketV4(fund).primaryMarketBurn(TRANCHE_Q, msg.sender, inQ, version); + require(underlying >= minUnderlying && underlying > 0, "Min underlying redeemed"); + index = redemptionQueueTail; + QueuedRedemption storage newRedemption = queuedRedemptions[index]; + newRedemption.account = recipient; + newRedemption.underlying = underlying; + // overflow is desired + queuedRedemptions[index + 1].previousPrefixSum = + newRedemption.previousPrefixSum + + underlying; + redemptionQueueTail = index + 1; + IFundForPrimaryMarketV4(fund).primaryMarketAddDebtAndFee(underlying, feeQ); + emit Redeemed(recipient, inQ, underlying, feeQ); + emit RedemptionQueued(recipient, index, underlying); + } + + /// @notice Remove a given number of redemptions from the front of the redemption queue and + /// fetch underlying tokens of these redemptions from the fund. Revert if the fund + /// cannot pay these redemptions now. + /// @param count The number of redemptions to be removed, or zero to completely empty the queue + function popRedemptionQueue(uint256 count) external nonReentrant { + _popRedemptionQueue(count); + } + + function _popRedemptionQueue(uint256 count) private { + uint256 oldHead = redemptionQueueHead; + uint256 oldTail = redemptionQueueTail; + uint256 newHead; + if (count == 0) { + if (oldHead == oldTail) { + return; + } + newHead = oldTail; + } else { + newHead = oldHead.add(count); + require(newHead <= oldTail, "Redemption queue out of bound"); + } + // overflow is desired + uint256 requiredUnderlying = queuedRedemptions[newHead].previousPrefixSum - + queuedRedemptions[oldHead].previousPrefixSum; + // Redundant check for user-friendly revert message. + require( + requiredUnderlying <= _tokenUnderlying.balanceOf(fund), + "Not enough underlying in fund" + ); + claimableUnderlying = claimableUnderlying.add(requiredUnderlying); + IFundForPrimaryMarketV4(fund).primaryMarketPayDebt(requiredUnderlying); + redemptionQueueHead = newHead; + emit RedemptionPopped(newHead - oldHead, newHead, requiredUnderlying); + } + + /// @notice Claim underlying tokens of queued redemptions. All these redemptions must + /// belong to the same account. + /// @param account Recipient of the redemptions + /// @param indices Indices of the redemptions in the queue, which must be in increasing order + /// @return underlying Total claimed underlying amount + function claimRedemptions( + address account, + uint256[] calldata indices + ) external override nonReentrant returns (uint256 underlying) { + underlying = _claimRedemptions(account, indices); + _tokenUnderlying.safeTransfer(account, underlying); + } + + /// @notice Claim native currency of queued redemptions. The underlying must be wrapped token + /// of the native currency. All these redemptions must belong to the same account. + /// @param account Recipient of the redemptions + /// @param indices Indices of the redemptions in the queue, which must be in increasing order + /// @return underlying Total claimed underlying amount + function claimRedemptionsAndUnwrap( + address account, + uint256[] calldata indices + ) external override nonReentrant returns (uint256 underlying) { + underlying = _claimRedemptions(account, indices); + IWrappedERC20(address(_tokenUnderlying)).withdraw(underlying); + (bool success, ) = account.call{value: underlying}(""); + require(success, "Transfer failed"); + } + + function _claimRedemptions( + address account, + uint256[] calldata indices + ) private returns (uint256 underlying) { + uint256 count = indices.length; + if (count == 0) { + return 0; + } + uint256 head = redemptionQueueHead; + if (indices[count - 1] >= head) { + _popRedemptionQueue(indices[count - 1] - head + 1); + } + for (uint256 i = 0; i < count; i++) { + require(i == 0 || indices[i] > indices[i - 1], "Indices out of order"); + QueuedRedemption storage redemption = queuedRedemptions[indices[i]]; + uint256 redemptionUnderlying = redemption.underlying; + require( + redemption.account == account && redemptionUnderlying != 0, + "Invalid redemption index" + ); + underlying = underlying.add(redemptionUnderlying); + emit RedemptionClaimed(account, indices[i], redemptionUnderlying); + delete queuedRedemptions[indices[i]]; + } + claimableUnderlying = claimableUnderlying.sub(underlying); + } + + function split( + address recipient, + uint256 inQ, + uint256 version + ) external override returns (uint256 outB, uint256 outR) { + (outB, outR) = getSplit(inQ); + IFundForPrimaryMarketV4(fund).primaryMarketBurn(TRANCHE_Q, msg.sender, inQ, version); + IFundForPrimaryMarketV4(fund).primaryMarketMint(TRANCHE_B, recipient, outB, version); + IFundForPrimaryMarketV4(fund).primaryMarketMint(TRANCHE_R, recipient, outR, version); + emit Split(recipient, inQ, outB, outR); + } + + function merge( + address recipient, + uint256 inB, + uint256 version + ) external override returns (uint256 outQ) { + uint256 inR; + uint256 feeQ; + (inR, outQ, feeQ) = getMerge(inB); + IFundForPrimaryMarketV4(fund).primaryMarketBurn(TRANCHE_B, msg.sender, inB, version); + IFundForPrimaryMarketV4(fund).primaryMarketBurn(TRANCHE_R, msg.sender, inR, version); + IFundForPrimaryMarketV4(fund).primaryMarketMint(TRANCHE_Q, recipient, outQ, version); + IFundForPrimaryMarketV4(fund).primaryMarketAddDebtAndFee(0, feeQ); + emit Merged(recipient, outQ, inB, inB, feeQ); + } + + /// @dev Nothing to do for daily fund settlement. + function settle(uint256 day) external override onlyFund {} + + function _updateFundCap(uint256 newCap) private { + fundCap = newCap; + emit FundCapUpdated(newCap); + } + + function updateFundCap(uint256 newCap) external onlyOwner { + _updateFundCap(newCap); + } + + function _updateRedemptionFeeRate(uint256 newRedemptionFeeRate) private { + require(newRedemptionFeeRate <= MAX_REDEMPTION_FEE_RATE, "Exceed max redemption fee rate"); + redemptionFeeRate = newRedemptionFeeRate; + emit RedemptionFeeRateUpdated(newRedemptionFeeRate); + } + + function updateRedemptionFeeRate(uint256 newRedemptionFeeRate) external onlyOwner { + _updateRedemptionFeeRate(newRedemptionFeeRate); + } + + function _updateMergeFeeRate(uint256 newMergeFeeRate) private { + require(newMergeFeeRate <= MAX_MERGE_FEE_RATE, "Exceed max merge fee rate"); + mergeFeeRate = newMergeFeeRate; + emit MergeFeeRateUpdated(newMergeFeeRate); + } + + function updateMergeFeeRate(uint256 newMergeFeeRate) external onlyOwner { + _updateMergeFeeRate(newMergeFeeRate); + } + + /// @notice Receive unwrapped transfer from the wrapped token. + receive() external payable {} + + modifier onlyFund() { + require(msg.sender == fund, "Only fund"); + _; + } + + modifier allowRedemption() { + require(redemptionFlag, "Redemption N/A"); + _; + } +} diff --git a/contracts/fund/WstETHPrimaryMarketRouter.sol b/contracts/fund/WstETHPrimaryMarketRouter.sol new file mode 100644 index 00000000..021e61f8 --- /dev/null +++ b/contracts/fund/WstETHPrimaryMarketRouter.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; + +import "../interfaces/IPrimaryMarketV5.sol"; +import "../interfaces/IFundV3.sol"; +import "../interfaces/IWstETH.sol"; +import "../interfaces/ITrancheIndexV2.sol"; + +contract WstETHPrimaryMarketRouter is ITrancheIndexV2 { + using SafeERC20 for IERC20; + + IPrimaryMarketV5 public immutable primaryMarket; + IFundV3 public immutable fund; + address private immutable _wstETH; + address private immutable _stETH; + address private immutable _tokenB; + + constructor(address pm) public { + primaryMarket = IPrimaryMarketV5(pm); + IFundV3 fund_ = IFundV3(IPrimaryMarketV5(pm).fund()); + fund = fund_; + _wstETH = fund_.tokenUnderlying(); + _stETH = IWstETH(fund_.tokenUnderlying()).stETH(); + _tokenB = fund_.tokenB(); + } + + function create( + address recipient, + bool isWrapped, + uint256 underlying, + uint256 minOutQ, + uint256 version + ) public returns (uint256 outQ) { + if (isWrapped) { + IERC20(_stETH).safeTransferFrom(msg.sender, address(this), underlying); + underlying = IWstETH(_wstETH).wrap(underlying); + IERC20(_wstETH).safeTransfer(address(primaryMarket), underlying); + } else { + IERC20(_wstETH).safeTransferFrom(msg.sender, address(primaryMarket), underlying); + } + + outQ = primaryMarket.create(recipient, minOutQ, version); + } + + function createAndSplit( + uint256 underlying, + bool isWrapped, + uint256 minOutQ, + uint256 version + ) external { + // Create QUEEN + uint256 outQ = create(address(this), isWrapped, underlying, minOutQ, version); + + // Split QUEEN into BISHOP and ROOK + (uint256 outB, uint256 outR) = primaryMarket.split(address(this), outQ, version); + + fund.trancheTransfer(TRANCHE_B, msg.sender, outB, version); + fund.trancheTransfer(TRANCHE_R, msg.sender, outR, version); + } +} diff --git a/contracts/governance/FeeConverter.sol b/contracts/governance/FeeConverter.sol new file mode 100644 index 00000000..b949770b --- /dev/null +++ b/contracts/governance/FeeConverter.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +import "../interfaces/IFundV5.sol"; +import "../interfaces/IPrimaryMarketV5.sol"; +import "../interfaces/ITrancheIndexV2.sol"; + +contract FeeConverter is ITrancheIndexV2 { + IFundV5 public immutable fund; + IPrimaryMarketV5 public immutable primaryMarket; + address public immutable feeCollector; + + constructor(address primaryMarket_, address feeCollector_) public { + primaryMarket = IPrimaryMarketV5(primaryMarket_); + fund = IFundV5(IPrimaryMarketV5(primaryMarket_).fund()); + feeCollector = feeCollector_; + } + + function collectFee() external { + uint256 fee = fund.trancheBalanceOf(TRANCHE_Q, address(this)); + uint256 version = fund.getRebalanceSize(); + primaryMarket.redeem(feeCollector, fee, 0, version); + } +} diff --git a/contracts/interfaces/IFundV5.sol b/contracts/interfaces/IFundV5.sol new file mode 100644 index 00000000..77da7075 --- /dev/null +++ b/contracts/interfaces/IFundV5.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +import "./IFundV3.sol"; + +interface IFundV5 is IFundV3 { + function weightB() external view returns (uint256); + + function getSettledDay() external view returns (uint256); + + function getEquivalentTotalR() external view returns (uint256); + + function frozen() external view returns (bool); +} diff --git a/contracts/interfaces/IPrimaryMarketV5.sol b/contracts/interfaces/IPrimaryMarketV5.sol new file mode 100644 index 00000000..f59a7dac --- /dev/null +++ b/contracts/interfaces/IPrimaryMarketV5.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +interface IPrimaryMarketV5 { + function fund() external view returns (address); + + function getCreation(uint256 underlying) external view returns (uint256 outQ); + + function getRedemption(uint256 inQ) external view returns (uint256 underlying, uint256 fee); + + function getSplit(uint256 inQ) external view returns (uint256 outB, uint256 outQ); + + function getMerge(uint256 inB) external view returns (uint256 inR, uint256 outQ, uint256 feeQ); + + function canBeRemovedFromFund() external view returns (bool); + + function create( + address recipient, + uint256 minOutQ, + uint256 version + ) external returns (uint256 outQ); + + function redeem( + address recipient, + uint256 inQ, + uint256 minUnderlying, + uint256 version + ) external returns (uint256 underlying); + + function redeemAndUnwrap( + address recipient, + uint256 inQ, + uint256 minUnderlying, + uint256 version + ) external returns (uint256 underlying); + + function queueRedemption( + address recipient, + uint256 inQ, + uint256 minUnderlying, + uint256 version + ) external returns (uint256 underlying, uint256 index); + + function claimRedemptions( + address account, + uint256[] calldata indices + ) external returns (uint256 underlying); + + function claimRedemptionsAndUnwrap( + address account, + uint256[] calldata indices + ) external returns (uint256 underlying); + + function split( + address recipient, + uint256 inQ, + uint256 version + ) external returns (uint256 outB, uint256 outR); + + function merge(address recipient, uint256 inB, uint256 version) external returns (uint256 outQ); + + function settle(uint256 day) external; +} diff --git a/contracts/interfaces/IWstETH.sol b/contracts/interfaces/IWstETH.sol new file mode 100644 index 00000000..a93157a2 --- /dev/null +++ b/contracts/interfaces/IWstETH.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +interface IWstETH { + function stETH() external view returns (address); + + function stEthPerToken() external view returns (uint256); + + function getWstETHByStETH(uint256 _stETHAmount) external view returns (uint256); + + function getStETHByWstETH(uint256 _wstETHAmount) external view returns (uint256); + + function wrap(uint256 _stETHAmount) external returns (uint256); + + function unwrap(uint256 _wstETHAmount) external returns (uint256); +} diff --git a/contracts/oracle/ConstAprOracle.sol b/contracts/oracle/ConstAprOracle.sol new file mode 100644 index 00000000..98d506c2 --- /dev/null +++ b/contracts/oracle/ConstAprOracle.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "../interfaces/IAprOracle.sol"; + +contract ConstAprOracle is IAprOracle, Ownable { + event Updated(uint256 dailyRate); + + uint256 public dailyRate; + + constructor(uint256 dailyRate_) public { + dailyRate = dailyRate_; + emit Updated(dailyRate_); + } + + function update(uint256 newRate) external onlyOwner { + dailyRate = newRate; + emit Updated(newRate); + } + + function capture() external override returns (uint256) { + return dailyRate; + } +} diff --git a/contracts/oracle/WstETHPriceOracle.sol b/contracts/oracle/WstETHPriceOracle.sol new file mode 100644 index 00000000..6ddd0470 --- /dev/null +++ b/contracts/oracle/WstETHPriceOracle.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +import "../interfaces/ITwapOracleV2.sol"; +import "../interfaces/IWstETH.sol"; + +/// @title wstETH Price oracle +/// @author Tranchess +contract WstETHPriceOracle is ITwapOracleV2 { + IWstETH public immutable wstETH; + + constructor(address wstETH_) public { + wstETH = IWstETH(wstETH_); + } + + /// @notice Return the latest price with 18 decimal places. + function getLatest() external view override returns (uint256) { + return wstETH.stEthPerToken(); + } + + /// @notice Return the latest price with 18 decimal places. + function getTwap(uint256) external view override returns (uint256) { + return wstETH.stEthPerToken(); + } +} diff --git a/contracts/swap/StableSwapV3.sol b/contracts/swap/StableSwapV3.sol new file mode 100644 index 00000000..74e8a44e --- /dev/null +++ b/contracts/swap/StableSwapV3.sol @@ -0,0 +1,817 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +import "../interfaces/IStableSwap.sol"; +import "../interfaces/ILiquidityGauge.sol"; +import "../interfaces/ITranchessSwapCallee.sol"; +import "../interfaces/IWrappedERC20.sol"; + +import "../utils/SafeDecimalMath.sol"; +import "../utils/AdvancedMath.sol"; +import "../utils/ManagedPausable.sol"; + +abstract contract StableSwapV3 is IStableSwap, Ownable, ReentrancyGuard, ManagedPausable { + using SafeMath for uint256; + using SafeDecimalMath for uint256; + using SafeERC20 for IERC20; + + event LiquidityAdded( + address indexed sender, + address indexed recipient, + uint256 baseIn, + uint256 quoteIn, + uint256 lpOut, + uint256 fee, + uint256 adminFee, + uint256 oraclePrice + ); + event LiquidityRemoved( + address indexed account, + uint256 lpIn, + uint256 baseOut, + uint256 quotOut, + uint256 fee, + uint256 adminFee, + uint256 oraclePrice + ); + event Swap( + address indexed sender, + address indexed recipient, + uint256 baseIn, + uint256 quoteIn, + uint256 baseOut, + uint256 quoteOut, + uint256 fee, + uint256 adminFee, + uint256 oraclePrice + ); + event Sync(uint256 base, uint256 quote, uint256 oraclePrice); + event AmplRampUpdated(uint256 start, uint256 end, uint256 startTimestamp, uint256 endTimestamp); + event FeeCollectorUpdated(address newFeeCollector); + event FeeRateUpdated(uint256 newFeeRate); + event AdminFeeRateUpdated(uint256 newAdminFeeRate); + + /// @param base Amount of base tokens after rebalance + /// @param quote Amount of quote tokens after rebalance + /// @param rebalanceTimestamp Rebalance timestamp if the stored base and quote amount are rebalanced, or zero otherwise + struct RebalanceResult { + uint256 base; + uint256 quote; + uint256 rebalanceTimestamp; + } + + uint256 private constant AMPL_MAX_VALUE = 1e6; + uint256 private constant AMPL_RAMP_MIN_TIME = 86400; + uint256 private constant AMPL_RAMP_MAX_CHANGE = 10; + uint256 private constant MAX_FEE_RATE = 0.9e18; + uint256 private constant MAX_ADMIN_FEE_RATE = 1e18; + uint256 private constant MAX_ITERATION = 255; + uint256 private constant MINIMUM_LIQUIDITY = 1e3; + + address public immutable lpToken; + IFundV3 public immutable override fund; + uint256 public immutable override baseTranche; + address public immutable override quoteAddress; + + /// @dev A multipler that normalizes a quote asset balance to 18 decimal places. + uint256 internal immutable _quoteDecimalMultiplier; + + uint256 public baseBalance; + uint256 public quoteBalance; + + uint256 private _priceOverOracleIntegral; + uint256 private _priceOverOracleTimestamp; + + uint256 public amplRampStart; + uint256 public amplRampEnd; + uint256 public amplRampStartTimestamp; + uint256 public amplRampEndTimestamp; + + address public feeCollector; + uint256 public normalFeeRate; + uint256 public adminFeeRate; + uint256 public totalAdminFee; + + constructor( + address lpToken_, + address fund_, + uint256 baseTranche_, + address quoteAddress_, + uint256 quoteDecimals_, + uint256 ampl_, + address feeCollector_, + uint256 feeRate_, + uint256 adminFeeRate_ + ) public { + lpToken = lpToken_; + fund = IFundV3(fund_); + baseTranche = baseTranche_; + quoteAddress = quoteAddress_; + require(quoteDecimals_ <= 18, "Quote asset decimals larger than 18"); + _quoteDecimalMultiplier = 10 ** (18 - quoteDecimals_); + + require(ampl_ > 0 && ampl_ < AMPL_MAX_VALUE, "Invalid A"); + amplRampEnd = ampl_; + emit AmplRampUpdated(ampl_, ampl_, 0, 0); + + _updateFeeCollector(feeCollector_); + _updateFeeRate(feeRate_); + _updateAdminFeeRate(adminFeeRate_); + + _initializeManagedPausable(msg.sender); + } + + receive() external payable {} + + function baseAddress() external view override returns (address) { + return fund.tokenShare(baseTranche); + } + + function allBalances() external view override returns (uint256, uint256) { + (RebalanceResult memory result, , , , ) = _getRebalanceResult(fund.getRebalanceSize()); + return (result.base, result.quote); + } + + function getAmpl() public view returns (uint256) { + uint256 endTimestamp = amplRampEndTimestamp; + if (block.timestamp < endTimestamp) { + uint256 startTimestamp = amplRampStartTimestamp; + uint256 start = amplRampStart; + uint256 end = amplRampEnd; + if (end > start) { + return + start + + ((end - start) * (block.timestamp - startTimestamp)) / + (endTimestamp - startTimestamp); + } else { + return + start - + ((start - end) * (block.timestamp - startTimestamp)) / + (endTimestamp - startTimestamp); + } + } else { + return amplRampEnd; + } + } + + function feeRate() external view returns (uint256) { + uint256 version = fund.getRebalanceSize(); + uint256 rebalanceTimestamp = fund.getRebalanceTimestamp(version - 1); // underflow is desired + return _getFeeRate(rebalanceTimestamp); + } + + function _getFeeRate(uint256 rebalanceTimestamp) private view returns (uint256) { + if (rebalanceTimestamp <= block.timestamp - 1 days) { + return normalFeeRate; + } + uint256 coolingOff = rebalanceTimestamp > block.timestamp + ? 1 days + : rebalanceTimestamp - (block.timestamp - 1 days); + uint256 coolingFeeRate = (MAX_FEE_RATE * coolingOff) / 1 days; + return normalFeeRate < coolingFeeRate ? coolingFeeRate : normalFeeRate; + } + + function getCurrentD() external view override returns (uint256) { + (RebalanceResult memory result, , , , ) = _getRebalanceResult(fund.getRebalanceSize()); + return _getD(result.base, result.quote, getAmpl(), getOraclePrice()); + } + + function getCurrentPriceOverOracle() public view override returns (uint256) { + (RebalanceResult memory result, , , , ) = _getRebalanceResult(fund.getRebalanceSize()); + if (result.base == 0 || result.quote == 0) { + return 1e18; + } + uint256 ampl = getAmpl(); + uint256 oraclePrice = getOraclePrice(); + uint256 d = _getD(result.base, result.quote, ampl, oraclePrice); + return _getPriceOverOracle(result.base, result.quote, ampl, oraclePrice, d); + } + + /// @notice Get the current swap price, i.e. negative slope at the current point on the curve. + /// The returned value is computed after both base and quote balances are normalized to + /// 18 decimal places. If the quote token does not have 18 decimal places, the returned + /// value has a different order of magnitude than the ratio of quote amount to base + /// amount in a swap. + function getCurrentPrice() external view override returns (uint256) { + (RebalanceResult memory result, , , , ) = _getRebalanceResult(fund.getRebalanceSize()); + uint256 oraclePrice = getOraclePrice(); + if (result.base == 0 || result.quote == 0) { + return oraclePrice; + } + uint256 ampl = getAmpl(); + uint256 d = _getD(result.base, result.quote, ampl, oraclePrice); + return + _getPriceOverOracle(result.base, result.quote, ampl, oraclePrice, d).multiplyDecimal( + oraclePrice + ); + } + + function getPriceOverOracleIntegral() external view override returns (uint256) { + return + _priceOverOracleIntegral + + getCurrentPriceOverOracle() * + (block.timestamp - _priceOverOracleTimestamp); + } + + function getQuoteOut(uint256 baseIn) external view override returns (uint256 quoteOut) { + (RebalanceResult memory old, , , , ) = _getRebalanceResult(fund.getRebalanceSize()); + uint256 newBase = old.base.add(baseIn); + uint256 ampl = getAmpl(); + uint256 oraclePrice = getOraclePrice(); + // Add 1 in case of rounding errors + uint256 d = _getD(old.base, old.quote, ampl, oraclePrice) + 1; + uint256 newQuote = _getQuote(ampl, newBase, oraclePrice, d) + 1; + quoteOut = old.quote.sub(newQuote); + // Round down output after fee + quoteOut = quoteOut.multiplyDecimal(1e18 - _getFeeRate(old.rebalanceTimestamp)); + } + + function getQuoteIn(uint256 baseOut) external view override returns (uint256 quoteIn) { + (RebalanceResult memory old, , , , ) = _getRebalanceResult(fund.getRebalanceSize()); + uint256 newBase = old.base.sub(baseOut); + uint256 ampl = getAmpl(); + uint256 oraclePrice = getOraclePrice(); + // Add 1 in case of rounding errors + uint256 d = _getD(old.base, old.quote, ampl, oraclePrice) + 1; + uint256 newQuote = _getQuote(ampl, newBase, oraclePrice, d) + 1; + quoteIn = newQuote.sub(old.quote); + uint256 feeRate_ = _getFeeRate(old.rebalanceTimestamp); + // Round up input before fee + quoteIn = quoteIn.mul(1e18).add(1e18 - feeRate_ - 1) / (1e18 - feeRate_); + } + + function getBaseOut(uint256 quoteIn) external view override returns (uint256 baseOut) { + (RebalanceResult memory old, , , , ) = _getRebalanceResult(fund.getRebalanceSize()); + // Round down input after fee + uint256 quoteInAfterFee = quoteIn.multiplyDecimal( + 1e18 - _getFeeRate(old.rebalanceTimestamp) + ); + uint256 newQuote = old.quote.add(quoteInAfterFee); + uint256 ampl = getAmpl(); + uint256 oraclePrice = getOraclePrice(); + // Add 1 in case of rounding errors + uint256 d = _getD(old.base, old.quote, ampl, oraclePrice) + 1; + uint256 newBase = _getBase(ampl, newQuote, oraclePrice, d) + 1; + baseOut = old.base.sub(newBase); + } + + function getBaseIn(uint256 quoteOut) external view override returns (uint256 baseIn) { + (RebalanceResult memory old, , , , ) = _getRebalanceResult(fund.getRebalanceSize()); + uint256 feeRate_ = _getFeeRate(old.rebalanceTimestamp); + // Round up output before fee + uint256 quoteOutBeforeFee = quoteOut.mul(1e18).add(1e18 - feeRate_ - 1) / (1e18 - feeRate_); + uint256 newQuote = old.quote.sub(quoteOutBeforeFee); + uint256 ampl = getAmpl(); + uint256 oraclePrice = getOraclePrice(); + // Add 1 in case of rounding errors + uint256 d = _getD(old.base, old.quote, ampl, oraclePrice) + 1; + uint256 newBase = _getBase(ampl, newQuote, oraclePrice, d) + 1; + baseIn = newBase.sub(old.base); + } + + function buy( + uint256 version, + uint256 baseOut, + address recipient, + bytes calldata data + ) + external + override + nonReentrant + checkVersion(version) + whenNotPaused + returns (uint256 realBaseOut) + { + require(baseOut > 0, "Zero output"); + realBaseOut = baseOut; + RebalanceResult memory old = _handleRebalance(version); + require(baseOut < old.base, "Insufficient liquidity"); + // Optimistically transfer tokens. + fund.trancheTransfer(baseTranche, recipient, baseOut, version); + if (data.length > 0) { + ITranchessSwapCallee(msg.sender).tranchessSwapCallback(baseOut, 0, data); + _checkVersion(version); // Make sure no rebalance is triggered in the callback + } + uint256 newQuote = _getNewQuoteBalance(); + uint256 quoteIn = newQuote.sub(old.quote); + uint256 fee = quoteIn.multiplyDecimal(_getFeeRate(old.rebalanceTimestamp)); + uint256 oraclePrice = getOraclePrice(); + { + uint256 ampl = getAmpl(); + uint256 oldD = _getD(old.base, old.quote, ampl, oraclePrice); + _updatePriceOverOracleIntegral(old.base, old.quote, ampl, oraclePrice, oldD); + uint256 newD = _getD(old.base - baseOut, newQuote.sub(fee), ampl, oraclePrice); + require(newD >= oldD, "Invariant mismatch"); + } + uint256 adminFee = fee.multiplyDecimal(adminFeeRate); + baseBalance = old.base - baseOut; + quoteBalance = newQuote.sub(adminFee); + totalAdminFee = totalAdminFee.add(adminFee); + uint256 baseOut_ = baseOut; + emit Swap(msg.sender, recipient, 0, quoteIn, baseOut_, 0, fee, adminFee, oraclePrice); + } + + function sell( + uint256 version, + uint256 quoteOut, + address recipient, + bytes calldata data + ) + external + override + nonReentrant + checkVersion(version) + whenNotPaused + returns (uint256 realQuoteOut) + { + require(quoteOut > 0, "Zero output"); + realQuoteOut = quoteOut; + RebalanceResult memory old = _handleRebalance(version); + // Optimistically transfer tokens. + IERC20(quoteAddress).safeTransfer(recipient, quoteOut); + if (data.length > 0) { + ITranchessSwapCallee(msg.sender).tranchessSwapCallback(0, quoteOut, data); + _checkVersion(version); // Make sure no rebalance is triggered in the callback + } + uint256 newBase = fund.trancheBalanceOf(baseTranche, address(this)); + uint256 baseIn = newBase.sub(old.base); + uint256 fee; + { + uint256 feeRate_ = _getFeeRate(old.rebalanceTimestamp); + fee = quoteOut.mul(feeRate_).div(1e18 - feeRate_); + } + require(quoteOut.add(fee) < old.quote, "Insufficient liquidity"); + uint256 oraclePrice = getOraclePrice(); + { + uint256 newQuote = old.quote - quoteOut; + uint256 ampl = getAmpl(); + uint256 oldD = _getD(old.base, old.quote, ampl, oraclePrice); + _updatePriceOverOracleIntegral(old.base, old.quote, ampl, oraclePrice, oldD); + uint256 newD = _getD(newBase, newQuote - fee, ampl, oraclePrice); + require(newD >= oldD, "Invariant mismatch"); + } + uint256 adminFee = fee.multiplyDecimal(adminFeeRate); + baseBalance = newBase; + quoteBalance = old.quote - quoteOut - adminFee; + totalAdminFee = totalAdminFee.add(adminFee); + uint256 quoteOut_ = quoteOut; + emit Swap(msg.sender, recipient, baseIn, 0, 0, quoteOut_, fee, adminFee, oraclePrice); + } + + /// @notice Add liquidity. This function should be called by a smart contract, which transfers + /// base and quote tokens to this contract in the same transaction. + /// @param version The latest rebalance version + /// @param recipient Recipient of minted LP tokens + /// @param lpOut Amount of minted LP tokens + function addLiquidity( + uint256 version, + address recipient + ) external override nonReentrant checkVersion(version) whenNotPaused returns (uint256 lpOut) { + RebalanceResult memory old = _handleRebalance(version); + uint256 newBase = fund.trancheBalanceOf(baseTranche, address(this)); + uint256 newQuote = _getNewQuoteBalance(); + uint256 ampl = getAmpl(); + uint256 oraclePrice = getOraclePrice(); + uint256 lpSupply = IERC20(lpToken).totalSupply(); + if (lpSupply == 0) { + require(newBase > 0 && newQuote > 0, "Zero initial balance"); + baseBalance = newBase; + quoteBalance = newQuote; + // Overflow is desired + _priceOverOracleIntegral += 1e18 * (block.timestamp - _priceOverOracleTimestamp); + _priceOverOracleTimestamp = block.timestamp; + uint256 d1 = _getD(newBase, newQuote, ampl, oraclePrice); + ILiquidityGauge(lpToken).mint(address(this), MINIMUM_LIQUIDITY); + ILiquidityGauge(lpToken).mint(recipient, d1.sub(MINIMUM_LIQUIDITY)); + emit LiquidityAdded(msg.sender, recipient, newBase, newQuote, d1, 0, 0, oraclePrice); + return d1; + } + uint256 fee; + uint256 adminFee; + { + // Initial invariant + uint256 d0 = _getD(old.base, old.quote, ampl, oraclePrice); + _updatePriceOverOracleIntegral(old.base, old.quote, ampl, oraclePrice, d0); + { + // New invariant before charging fee + uint256 d1 = _getD(newBase, newQuote, ampl, oraclePrice); + uint256 idealQuote = d1.mul(old.quote) / d0; + uint256 difference = idealQuote > newQuote + ? idealQuote - newQuote + : newQuote - idealQuote; + fee = difference.multiplyDecimal(_getFeeRate(old.rebalanceTimestamp)); + } + adminFee = fee.multiplyDecimal(adminFeeRate); + totalAdminFee = totalAdminFee.add(adminFee); + baseBalance = newBase; + quoteBalance = newQuote.sub(adminFee); + // New invariant after charging fee + uint256 d2 = _getD(newBase, newQuote.sub(fee), ampl, oraclePrice); + require(d2 > d0, "No liquidity is added"); + lpOut = lpSupply.mul(d2.sub(d0)).div(d0); + } + ILiquidityGauge(lpToken).mint(recipient, lpOut); + emit LiquidityAdded( + msg.sender, + recipient, + newBase - old.base, + newQuote - old.quote, + lpOut, + fee, + adminFee, + oraclePrice + ); + } + + /// @dev Remove liquidity proportionally. + /// @param lpIn Exact amount of LP token to burn + /// @param minBaseOut Least amount of base asset to withdraw + /// @param minQuoteOut Least amount of quote asset to withdraw + function removeLiquidity( + uint256 version, + uint256 lpIn, + uint256 minBaseOut, + uint256 minQuoteOut + ) + external + override + nonReentrant + checkVersion(version) + returns (uint256 baseOut, uint256 quoteOut) + { + (baseOut, quoteOut) = _removeLiquidity(version, lpIn, minBaseOut, minQuoteOut); + IERC20(quoteAddress).safeTransfer(msg.sender, quoteOut); + } + + /// @dev Remove liquidity proportionally and unwrap for native token. + /// @param lpIn Exact amount of LP token to burn + /// @param minBaseOut Least amount of base asset to withdraw + /// @param minQuoteOut Least amount of quote asset to withdraw + function removeLiquidityUnwrap( + uint256 version, + uint256 lpIn, + uint256 minBaseOut, + uint256 minQuoteOut + ) + external + override + nonReentrant + checkVersion(version) + returns (uint256 baseOut, uint256 quoteOut) + { + (baseOut, quoteOut) = _removeLiquidity(version, lpIn, minBaseOut, minQuoteOut); + IWrappedERC20(quoteAddress).withdraw(quoteOut); + (bool success, ) = msg.sender.call{value: quoteOut}(""); + require(success, "Transfer failed"); + } + + function _removeLiquidity( + uint256 version, + uint256 lpIn, + uint256 minBaseOut, + uint256 minQuoteOut + ) private returns (uint256 baseOut, uint256 quoteOut) { + uint256 lpSupply = IERC20(lpToken).totalSupply(); + RebalanceResult memory old = _handleRebalance(version); + baseOut = old.base.mul(lpIn).div(lpSupply); + quoteOut = old.quote.mul(lpIn).div(lpSupply); + require(baseOut >= minBaseOut, "Insufficient output"); + require(quoteOut >= minQuoteOut, "Insufficient output"); + baseBalance = old.base.sub(baseOut); + quoteBalance = old.quote.sub(quoteOut); + ILiquidityGauge(lpToken).burnFrom(msg.sender, lpIn); + fund.trancheTransfer(baseTranche, msg.sender, baseOut, version); + emit LiquidityRemoved(msg.sender, lpIn, baseOut, quoteOut, 0, 0, 0); + } + + /// @dev Remove base liquidity only. + /// @param lpIn Exact amount of LP token to burn + /// @param minBaseOut Least amount of base asset to withdraw + function removeBaseLiquidity( + uint256 version, + uint256 lpIn, + uint256 minBaseOut + ) external override nonReentrant checkVersion(version) whenNotPaused returns (uint256 baseOut) { + RebalanceResult memory old = _handleRebalance(version); + uint256 lpSupply = IERC20(lpToken).totalSupply(); + uint256 ampl = getAmpl(); + uint256 oraclePrice = getOraclePrice(); + uint256 d1; + { + uint256 d0 = _getD(old.base, old.quote, ampl, oraclePrice); + _updatePriceOverOracleIntegral(old.base, old.quote, ampl, oraclePrice, d0); + d1 = d0.sub(d0.mul(lpIn).div(lpSupply)); + } + { + uint256 fee = old.quote.mul(lpIn).div(lpSupply).multiplyDecimal( + _getFeeRate(old.rebalanceTimestamp) + ); + // Add 1 in case of rounding errors + uint256 newBase = _getBase(ampl, old.quote.sub(fee), oraclePrice, d1) + 1; + baseOut = old.base.sub(newBase); + require(baseOut >= minBaseOut, "Insufficient output"); + ILiquidityGauge(lpToken).burnFrom(msg.sender, lpIn); + baseBalance = newBase; + uint256 adminFee = fee.multiplyDecimal(adminFeeRate); + totalAdminFee = totalAdminFee.add(adminFee); + quoteBalance = old.quote.sub(adminFee); + emit LiquidityRemoved(msg.sender, lpIn, baseOut, 0, fee, adminFee, oraclePrice); + } + fund.trancheTransfer(baseTranche, msg.sender, baseOut, version); + } + + /// @dev Remove quote liquidity only. + /// @param lpIn Exact amount of LP token to burn + /// @param minQuoteOut Least amount of quote asset to withdraw + function removeQuoteLiquidity( + uint256 version, + uint256 lpIn, + uint256 minQuoteOut + ) + external + override + nonReentrant + checkVersion(version) + whenNotPaused + returns (uint256 quoteOut) + { + quoteOut = _removeQuoteLiquidity(version, lpIn, minQuoteOut); + IERC20(quoteAddress).safeTransfer(msg.sender, quoteOut); + } + + /// @dev Remove quote liquidity only and unwrap for native token. + /// @param lpIn Exact amount of LP token to burn + /// @param minQuoteOut Least amount of quote asset to withdraw + function removeQuoteLiquidityUnwrap( + uint256 version, + uint256 lpIn, + uint256 minQuoteOut + ) + external + override + nonReentrant + checkVersion(version) + whenNotPaused + returns (uint256 quoteOut) + { + quoteOut = _removeQuoteLiquidity(version, lpIn, minQuoteOut); + IWrappedERC20(quoteAddress).withdraw(quoteOut); + (bool success, ) = msg.sender.call{value: quoteOut}(""); + require(success, "Transfer failed"); + } + + function _removeQuoteLiquidity( + uint256 version, + uint256 lpIn, + uint256 minQuoteOut + ) private returns (uint256 quoteOut) { + RebalanceResult memory old = _handleRebalance(version); + uint256 lpSupply = IERC20(lpToken).totalSupply(); + uint256 ampl = getAmpl(); + uint256 oraclePrice = getOraclePrice(); + uint256 d1; + { + uint256 d0 = _getD(old.base, old.quote, ampl, oraclePrice); + _updatePriceOverOracleIntegral(old.base, old.quote, ampl, oraclePrice, d0); + d1 = d0.sub(d0.mul(lpIn).div(lpSupply)); + } + uint256 idealQuote = old.quote.mul(lpSupply.sub(lpIn)).div(lpSupply); + // Add 1 in case of rounding errors + uint256 newQuote = _getQuote(ampl, old.base, oraclePrice, d1) + 1; + uint256 fee = idealQuote.sub(newQuote).multiplyDecimal(_getFeeRate(old.rebalanceTimestamp)); + quoteOut = old.quote.sub(newQuote).sub(fee); + require(quoteOut >= minQuoteOut, "Insufficient output"); + ILiquidityGauge(lpToken).burnFrom(msg.sender, lpIn); + uint256 adminFee = fee.multiplyDecimal(adminFeeRate); + totalAdminFee = totalAdminFee.add(adminFee); + quoteBalance = newQuote.add(fee).sub(adminFee); + emit LiquidityRemoved(msg.sender, lpIn, 0, quoteOut, fee, adminFee, oraclePrice); + } + + /// @notice Force stored values to match balances. + function sync() external nonReentrant { + RebalanceResult memory old = _handleRebalance(fund.getRebalanceSize()); + uint256 ampl = getAmpl(); + uint256 oraclePrice = getOraclePrice(); + uint256 d = _getD(old.base, old.quote, ampl, oraclePrice); + _updatePriceOverOracleIntegral(old.base, old.quote, ampl, oraclePrice, d); + uint256 newBase = fund.trancheBalanceOf(baseTranche, address(this)); + uint256 newQuote = _getNewQuoteBalance(); + baseBalance = newBase; + quoteBalance = newQuote; + emit Sync(newBase, newQuote, oraclePrice); + } + + function collectFee() external { + uint256 totalAdminFee_ = totalAdminFee; + delete totalAdminFee; + IERC20(quoteAddress).safeTransfer(feeCollector, totalAdminFee_); + } + + function _getNewQuoteBalance() private view returns (uint256) { + return IERC20(quoteAddress).balanceOf(address(this)).sub(totalAdminFee); + } + + function _updatePriceOverOracleIntegral( + uint256 base, + uint256 quote, + uint256 ampl, + uint256 oraclePrice, + uint256 d + ) private { + // Overflow is desired + _priceOverOracleIntegral += + _getPriceOverOracle(base, quote, ampl, oraclePrice, d) * + (block.timestamp - _priceOverOracleTimestamp); + _priceOverOracleTimestamp = block.timestamp; + } + + function _getD( + uint256 base, + uint256 quote, + uint256 ampl, + uint256 oraclePrice + ) private view returns (uint256) { + // Newtonian: D' = (4A(kx + y) + D^3 / 2kxy)D / ((4A - 1)D + 3D^3 / 4kxy) + uint256 normalizedQuote = quote.mul(_quoteDecimalMultiplier); + uint256 baseValue = base.multiplyDecimal(oraclePrice); + uint256 sum = baseValue.add(normalizedQuote); + if (sum == 0) return 0; + + uint256 prev = 0; + uint256 d = sum; + for (uint256 i = 0; i < MAX_ITERATION; i++) { + prev = d; + uint256 d3 = d.mul(d).div(baseValue).mul(d) / normalizedQuote / 4; + d = (sum.mul(4 * ampl) + 2 * d3).mul(d) / d.mul(4 * ampl - 1).add(3 * d3); + if (d <= prev + 1 && prev <= d + 1) { + break; + } + } + return d; + } + + function _getPriceOverOracle( + uint256 base, + uint256 quote, + uint256 ampl, + uint256 oraclePrice, + uint256 d + ) private view returns (uint256) { + uint256 commonExp = d.multiplyDecimal(4e18 - 1e18 / ampl); + uint256 baseValue = base.multiplyDecimal(oraclePrice); + uint256 normalizedQuote = quote.mul(_quoteDecimalMultiplier); + return + (baseValue.mul(8).add(normalizedQuote.mul(4)).sub(commonExp)) + .multiplyDecimal(normalizedQuote) + .divideDecimal(normalizedQuote.mul(8).add(baseValue.mul(4)).sub(commonExp)) + .divideDecimal(baseValue); + } + + function _getBase( + uint256 ampl, + uint256 quote, + uint256 oraclePrice, + uint256 d + ) private view returns (uint256 base) { + // Solve 16Ayk^2·x^2 + 4ky(4Ay - 4AD + D)·x - D^3 = 0 + // Newtonian: kx' = ((kx)^2 + D^3 / 16Ay) / (2kx + y - D + D/4A) + uint256 normalizedQuote = quote.mul(_quoteDecimalMultiplier); + uint256 d3 = d.mul(d).div(normalizedQuote).mul(d) / (16 * ampl); + uint256 prev = 0; + uint256 baseValue = d; + for (uint256 i = 0; i < MAX_ITERATION; i++) { + prev = baseValue; + baseValue = + baseValue.mul(baseValue).add(d3) / + (2 * baseValue).add(normalizedQuote).add(d / (4 * ampl)).sub(d); + if (baseValue <= prev + 1 && prev <= baseValue + 1) { + break; + } + } + base = baseValue.divideDecimal(oraclePrice); + } + + function _getQuote( + uint256 ampl, + uint256 base, + uint256 oraclePrice, + uint256 d + ) private view returns (uint256 quote) { + // Solve 16Axk·y^2 + 4kx(4Akx - 4AD + D)·y - D^3 = 0 + // Newtonian: y' = (y^2 + D^3 / 16Akx) / (2y + kx - D + D/4A) + uint256 baseValue = base.multiplyDecimal(oraclePrice); + uint256 d3 = d.mul(d).div(baseValue).mul(d) / (16 * ampl); + uint256 prev = 0; + uint256 normalizedQuote = d; + for (uint256 i = 0; i < MAX_ITERATION; i++) { + prev = normalizedQuote; + normalizedQuote = + normalizedQuote.mul(normalizedQuote).add(d3) / + (2 * normalizedQuote).add(baseValue).add(d / (4 * ampl)).sub(d); + if (normalizedQuote <= prev + 1 && prev <= normalizedQuote + 1) { + break; + } + } + quote = normalizedQuote / _quoteDecimalMultiplier; + } + + function updateAmplRamp(uint256 endAmpl, uint256 endTimestamp) external onlyOwner { + require(endAmpl > 0 && endAmpl < AMPL_MAX_VALUE, "Invalid A"); + require(endTimestamp >= block.timestamp + AMPL_RAMP_MIN_TIME, "A ramp time too short"); + uint256 ampl = getAmpl(); + require( + (endAmpl >= ampl && endAmpl <= ampl * AMPL_RAMP_MAX_CHANGE) || + (endAmpl < ampl && endAmpl * AMPL_RAMP_MAX_CHANGE >= ampl), + "A ramp change too large" + ); + amplRampStart = ampl; + amplRampEnd = endAmpl; + amplRampStartTimestamp = block.timestamp; + amplRampEndTimestamp = endTimestamp; + emit AmplRampUpdated(ampl, endAmpl, block.timestamp, endTimestamp); + } + + function _updateFeeCollector(address newFeeCollector) private { + feeCollector = newFeeCollector; + emit FeeCollectorUpdated(newFeeCollector); + } + + function updateFeeCollector(address newFeeCollector) external onlyOwner { + _updateFeeCollector(newFeeCollector); + } + + function _updateFeeRate(uint256 newFeeRate) private { + require(newFeeRate <= MAX_FEE_RATE, "Exceed max fee rate"); + normalFeeRate = newFeeRate; + emit FeeRateUpdated(newFeeRate); + } + + function updateFeeRate(uint256 newFeeRate) external onlyOwner { + _updateFeeRate(newFeeRate); + } + + function _updateAdminFeeRate(uint256 newAdminFeeRate) private { + require(newAdminFeeRate <= MAX_ADMIN_FEE_RATE, "Exceed max admin fee rate"); + adminFeeRate = newAdminFeeRate; + emit AdminFeeRateUpdated(newAdminFeeRate); + } + + function updateAdminFeeRate(uint256 newAdminFeeRate) external onlyOwner { + _updateAdminFeeRate(newAdminFeeRate); + } + + /// @dev Check if the user-specified version is correct. + modifier checkVersion(uint256 version) { + _checkVersion(version); + _; + } + + /// @dev Revert if the user-specified version is not correct. + function _checkVersion(uint256 version) internal view virtual {} + + /// @dev Compute the new base and quote amount after rebalanced to the latest version. + /// If any tokens should be distributed to LP holders, their amounts are also returned. + /// + /// The latest rebalance version is passed in a parameter and it is caller's responsibility + /// to pass the correct version. + /// @param latestVersion The latest rebalance version + /// @return result Amount of stored base and quote tokens after rebalance + /// @return excessiveQ Amount of QUEEN that should be distributed to LP holders due to rebalance + /// @return excessiveB Amount of BISHOP that should be distributed to LP holders due to rebalance + /// @return excessiveR Amount of ROOK that should be distributed to LP holders due to rebalance + /// @return excessiveQuote Amount of quote tokens that should be distributed to LP holders due to rebalance + function _getRebalanceResult( + uint256 latestVersion + ) + internal + view + virtual + returns ( + RebalanceResult memory result, + uint256 excessiveQ, + uint256 excessiveB, + uint256 excessiveR, + uint256 excessiveQuote + ); + + /// @dev Update the stored base and quote balance to the latest rebalance version and distribute + /// any excessive tokens to LP holders. + /// + /// The latest rebalance version is passed in a parameter and it is caller's responsibility + /// to pass the correct version. + /// @param latestVersion The latest rebalance version + /// @return result Amount of stored base and quote tokens after rebalance + function _handleRebalance( + uint256 latestVersion + ) internal virtual returns (RebalanceResult memory result); + + /// @notice Get the base token price from the price oracle. The returned price is normalized + /// to 18 decimal places. + function getOraclePrice() public view virtual override returns (uint256); +} diff --git a/contracts/swap/SwapRouter.sol b/contracts/swap/SwapRouter.sol index 18fc8591..3cd8f35c 100644 --- a/contracts/swap/SwapRouter.sol +++ b/contracts/swap/SwapRouter.sol @@ -8,6 +8,7 @@ import "../interfaces/ISwapRouter.sol"; import "../interfaces/ITrancheIndexV2.sol"; import "../fund/ShareStaking.sol"; import "../interfaces/IWrappedERC20.sol"; +import "../interfaces/IWstETH.sol"; /// @title Tranchess Swap Router /// @notice Router for stateless execution of swaps against Tranchess stable swaps @@ -15,6 +16,14 @@ contract SwapRouter is ISwapRouter, ITrancheIndexV2, Ownable { using SafeMath for uint256; using SafeERC20 for IERC20; + address public immutable wstETH; + address public immutable stETH; + + constructor(address wstETH_) public { + wstETH = wstETH_; + stETH = IWstETH(wstETH_).stETH(); + } + event SwapAdded(address addr0, address addr1, address swap); mapping(address => mapping(address => IStableSwap)) private _swapMap; @@ -55,6 +64,9 @@ contract SwapRouter is ISwapRouter, ITrancheIndexV2, Ownable { uint256 deadline ) external payable override checkDeadline(deadline) { IStableSwap swap = getSwap(baseAddress, quoteAddress); + if (quoteAddress == stETH) { + swap = getSwap(baseAddress, wstETH); + } require(address(swap) != address(0), "Unknown swap"); swap.fund().trancheTransferFrom( @@ -68,6 +80,10 @@ contract SwapRouter is ISwapRouter, ITrancheIndexV2, Ownable { require(msg.value == quoteIn); // sanity check IWrappedERC20(quoteAddress).deposit{value: quoteIn}(); IERC20(quoteAddress).safeTransfer(address(swap), quoteIn); + } else if (quoteAddress == stETH) { + IERC20(stETH).safeTransferFrom(msg.sender, address(this), quoteIn); + quoteIn = IWstETH(wstETH).wrap(quoteIn); + IERC20(wstETH).safeTransfer(address(swap), quoteIn); } else { IERC20(quoteAddress).safeTransferFrom(msg.sender, address(swap), quoteIn); } diff --git a/contracts/swap/WstETHBishopStableSwap.sol b/contracts/swap/WstETHBishopStableSwap.sol new file mode 100644 index 00000000..4bc97359 --- /dev/null +++ b/contracts/swap/WstETHBishopStableSwap.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +import "./WstETHStableSwap.sol"; + +abstract contract WstETHBishopStableSwap is WstETHStableSwap { + event Rebalanced(uint256 base, uint256 quote, uint256 version); + + constructor( + address lpToken_, + address fund_, + address quoteAddress_, + uint256 quoteDecimals_, + uint256 ampl_, + address feeCollector_, + uint256 feeRate_, + uint256 adminFeeRate_ + ) + public + WstETHStableSwap( + lpToken_, + fund_, + TRANCHE_B, + quoteAddress_, + quoteDecimals_, + ampl_, + feeCollector_, + feeRate_, + adminFeeRate_ + ) + {} + + function _rebalanceBase( + uint256 oldBase, + uint256 fromVersion, + uint256 toVersion + ) internal view override returns (uint256 excessiveQ, uint256 newBase) { + (excessiveQ, newBase, ) = fund.batchRebalance(0, oldBase, 0, fromVersion, toVersion); + } + + function _getBaseNav() internal view override returns (uint256) { + uint256 price = fund.twapOracle().getLatest(); + (, uint256 navB, ) = fund.extrapolateNav(price); + return navB; + } +} diff --git a/contracts/swap/WstETHRookStableSwap.sol b/contracts/swap/WstETHRookStableSwap.sol new file mode 100644 index 00000000..4f053ffd --- /dev/null +++ b/contracts/swap/WstETHRookStableSwap.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +import "./WstETHStableSwap.sol"; + +abstract contract WstETHRookStableSwap is WstETHStableSwap { + event Rebalanced(uint256 base, uint256 quote, uint256 version); + + constructor( + address lpToken_, + address fund_, + address quoteAddress_, + uint256 quoteDecimals_, + uint256 ampl_, + address feeCollector_, + uint256 feeRate_, + uint256 adminFeeRate_ + ) + public + WstETHStableSwap( + lpToken_, + fund_, + TRANCHE_R, + quoteAddress_, + quoteDecimals_, + ampl_, + feeCollector_, + feeRate_, + adminFeeRate_ + ) + {} + + function _rebalanceBase( + uint256 oldBase, + uint256 fromVersion, + uint256 toVersion + ) internal view override returns (uint256 excessiveQ, uint256 newBase) { + (excessiveQ, , newBase) = fund.batchRebalance(0, 0, oldBase, fromVersion, toVersion); + } + + function _getBaseNav() internal view override returns (uint256) { + uint256 price = fund.twapOracle().getLatest(); + (, , uint256 navR) = fund.extrapolateNav(price); + return navR; + } +} diff --git a/contracts/swap/WstETHStableSwap.sol b/contracts/swap/WstETHStableSwap.sol new file mode 100644 index 00000000..5deda7ec --- /dev/null +++ b/contracts/swap/WstETHStableSwap.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +import "../interfaces/ITrancheIndexV2.sol"; +import "../interfaces/IWstETH.sol"; +import "./StableSwapV3.sol"; + +abstract contract WstETHStableSwap is StableSwapV3, ITrancheIndexV2 { + event Rebalanced(uint256 base, uint256 quote, uint256 version); + + address public immutable wstETH; + + uint256 public currentVersion; + + constructor( + address lpToken_, + address fund_, + uint256 baseTranche_, + address quoteAddress_, + uint256 quoteDecimals_, + uint256 ampl_, + address feeCollector_, + uint256 feeRate_, + uint256 adminFeeRate_ + ) + public + StableSwapV3( + lpToken_, + fund_, + baseTranche_, + quoteAddress_, + quoteDecimals_, + ampl_, + feeCollector_, + feeRate_, + adminFeeRate_ + ) + { + require(quoteAddress_ == IFundV3(fund_).tokenUnderlying()); + wstETH = quoteAddress_; + currentVersion = IFundV3(fund_).getRebalanceSize(); + } + + /// @dev Make sure the user-specified version is the latest rebalance version. + function _checkVersion(uint256 version) internal view override { + require(version == fund.getRebalanceSize(), "Obsolete rebalance version"); + } + + function _getRebalanceResult( + uint256 latestVersion + ) + internal + view + override + returns (RebalanceResult memory result, uint256 excessiveQ, uint256, uint256, uint256) + { + result.quote = quoteBalance; + if (latestVersion == currentVersion) { + result.base = baseBalance; + return (result, 0, 0, 0, 0); + } + result.rebalanceTimestamp = fund.getRebalanceTimestamp(latestVersion - 1); // underflow is desired + (excessiveQ, result.base, ) = fund.batchRebalance( + 0, + baseBalance, + 0, + currentVersion, + latestVersion + ); + } + + function _handleRebalance( + uint256 latestVersion + ) internal override returns (RebalanceResult memory result) { + uint256 excessiveQ; + (result, excessiveQ, , , ) = _getRebalanceResult(latestVersion); + if (result.rebalanceTimestamp != 0) { + baseBalance = result.base; + quoteBalance = result.quote; + currentVersion = latestVersion; + emit Rebalanced(result.base, result.quote, latestVersion); + if (excessiveQ > 0) { + fund.trancheTransfer(TRANCHE_Q, lpToken, excessiveQ, latestVersion); + } + ILiquidityGauge(lpToken).distribute(excessiveQ, 0, 0, 0, latestVersion); + } + } + + function getOraclePrice() public view override returns (uint256) { + uint256 price = fund.twapOracle().getLatest(); + (, uint256 navB, ) = fund.extrapolateNav(price); + return navB.divideDecimal(IWstETH(wstETH).stEthPerToken()); + } + + function _rebalanceBase( + uint256 oldBase, + uint256 fromVersion, + uint256 toVersion + ) internal view virtual returns (uint256 excessiveQ, uint256 newBase); + + function _getBaseNav() internal view virtual returns (uint256); +} diff --git a/contracts/swap/WstETHWrappingSwap.sol b/contracts/swap/WstETHWrappingSwap.sol new file mode 100644 index 00000000..aadcfd4f --- /dev/null +++ b/contracts/swap/WstETHWrappingSwap.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "../interfaces/IStableSwap.sol"; +import "../interfaces/IWstETH.sol"; +import "../utils/SafeDecimalMath.sol"; + +contract WstETHWrappingSwap is IStableSwap { + using SafeERC20 for IERC20; + using SafeDecimalMath for uint256; + + address public immutable wstETH; // Base + address public immutable stETH; // Quote + + constructor(address wstETH_) public { + wstETH = wstETH_; + stETH = IWstETH(wstETH_).stETH(); + } + + function getQuoteOut(uint256 baseIn) external view override returns (uint256 quoteOut) { + quoteOut = IWstETH(wstETH).getStETHByWstETH(baseIn); + } + + function getQuoteIn(uint256 baseOut) external view override returns (uint256 quoteIn) { + quoteIn = IWstETH(wstETH).getStETHByWstETH(baseOut); + } + + function getBaseOut(uint256 quoteIn) external view override returns (uint256 baseOut) { + baseOut = IWstETH(wstETH).getWstETHByStETH(quoteIn); + } + + function getBaseIn(uint256 quoteOut) external view override returns (uint256 baseIn) { + baseIn = IWstETH(wstETH).getWstETHByStETH(quoteOut); + } + + function buy( + uint256, + uint256, + address recipient, + bytes calldata + ) external override returns (uint256 realBaseOut) { + uint256 quoteIn = IERC20(stETH).balanceOf(address(this)); + realBaseOut = IWstETH(wstETH).wrap(quoteIn); + IERC20(wstETH).safeTransfer(recipient, realBaseOut); + } + + function sell( + uint256, + uint256, + address recipient, + bytes calldata + ) external override returns (uint256 realQuoteOut) { + uint256 baseIn = IERC20(wstETH).balanceOf(address(this)); + realQuoteOut = IWstETH(wstETH).unwrap(baseIn); + IERC20(stETH).safeTransfer(recipient, realQuoteOut); + } + + function baseAddress() external view override returns (address) { + return wstETH; + } + + function quoteAddress() external view override returns (address) { + return stETH; + } + + function getOraclePrice() external view override returns (uint256) { + return IWstETH(wstETH).stEthPerToken(); + } + + function getCurrentPrice() external view override returns (uint256) { + return IWstETH(wstETH).stEthPerToken(); + } + + function fund() external view override returns (IFundV3) { + revert("Not implemented"); + } + + function baseTranche() external view override returns (uint256) { + revert("Not implemented"); + } + + function allBalances() external view override returns (uint256, uint256) { + revert("Not implemented"); + } + + function getCurrentD() external view override returns (uint256) { + revert("Not implemented"); + } + + function getCurrentPriceOverOracle() external view override returns (uint256) { + revert("Not implemented"); + } + + function getPriceOverOracleIntegral() external view override returns (uint256) { + revert("Not implemented"); + } + + function addLiquidity(uint256, address) external override returns (uint256) { + revert("Not implemented"); + } + + function removeLiquidity( + uint256, + uint256, + uint256, + uint256 + ) external override returns (uint256, uint256) { + revert("Not implemented"); + } + + function removeLiquidityUnwrap( + uint256, + uint256, + uint256, + uint256 + ) external override returns (uint256, uint256) { + revert("Not implemented"); + } + + function removeBaseLiquidity(uint256, uint256, uint256) external override returns (uint256) { + revert("Not implemented"); + } + + function removeQuoteLiquidity(uint256, uint256, uint256) external override returns (uint256) { + revert("Not implemented"); + } + + function removeQuoteLiquidityUnwrap( + uint256, + uint256, + uint256 + ) external override returns (uint256) { + revert("Not implemented"); + } +}