From ce9007ef5327a3054089d23b53f516287e3f4578 Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Sat, 18 Nov 2023 03:12:06 +0800 Subject: [PATCH 01/29] initial commit v5 --- contracts/fund/FundV5.sol | 1152 ++++++++++++++++++++++++++++ contracts/fund/PrimaryMarketV5.sol | 594 ++++++++++++++ 2 files changed, 1746 insertions(+) create mode 100644 contracts/fund/FundV5.sol create mode 100644 contracts/fund/PrimaryMarketV5.sol diff --git a/contracts/fund/FundV5.sol b/contracts/fund/FundV5.sol new file mode 100644 index 00000000..2e178479 --- /dev/null +++ b/contracts/fund/FundV5.sol @@ -0,0 +1,1152 @@ +// 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/IFundV4.sol"; +import "../interfaces/IFundForPrimaryMarketV4.sol"; +import "../interfaces/IFundForStrategyV2.sol"; +import "../interfaces/IShareV2.sol"; +import "../interfaces/ITwapOracleV2.sol"; +import "../interfaces/IAprOracle.sol"; +import "../interfaces/IBallot.sol"; +import "../interfaces/IVotingEscrow.sol"; + +import "./FundRolesV2.sol"; + +contract FundV4 is + IFundV4, + 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 DailyProtocolFeeRateUpdated(uint256 newDailyProtocolFeeRate); + event TwapOracleUpdated(address newTwapOracle); + event AprOracleUpdated(address newAprOracle); + event BallotUpdated(address newBallot); + event FeeCollectorUpdated(address newFeeCollector); + event ActivityDelayTimeUpdated(uint256 delayTime); + event SplitRatioUpdated(uint256 newSplitRatio); + event TotalDebtUpdated(uint256 newTotalDebt); + + uint256 private constant UNIT = 1e18; + uint256 private constant MAX_INTEREST_RATE = 0.2e18; // 20% daily + uint256 private constant MAX_DAILY_PROTOCOL_FEE_RATE = 0.05e18; // 5% daily rate + + /// @notice Upper bound of `NAV_R / NAV_B` to trigger a rebalance. + uint256 public immutable upperRebalanceThreshold; + + /// @notice Lower bound of `NAV_R / NAV_B` to trigger a rebalance. + uint256 public immutable lowerRebalanceThreshold; + + /// @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 Daily protocol fee rate. + uint256 public dailyProtocolFeeRate; + + /// @notice TwapOracle address for the underlying asset. + ITwapOracleV2 public override twapOracle; + + /// @notice AprOracle address. + IAprOracle public aprOracle; + + /// @notice Address of the interest rate ballot. + IBallot public ballot; + + /// @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; + + /// @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 in the following trading day. + 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 { + address tokenUnderlying; + uint256 underlyingDecimals; + address tokenQ; + address tokenB; + address tokenR; + address primaryMarket; + address strategy; + uint256 dailyProtocolFeeRate; + uint256 upperRebalanceThreshold; + uint256 lowerRebalanceThreshold; + address twapOracle; + address aprOracle; + address ballot; + address feeCollector; + } + + constructor( + ConstructorParameters memory params + ) + public + Ownable() + FundRolesV2( + params.tokenQ, + params.tokenB, + params.tokenR, + params.primaryMarket, + params.strategy + ) + { + tokenUnderlying = params.tokenUnderlying; + require(params.underlyingDecimals <= 18, "Underlying decimals larger than 18"); + underlyingDecimalMultiplier = 10 ** (18 - params.underlyingDecimals); + _updateDailyProtocolFeeRate(params.dailyProtocolFeeRate); + upperRebalanceThreshold = params.upperRebalanceThreshold; + lowerRebalanceThreshold = params.lowerRebalanceThreshold; + _updateTwapOracle(params.twapOracle); + _updateAprOracle(params.aprOracle); + _updateBallot(params.ballot); + _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 && !_shouldTriggerRebalance(lastNavB, lastNavR), + "Invalid parameters" + ); + currentDay = endOfDay(block.timestamp); + splitRatio = newSplitRatio; + _historicalSplitRatio[0] = newSplitRatio; + emit SplitRatioUpdated(newSplitRatio); + uint256 lastDay = currentDay - 1 days; + 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 BISHOP supply, as if all QUEEN are split. + function getEquivalentTotalB() public view override returns (uint256) { + return _totalSupplies[TRANCHE_Q].multiplyDecimal(splitRatio).add(_totalSupplies[TRANCHE_B]); + } + + /// @notice Equivalent QUEEN supply, as if all BISHOP and ROOK are merged. + function getEquivalentTotalQ() public view override returns (uint256) { + return _totalSupplies[TRANCHE_B].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; + } + + /// @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, + /// accrued protocol fee and accrued interest since the previous settlement. + /// + /// The extrapolation uses simple interest instead of daily compound interest in + /// calculating protocol fee and 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 the estimated NAV of BISHOP and 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 = currentDay - 1 days; + uint256 underlying = getTotalUnderlying(); + uint256 protocolFee = underlying + .multiplyDecimal(dailyProtocolFeeRate) + .mul(block.timestamp - settledDay) + .div(1 days); + underlying = underlying.sub(protocolFee); + return + _extrapolateNav(block.timestamp, settledDay, price, getEquivalentTotalB(), underlying); + } + + function _extrapolateNav( + uint256 timestamp, + uint256 settledDay, + uint256 price, + uint256 equivalentTotalB, + uint256 underlying + ) private view returns (uint256 navSum, uint256 navB, uint256 navROrZero) { + navB = _historicalNavB[settledDay]; + if (equivalentTotalB > 0) { + navSum = price.mul(underlying.mul(underlyingDecimalMultiplier)).div(equivalentTotalB); + navB = navB.multiplyDecimal( + historicalInterestRate[settledDay].mul(timestamp - settledDay).div(1 days).add(UNIT) + ); + navROrZero = navSum >= navB ? navSum - navB : 0; + } else { + // If the fund is empty, use NAV in the last day + navROrZero = _historicalNavR[settledDay]; + navSum = navB + navROrZero; + } + } + + /// @notice Return the fund's relative income in a trading day. Note that denominators + /// of the returned ratios are the latest value instead of that at the last settlement. + /// If the amount of underlying token increases from 100 to 110 and assume that there's + /// no creation/redemption or underlying price change, return value `incomeOverQ` will + /// be 1/11 rather than 1/10. + /// @param day End timestamp of a trading day + /// @return incomeOverQ The ratio of income to the fund's total value + /// @return incomeOverB The ratio of income to equivalent BISHOP total value if all QUEEN are split + function getRelativeIncome( + uint256 day + ) external view override returns (uint256 incomeOverQ, uint256 incomeOverB) { + uint256 navB = _historicalNavB[day]; + if (navB == 0) { + return (0, 0); + } + uint256 navR = _historicalNavR[day]; + if (navB == UNIT && navR == UNIT) { + return (0, 0); // Rebalance is triggered + } + uint256 lastUnderlying = historicalUnderlying[day - 1 days]; + uint256 lastEquivalentTotalB = historicalEquivalentTotalB[day - 1 days]; + if (lastUnderlying == 0 || lastEquivalentTotalB == 0) { + return (0, 0); + } + uint256 currentUnderlying = historicalUnderlying[day]; + uint256 currentEquivalentTotalB = historicalEquivalentTotalB[day]; + if (currentUnderlying == 0 || currentEquivalentTotalB == 0) { + return (0, 0); + } + { + uint256 ratio = ((lastUnderlying * currentEquivalentTotalB) / currentUnderlying) + .divideDecimal(lastEquivalentTotalB); + incomeOverQ = ratio > 1e18 ? 0 : 1e18 - ratio; + } + incomeOverB = incomeOverQ.mul(navB + navR) / navB; + } + + /// @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. Charge protocol fee of the day. + /// 2. Settle all pending creations and redemptions from the primary market. + /// 3. Calculate NAV of the day and trigger rebalance if necessary. + /// 4. Capture new interest rate for BISHOP. + function settle() external nonReentrant { + uint256 day = currentDay; + require(day != 0, "Not initialized"); + require(block.timestamp >= day, "The current trading day does not end yet"); + uint256 price = twapOracle.getTwap(day); + require(price != 0, "Underlying price for settlement is not ready yet"); + + _collectFee(); + + IPrimaryMarketV3(_primaryMarket).settle(day); + + // Calculate NAV + uint256 equivalentTotalB = getEquivalentTotalB(); + uint256 underlying = getTotalUnderlying(); + (uint256 navSum, uint256 navB, uint256 navR) = _extrapolateNav( + day, + day - 1 days, + price, + equivalentTotalB, + underlying + ); + + if (_shouldTriggerRebalance(navB, navR)) { + uint256 newSplitRatio = splitRatio.multiplyDecimal(navSum) / 2; + _triggerRebalance(day, navSum, navB, navR, newSplitRatio); + navB = UNIT; + navR = UNIT; + equivalentTotalB = getEquivalentTotalB(); + fundActivityStartTime = day + activityDelayTimeAfterRebalance; + } else { + fundActivityStartTime = day; + } + + historicalEquivalentTotalB[day] = equivalentTotalB; + historicalUnderlying[day] = underlying; + _historicalNavB[day] = navB; + _historicalNavR[day] = navR; + uint256 interestRate = _updateInterestRate(day); + historicalInterestRate[day] = interestRate; + currentDay = day + 1 days; + + 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 _updateDailyProtocolFeeRate(uint256 newDailyProtocolFeeRate) private { + require( + newDailyProtocolFeeRate <= MAX_DAILY_PROTOCOL_FEE_RATE, + "Exceed max protocol fee rate" + ); + dailyProtocolFeeRate = newDailyProtocolFeeRate; + emit DailyProtocolFeeRateUpdated(newDailyProtocolFeeRate); + } + + function updateDailyProtocolFeeRate(uint256 newDailyProtocolFeeRate) external onlyOwner { + _updateDailyProtocolFeeRate(newDailyProtocolFeeRate); + } + + 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 _updateBallot(address newBallot) private { + ballot = IBallot(newBallot); + emit BallotUpdated(newBallot); + } + + function updateBallot(address newBallot) external onlyOwner { + _updateBallot(newBallot); + } + + 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 Collect protocol fee by minting QUEEN tokens to the fee collector. + function _collectFee() private { + uint256 feeRate = dailyProtocolFeeRate; + if (feeRate == 0) { + return; + } + uint256 feeQ = getEquivalentTotalQ().mul(feeRate) / (1e18 - feeRate); + if (feeQ > 0) { + _mint(TRANCHE_Q, feeCollector, feeQ); + } + } + + /// @dev Check whether a new rebalance should be triggered. Rebalance is triggered if + /// ROOK's NAV over BISHOP's NAV is greater than the upper threshold or + /// less than the lower threshold. + /// @param navB BISHOP's NAV before the rebalance + /// @param navROrZero ROOK's NAV before the rebalance or zero if the NAV is negative + /// @return Whether a new rebalance should be triggered + function _shouldTriggerRebalance(uint256 navB, uint256 navROrZero) private view returns (bool) { + uint256 rOverB = navROrZero.divideDecimal(navB); + return rOverB < lowerRebalanceThreshold || rOverB > upperRebalanceThreshold; + } + + /// @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 navSum Sum of BISHOP and ROOK's NAV + /// @param navB BISHOP's NAV before this rebalance + /// @param navROrZero ROOK's NAV before this rebalance or zero if the NAV is negative + /// @param newSplitRatio The new split ratio after this rebalance + function _triggerRebalance( + uint256 day, + uint256 navSum, + uint256 navB, + uint256 navROrZero, + uint256 newSplitRatio + ) private { + Rebalance memory rebalance = _calculateRebalance(navSum, navB, navROrZero, 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, + navROrZero, + 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). + /// + /// Note that ROOK's NAV can be negative before the rebalance when the underlying price + /// drops dramatically in a single trading day, in which case zero should be passed to + /// this function instead of the negative NAV. + /// @param navSum Sum of BISHOP and ROOK's NAV + /// @param navB BISHOP's NAV before the rebalance + /// @param navROrZero ROOK's NAV before the rebalance or zero if the NAV is negative + /// @param newSplitRatio The new split ratio after this rebalance + /// @return The rebalance matrix + function _calculateRebalance( + uint256 navSum, + uint256 navB, + uint256 navROrZero, + uint256 newSplitRatio + ) private view returns (Rebalance memory) { + uint256 ratioBR; + uint256 ratioB2Q; + uint256 ratioR2Q; + if (navROrZero <= navB) { + // Lower rebalance + ratioBR = navROrZero; + ratioB2Q = (navSum / 2 - navROrZero).divideDecimal(newSplitRatio); + ratioR2Q = 0; + } else { + // Upper rebalance + ratioBR = UNIT; + ratioB2Q = (navB - UNIT).divideDecimal(newSplitRatio) / 2; + ratioR2Q = (navROrZero - UNIT).divideDecimal(newSplitRatio) / 2; + } + return + Rebalance({ + ratioB2Q: ratioB2Q, + ratioR2Q: ratioR2Q, + ratioBR: ratioBR, + timestamp: block.timestamp + }); + } + + function _updateInterestRate(uint256 day) private returns (uint256) { + uint256 baseInterestRate = MAX_INTEREST_RATE.min(aprOracle.capture()); + uint256 floatingInterestRate = ballot.count(day).div(365); + uint256 rate = baseInterestRate.add(floatingInterestRate); + + emit InterestRateUpdated(baseInterestRate, floatingInterestRate); + + 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"); + _; + } +} diff --git a/contracts/fund/PrimaryMarketV5.sol b/contracts/fund/PrimaryMarketV5.sol new file mode 100644 index 00000000..314f5f52 --- /dev/null +++ b/contracts/fund/PrimaryMarketV5.sol @@ -0,0 +1,594 @@ +// 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/IPrimaryMarketV3.sol"; +import "../interfaces/IFundV3.sol"; +import "../interfaces/IFundForPrimaryMarketV4.sol"; +import "../interfaces/ITrancheIndexV2.sol"; +import "../interfaces/IWrappedERC20.sol"; + +contract PrimaryMarketV5 is IPrimaryMarketV3, 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; + 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_); + 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 = IFundV3(fund).currentDay() - 1 days; + uint256 underlyingPrice = IFundV3(fund).twapOracle().getTwap(settledDay); + (uint256 navB, uint256 navR) = IFundV3(fund).historicalNavs(settledDay); + outQ = outQ.mul(underlyingPrice).div(splitRatio).divideDecimal(navB.add(navR)); + } else { + require( + fundUnderlying != 0, + "Cannot create QUEEN for fund with shares but no underlying" + ); + outQ = underlying.mul(fundEquivalentTotalQ).div(fundUnderlying); + } + } + + /// @notice Calculate the amount of underlying tokens to create at least the given QUEEN amount. + /// This only works with non-empty fund for simplicity. + /// @param minOutQ Minimum received QUEEN amount + /// @return underlying Underlying amount that should be used for creation + function getCreationForQ(uint256 minOutQ) external view override returns (uint256 underlying) { + // Assume: + // minOutQ * fundUnderlying = a * fundEquivalentTotalQ - b + // where a and b are integers and 0 <= b < fundEquivalentTotalQ + // Then + // underlying = a + // getCreation(underlying) + // = floor(a * fundEquivalentTotalQ / fundUnderlying) + // >= floor((a * fundEquivalentTotalQ - b) / fundUnderlying) + // = minOutQ + // getCreation(underlying - 1) + // = floor((a * fundEquivalentTotalQ - fundEquivalentTotalQ) / fundUnderlying) + // < (a * fundEquivalentTotalQ - b) / fundUnderlying + // = minOutQ + uint256 fundUnderlying = IFundV3(fund).getTotalUnderlying(); + uint256 fundEquivalentTotalQ = IFundV3(fund).getEquivalentTotalQ(); + require(fundEquivalentTotalQ > 0, "Cannot calculate creation for empty fund"); + return minOutQ.mul(fundUnderlying).add(fundEquivalentTotalQ - 1).div(fundEquivalentTotalQ); + } + + 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 amount of QUEEN that can be redeemed for at least the given amount + /// of underlying tokens. + /// @dev The return value may not be the minimum solution due to rounding errors. + /// @param minUnderlying Minimum received underlying amount + /// @return inQ QUEEN amount that should be redeemed + function getRedemptionForUnderlying( + uint256 minUnderlying + ) external view override returns (uint256 inQ) { + // Assume: + // minUnderlying * fundEquivalentTotalQ = a * fundUnderlying - b + // a * 1e18 = c * (1e18 - redemptionFeeRate) + d + // where + // a, b, c, d are integers + // 0 <= b < fundUnderlying + // 0 <= d < 1e18 - redemeptionFeeRate + // Then + // inQAfterFee = a + // inQ = c + // getRedemption(inQ).underlying + // = floor((c - floor(c * redemptionFeeRate / 1e18)) * fundUnderlying / fundEquivalentTotalQ) + // = floor(ceil(c * (1e18 - redemptionFeeRate) / 1e18) * fundUnderlying / fundEquivalentTotalQ) + // = floor(((c * (1e18 - redemptionFeeRate) + d) / 1e18) * fundUnderlying / fundEquivalentTotalQ) + // = floor(a * fundUnderlying / fundEquivalentTotalQ) + // => floor((a * fundUnderlying - b) / fundEquivalentTotalQ) + // = minUnderlying + uint256 fundUnderlying = IFundV3(fund).getTotalUnderlying(); + uint256 fundEquivalentTotalQ = IFundV3(fund).getEquivalentTotalQ(); + uint256 inQAfterFee = minUnderlying.mul(fundEquivalentTotalQ).add(fundUnderlying - 1).div( + fundUnderlying + ); + return inQAfterFee.divideDecimal(1e18 - redemptionFeeRate); + } + + /// @notice Calculate the result of a split. + /// @param inQ QUEEN amount to be split + /// @return outB Received BISHOP amount, which is also received ROOK amount + function getSplit(uint256 inQ) public view override returns (uint256 outB) { + return inQ.multiplyDecimal(IFundV3(fund).splitRatio()); + } + + /// @notice Calculate the amount of QUEEN that can be split into at least the given amount of + /// BISHOP and ROOK. + /// @param minOutB Received BISHOP amount, which is also received ROOK amount + /// @return inQ QUEEN amount that should be split + function getSplitForB(uint256 minOutB) external view override returns (uint256 inQ) { + uint256 splitRatio = IFundV3(fund).splitRatio(); + return minOutB.mul(1e18).add(splitRatio.sub(1)).div(splitRatio); + } + + /// @notice Calculate the result of a merge. + /// @param inB Spent BISHOP amount, which is also 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 outQ, uint256 feeQ) { + uint256 outQBeforeFee = inB.divideDecimal(IFundV3(fund).splitRatio()); + feeQ = outQBeforeFee.multiplyDecimal(mergeFeeRate); + outQ = outQBeforeFee.sub(feeQ); + } + + /// @notice Calculate the amount of BISHOP and ROOK that can be merged into at least + /// the given amount of QUEEN. + /// @dev The return value may not be the minimum solution due to rounding errors. + /// @param minOutQ Minimum received QUEEN amount + /// @return inB BISHOP amount that should be merged, which is also spent ROOK amount + function getMergeForQ(uint256 minOutQ) external view override returns (uint256 inB) { + // Assume: + // minOutQ * 1e18 = a * (1e18 - mergeFeeRate) + b + // c = ceil(a * splitRatio / 1e18) + // where a and b are integers and 0 <= b < 1e18 - mergeFeeRate + // Then + // outQBeforeFee = a + // inB = c + // getMerge(inB).outQ + // = c * 1e18 / splitRatio - floor(c * 1e18 / splitRatio * mergeFeeRate / 1e18) + // = ceil(c * 1e18 / splitRatio * (1e18 - mergeFeeRate) / 1e18) + // >= ceil(a * (1e18 - mergeFeeRate) / 1e18) + // = (a * (1e18 - mergeFeeRate) + b) / 1e18 // because b < 1e18 + // = minOutQ + uint256 outQBeforeFee = minOutQ.divideDecimal(1e18 - mergeFeeRate); + inB = outQBeforeFee.mul(IFundV3(fund).splitRatio()).add(1e18 - 1).div(1e18); + } + + /// @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); + + // Call an optional hook in the strategy and ignore errors. + (bool success, ) = IFundV3(fund).strategy().call( + abi.encodeWithSignature("onPrimaryMarketCreate()") + ); + if (!success) { + // ignore + } + } + + /// @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); + + // Call an optional hook in the strategy and ignore errors. + (bool success, ) = IFundV3(fund).strategy().call( + abi.encodeWithSignature("onPrimaryMarketRedeem()") + ); + if (!success) { + // ignore + } + } + + /// @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) { + outB = 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, outB, version); + emit Split(recipient, inQ, outB, outB); + } + + function merge( + address recipient, + uint256 inB, + uint256 version + ) external override returns (uint256 outQ) { + uint256 feeQ; + (outQ, feeQ) = getMerge(inB); + IFundForPrimaryMarketV4(fund).primaryMarketBurn(TRANCHE_B, msg.sender, inB, version); + IFundForPrimaryMarketV4(fund).primaryMarketBurn(TRANCHE_R, msg.sender, inB, 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"); + _; + } +} From bf81f2cf9aa99eead32e9334a7582241e34ea18a Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Mon, 20 Nov 2023 09:38:33 +0800 Subject: [PATCH 02/29] add FundV5 draft --- contracts/fund/FundV5.sol | 93 ++++++----------------- contracts/fund/PrimaryMarketV5.sol | 65 +++++++++------- contracts/interfaces/IFundV5.sol | 8 ++ contracts/interfaces/IPrimaryMarketV5.sol | 71 +++++++++++++++++ contracts/oracle/WstETHPriceOracle.sol | 29 +++++++ 5 files changed, 169 insertions(+), 97 deletions(-) create mode 100644 contracts/interfaces/IFundV5.sol create mode 100644 contracts/interfaces/IPrimaryMarketV5.sol create mode 100644 contracts/oracle/WstETHPriceOracle.sol diff --git a/contracts/fund/FundV5.sol b/contracts/fund/FundV5.sol index 2e178479..61f88b12 100644 --- a/contracts/fund/FundV5.sol +++ b/contracts/fund/FundV5.sol @@ -12,19 +12,17 @@ import "../utils/SafeDecimalMath.sol"; import "../utils/CoreUtility.sol"; import "../interfaces/IPrimaryMarketV3.sol"; -import "../interfaces/IFundV4.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/IBallot.sol"; import "../interfaces/IVotingEscrow.sol"; import "./FundRolesV2.sol"; -contract FundV4 is - IFundV4, +contract FundV5 is + IFundV5, IFundForPrimaryMarketV4, IFundForStrategyV2, Ownable, @@ -41,17 +39,17 @@ contract FundV4 is event LossReported(uint256 loss); event DailyProtocolFeeRateUpdated(uint256 newDailyProtocolFeeRate); event TwapOracleUpdated(address newTwapOracle); - event AprOracleUpdated(address newAprOracle); - event BallotUpdated(address newBallot); event FeeCollectorUpdated(address newFeeCollector); event ActivityDelayTimeUpdated(uint256 delayTime); event SplitRatioUpdated(uint256 newSplitRatio); event TotalDebtUpdated(uint256 newTotalDebt); uint256 private constant UNIT = 1e18; - uint256 private constant MAX_INTEREST_RATE = 0.2e18; // 20% daily + uint256 private constant INTEREST_RATE = 8219178082191780; // 3% yearly uint256 private constant MAX_DAILY_PROTOCOL_FEE_RATE = 0.05e18; // 5% daily rate + uint256 public constant override WEIGHT_B = 9; + /// @notice Upper bound of `NAV_R / NAV_B` to trigger a rebalance. uint256 public immutable upperRebalanceThreshold; @@ -70,12 +68,6 @@ contract FundV4 is /// @notice TwapOracle address for the underlying asset. ITwapOracleV2 public override twapOracle; - /// @notice AprOracle address. - IAprOracle public aprOracle; - - /// @notice Address of the interest rate ballot. - IBallot public ballot; - /// @notice Fee Collector address. address public override feeCollector; @@ -141,12 +133,6 @@ contract FundV4 is /// 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 in the following trading day. - mapping(uint256 => uint256) public historicalInterestRate; - /// @dev Amount of redemption underlying that the fund owes the primary market uint256 private _totalDebt; @@ -164,8 +150,6 @@ contract FundV4 is uint256 upperRebalanceThreshold; uint256 lowerRebalanceThreshold; address twapOracle; - address aprOracle; - address ballot; address feeCollector; } @@ -189,8 +173,6 @@ contract FundV4 is upperRebalanceThreshold = params.upperRebalanceThreshold; lowerRebalanceThreshold = params.lowerRebalanceThreshold; _updateTwapOracle(params.twapOracle); - _updateAprOracle(params.aprOracle); - _updateBallot(params.ballot); _updateFeeCollector(params.feeCollector); _updateActivityDelayTime(30 minutes); } @@ -216,9 +198,7 @@ contract FundV4 is _historicalNavB[lastDay] = lastNavB; _historicalNavR[lastDay] = lastNavR; _strategyUnderlying = strategyUnderlying; - uint256 lastInterestRate = _updateInterestRate(lastDay); - historicalInterestRate[lastDay] = lastInterestRate; - emit Settled(lastDay, lastNavB, lastNavR, lastInterestRate); + emit Settled(lastDay, lastNavB, lastNavR, INTEREST_RATE); fundActivityStartTime = lastDay; } @@ -302,12 +282,18 @@ contract FundV4 is /// @notice Equivalent BISHOP supply, as if all QUEEN are split. function getEquivalentTotalB() public view override returns (uint256) { - return _totalSupplies[TRANCHE_Q].multiplyDecimal(splitRatio).add(_totalSupplies[TRANCHE_B]); + return + _totalSupplies[TRANCHE_Q].multiplyDecimal(splitRatio.mul(WEIGHT_B)).add( + _totalSupplies[TRANCHE_B] + ); } /// @notice Equivalent QUEEN supply, as if all BISHOP and ROOK are merged. function getEquivalentTotalQ() public view override returns (uint256) { - return _totalSupplies[TRANCHE_B].divideDecimal(splitRatio).add(_totalSupplies[TRANCHE_Q]); + return + _totalSupplies[TRANCHE_B].divideDecimal(splitRatio.mul(WEIGHT_B)).add( + _totalSupplies[TRANCHE_Q] + ); } /// @notice Return the rebalance matrix at a given index. A zero struct is returned @@ -384,13 +370,16 @@ contract FundV4 is if (equivalentTotalB > 0) { navSum = price.mul(underlying.mul(underlyingDecimalMultiplier)).div(equivalentTotalB); navB = navB.multiplyDecimal( - historicalInterestRate[settledDay].mul(timestamp - settledDay).div(1 days).add(UNIT) + INTEREST_RATE.mul(timestamp - settledDay).div(1 days).add(UNIT) ); - navROrZero = navSum >= navB ? navSum - navB : 0; + + navROrZero = navSum.divideDecimal(splitRatio) >= navB.mul(WEIGHT_B) + ? navSum.divideDecimal(splitRatio) - navB.mul(WEIGHT_B) + : 0; } else { // If the fund is empty, use NAV in the last day navROrZero = _historicalNavR[settledDay]; - navSum = navB + navROrZero; + navSum = navB.mul(WEIGHT_B) + navROrZero; } } @@ -796,11 +785,9 @@ contract FundV4 is historicalUnderlying[day] = underlying; _historicalNavB[day] = navB; _historicalNavR[day] = navR; - uint256 interestRate = _updateInterestRate(day); - historicalInterestRate[day] = interestRate; currentDay = day + 1 days; - emit Settled(day, navB, navR, interestRate); + emit Settled(day, navB, navR, INTEREST_RATE); } function transferToStrategy(uint256 amount) external override onlyStrategy { @@ -899,24 +886,6 @@ contract FundV4 is _updateTwapOracle(newTwapOracle); } - function _updateAprOracle(address newAprOracle) private { - aprOracle = IAprOracle(newAprOracle); - emit AprOracleUpdated(newAprOracle); - } - - function updateAprOracle(address newAprOracle) external onlyOwner { - _updateAprOracle(newAprOracle); - } - - function _updateBallot(address newBallot) private { - ballot = IBallot(newBallot); - emit BallotUpdated(newBallot); - } - - function updateBallot(address newBallot) external onlyOwner { - _updateBallot(newBallot); - } - function _updateFeeCollector(address newFeeCollector) private { feeCollector = newFeeCollector; emit FeeCollectorUpdated(newFeeCollector); @@ -976,7 +945,7 @@ contract FundV4 is uint256 navROrZero, uint256 newSplitRatio ) private { - Rebalance memory rebalance = _calculateRebalance(navSum, navB, navROrZero, newSplitRatio); + Rebalance memory rebalance = _calculateRebalance(navB, navROrZero, newSplitRatio); uint256 oldSize = _rebalanceSize; splitRatio = newSplitRatio; _historicalSplitRatio[oldSize + 1] = newSplitRatio; @@ -1012,13 +981,11 @@ contract FundV4 is /// Note that ROOK's NAV can be negative before the rebalance when the underlying price /// drops dramatically in a single trading day, in which case zero should be passed to /// this function instead of the negative NAV. - /// @param navSum Sum of BISHOP and ROOK's NAV /// @param navB BISHOP's NAV before the rebalance /// @param navROrZero ROOK's NAV before the rebalance or zero if the NAV is negative /// @param newSplitRatio The new split ratio after this rebalance /// @return The rebalance matrix function _calculateRebalance( - uint256 navSum, uint256 navB, uint256 navROrZero, uint256 newSplitRatio @@ -1028,8 +995,8 @@ contract FundV4 is uint256 ratioR2Q; if (navROrZero <= navB) { // Lower rebalance - ratioBR = navROrZero; - ratioB2Q = (navSum / 2 - navROrZero).divideDecimal(newSplitRatio); + ratioBR = UNIT; + ratioB2Q = 0; ratioR2Q = 0; } else { // Upper rebalance @@ -1046,16 +1013,6 @@ contract FundV4 is }); } - function _updateInterestRate(uint256 day) private returns (uint256) { - uint256 baseInterestRate = MAX_INTEREST_RATE.min(aprOracle.capture()); - uint256 floatingInterestRate = ballot.count(day).div(365); - uint256 rate = baseInterestRate.add(floatingInterestRate); - - emit InterestRateUpdated(baseInterestRate, floatingInterestRate); - - return rate; - } - function _updateTotalDebt(uint256 newTotalDebt) private { _totalDebt = newTotalDebt; emit TotalDebtUpdated(newTotalDebt); diff --git a/contracts/fund/PrimaryMarketV5.sol b/contracts/fund/PrimaryMarketV5.sol index 314f5f52..63d8161b 100644 --- a/contracts/fund/PrimaryMarketV5.sol +++ b/contracts/fund/PrimaryMarketV5.sol @@ -11,13 +11,13 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "../utils/SafeDecimalMath.sol"; -import "../interfaces/IPrimaryMarketV3.sol"; -import "../interfaces/IFundV3.sol"; +import "../interfaces/IPrimaryMarketV5.sol"; +import "../interfaces/IFundV5.sol"; import "../interfaces/IFundForPrimaryMarketV4.sol"; import "../interfaces/ITrancheIndexV2.sol"; import "../interfaces/IWrappedERC20.sol"; -contract PrimaryMarketV5 is IPrimaryMarketV3, ReentrancyGuard, ITrancheIndexV2, Ownable { +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); @@ -50,6 +50,7 @@ contract PrimaryMarketV5 is IPrimaryMarketV3, ReentrancyGuard, ITrancheIndexV2, address public immutable override fund; bool public immutable redemptionFlag; + uint256 public immutable weightB; IERC20 private immutable _tokenUnderlying; uint256 public redemptionFeeRate; @@ -86,10 +87,11 @@ contract PrimaryMarketV5 is IPrimaryMarketV3, ReentrancyGuard, ITrancheIndexV2, bool redemptionFlag_ ) public Ownable() { fund = fund_; - _tokenUnderlying = IERC20(IFundV3(fund_).tokenUnderlying()); + _tokenUnderlying = IERC20(IFundV5(fund_).tokenUnderlying()); _updateRedemptionFeeRate(redemptionFeeRate_); _updateMergeFeeRate(mergeFeeRate_); _updateFundCap(fundCap_); + weightB = IFundV5(fund_).WEIGHT_B(); redemptionFlag = redemptionFlag_; } @@ -97,17 +99,19 @@ contract PrimaryMarketV5 is IPrimaryMarketV3, ReentrancyGuard, ITrancheIndexV2, /// @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(); + uint256 fundUnderlying = IFundV5(fund).getTotalUnderlying(); + uint256 fundEquivalentTotalQ = IFundV5(fund).getEquivalentTotalQ(); require(fundUnderlying.add(underlying) <= fundCap, "Exceed fund cap"); if (fundEquivalentTotalQ == 0) { - outQ = underlying.mul(IFundV3(fund).underlyingDecimalMultiplier()); - uint256 splitRatio = IFundV3(fund).splitRatio(); + outQ = underlying.mul(IFundV5(fund).underlyingDecimalMultiplier()); + uint256 splitRatio = IFundV5(fund).splitRatio(); require(splitRatio != 0, "Fund is not initialized"); - uint256 settledDay = IFundV3(fund).currentDay() - 1 days; - uint256 underlyingPrice = IFundV3(fund).twapOracle().getTwap(settledDay); - (uint256 navB, uint256 navR) = IFundV3(fund).historicalNavs(settledDay); - outQ = outQ.mul(underlyingPrice).div(splitRatio).divideDecimal(navB.add(navR)); + uint256 settledDay = IFundV5(fund).currentDay() - 1 days; + uint256 underlyingPrice = IFundV5(fund).twapOracle().getTwap(settledDay); + (uint256 navB, uint256 navR) = IFundV5(fund).historicalNavs(settledDay); + outQ = outQ.mul(underlyingPrice).div(splitRatio).divideDecimal( + navB.mul(weightB).add(navR) + ); } else { require( fundUnderlying != 0, @@ -135,15 +139,15 @@ contract PrimaryMarketV5 is IPrimaryMarketV3, ReentrancyGuard, ITrancheIndexV2, // = floor((a * fundEquivalentTotalQ - fundEquivalentTotalQ) / fundUnderlying) // < (a * fundEquivalentTotalQ - b) / fundUnderlying // = minOutQ - uint256 fundUnderlying = IFundV3(fund).getTotalUnderlying(); - uint256 fundEquivalentTotalQ = IFundV3(fund).getEquivalentTotalQ(); + uint256 fundUnderlying = IFundV5(fund).getTotalUnderlying(); + uint256 fundEquivalentTotalQ = IFundV5(fund).getEquivalentTotalQ(); require(fundEquivalentTotalQ > 0, "Cannot calculate creation for empty fund"); return minOutQ.mul(fundUnderlying).add(fundEquivalentTotalQ - 1).div(fundEquivalentTotalQ); } function _getRedemption(uint256 inQ) private view returns (uint256 underlying) { - uint256 fundUnderlying = IFundV3(fund).getTotalUnderlying(); - uint256 fundEquivalentTotalQ = IFundV3(fund).getEquivalentTotalQ(); + uint256 fundUnderlying = IFundV5(fund).getTotalUnderlying(); + uint256 fundEquivalentTotalQ = IFundV5(fund).getEquivalentTotalQ(); underlying = inQ.mul(fundUnderlying).div(fundEquivalentTotalQ); } @@ -183,8 +187,8 @@ contract PrimaryMarketV5 is IPrimaryMarketV3, ReentrancyGuard, ITrancheIndexV2, // = floor(a * fundUnderlying / fundEquivalentTotalQ) // => floor((a * fundUnderlying - b) / fundEquivalentTotalQ) // = minUnderlying - uint256 fundUnderlying = IFundV3(fund).getTotalUnderlying(); - uint256 fundEquivalentTotalQ = IFundV3(fund).getEquivalentTotalQ(); + uint256 fundUnderlying = IFundV5(fund).getTotalUnderlying(); + uint256 fundEquivalentTotalQ = IFundV5(fund).getEquivalentTotalQ(); uint256 inQAfterFee = minUnderlying.mul(fundEquivalentTotalQ).add(fundUnderlying - 1).div( fundUnderlying ); @@ -194,8 +198,9 @@ contract PrimaryMarketV5 is IPrimaryMarketV3, ReentrancyGuard, ITrancheIndexV2, /// @notice Calculate the result of a split. /// @param inQ QUEEN amount to be split /// @return outB Received BISHOP amount, which is also received ROOK amount - function getSplit(uint256 inQ) public view override returns (uint256 outB) { - return inQ.multiplyDecimal(IFundV3(fund).splitRatio()); + 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 amount of QUEEN that can be split into at least the given amount of @@ -203,7 +208,7 @@ contract PrimaryMarketV5 is IPrimaryMarketV3, ReentrancyGuard, ITrancheIndexV2, /// @param minOutB Received BISHOP amount, which is also received ROOK amount /// @return inQ QUEEN amount that should be split function getSplitForB(uint256 minOutB) external view override returns (uint256 inQ) { - uint256 splitRatio = IFundV3(fund).splitRatio(); + uint256 splitRatio = IFundV5(fund).splitRatio(); return minOutB.mul(1e18).add(splitRatio.sub(1)).div(splitRatio); } @@ -212,7 +217,8 @@ contract PrimaryMarketV5 is IPrimaryMarketV3, ReentrancyGuard, ITrancheIndexV2, /// @return outQ Received QUEEN amount /// @return feeQ QUEEN amount charged as merge fee function getMerge(uint256 inB) public view override returns (uint256 outQ, uint256 feeQ) { - uint256 outQBeforeFee = inB.divideDecimal(IFundV3(fund).splitRatio()); + uint256 splitRatio = IFundV5(fund).splitRatio(); + uint256 outQBeforeFee = inB.divideDecimal(splitRatio.mul(weightB)); feeQ = outQBeforeFee.multiplyDecimal(mergeFeeRate); outQ = outQBeforeFee.sub(feeQ); } @@ -236,8 +242,9 @@ contract PrimaryMarketV5 is IPrimaryMarketV3, ReentrancyGuard, ITrancheIndexV2, // >= ceil(a * (1e18 - mergeFeeRate) / 1e18) // = (a * (1e18 - mergeFeeRate) + b) / 1e18 // because b < 1e18 // = minOutQ + uint256 splitRatio = IFundV5(fund).splitRatio(); uint256 outQBeforeFee = minOutQ.divideDecimal(1e18 - mergeFeeRate); - inB = outQBeforeFee.mul(IFundV3(fund).splitRatio()).add(1e18 - 1).div(1e18); + inB = outQBeforeFee.mul(splitRatio.mul(weightB)).add(1e18 - 1).div(1e18); } /// @notice Return index of the first queued redemption that cannot be claimed now. @@ -329,7 +336,7 @@ contract PrimaryMarketV5 is IPrimaryMarketV3, ReentrancyGuard, ITrancheIndexV2, emit Created(recipient, underlying, outQ); // Call an optional hook in the strategy and ignore errors. - (bool success, ) = IFundV3(fund).strategy().call( + (bool success, ) = IFundV5(fund).strategy().call( abi.encodeWithSignature("onPrimaryMarketCreate()") ); if (!success) { @@ -390,7 +397,7 @@ contract PrimaryMarketV5 is IPrimaryMarketV3, ReentrancyGuard, ITrancheIndexV2, emit Redeemed(recipient, inQ, underlying, feeQ); // Call an optional hook in the strategy and ignore errors. - (bool success, ) = IFundV3(fund).strategy().call( + (bool success, ) = IFundV5(fund).strategy().call( abi.encodeWithSignature("onPrimaryMarketRedeem()") ); if (!success) { @@ -525,12 +532,12 @@ contract PrimaryMarketV5 is IPrimaryMarketV3, ReentrancyGuard, ITrancheIndexV2, address recipient, uint256 inQ, uint256 version - ) external override returns (uint256 outB) { - outB = getSplit(inQ); + ) 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, outB, version); - emit Split(recipient, inQ, outB, outB); + IFundForPrimaryMarketV4(fund).primaryMarketMint(TRANCHE_R, recipient, outR, version); + emit Split(recipient, inQ, outB, outR); } function merge( diff --git a/contracts/interfaces/IFundV5.sol b/contracts/interfaces/IFundV5.sol new file mode 100644 index 00000000..c4e06c78 --- /dev/null +++ b/contracts/interfaces/IFundV5.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +import "./IFundV4.sol"; + +interface IFundV5 is IFundV4 { + function WEIGHT_B() external view returns (uint256); +} diff --git a/contracts/interfaces/IPrimaryMarketV5.sol b/contracts/interfaces/IPrimaryMarketV5.sol new file mode 100644 index 00000000..0be9425d --- /dev/null +++ b/contracts/interfaces/IPrimaryMarketV5.sol @@ -0,0 +1,71 @@ +// 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 getCreationForQ(uint256 minOutQ) external view returns (uint256 underlying); + + function getRedemption(uint256 inQ) external view returns (uint256 underlying, uint256 fee); + + function getRedemptionForUnderlying(uint256 minUnderlying) external view returns (uint256 inQ); + + function getSplit(uint256 inQ) external view returns (uint256 outB, uint256 outQ); + + function getSplitForB(uint256 minOutB) external view returns (uint256 inQ); + + function getMerge(uint256 inB) external view returns (uint256 outQ, uint256 feeQ); + + function getMergeForQ(uint256 minOutQ) external view returns (uint256 inB); + + 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/oracle/WstETHPriceOracle.sol b/contracts/oracle/WstETHPriceOracle.sol new file mode 100644 index 00000000..f52fc447 --- /dev/null +++ b/contracts/oracle/WstETHPriceOracle.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +import "../interfaces/ITwapOracleV2.sol"; + +interface IWstETH { + function stEthPerToken() external view returns (uint256); +} + +/// @title wstETH Price oracle +/// @author Tranchess +contract WstETHPriceOracle is ITwapOracleV2 { + /// @notice Chainlink aggregator used as the data source. + 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(); + } +} From 2e62d2fd20e8eed361494d090fd4eb922be2e43f Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Mon, 27 Nov 2023 07:55:37 +0800 Subject: [PATCH 03/29] add rookstableswap v2 --- contracts/swap/RookStableSwapV2.sol | 157 ++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 contracts/swap/RookStableSwapV2.sol diff --git a/contracts/swap/RookStableSwapV2.sol b/contracts/swap/RookStableSwapV2.sol new file mode 100644 index 00000000..4fe68ec3 --- /dev/null +++ b/contracts/swap/RookStableSwapV2.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +import "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol"; + +import "../interfaces/IPrimaryMarketV5.sol"; +import "../interfaces/ITrancheIndexV2.sol"; +import "./StableSwapV2.sol"; + +contract RookStableSwapV2 is StableSwapV2, ITrancheIndexV2 { + event Rebalanced(uint256 base, uint256 quote, uint256 version); + + uint256 public immutable tradingCurbThreshold; + + uint256 public currentVersion; + + constructor( + address lpToken_, + address fund_, + address quoteAddress_, + uint256 quoteDecimals_, + uint256 ampl_, + address feeCollector_, + uint256 feeRate_, + uint256 adminFeeRate_, + uint256 tradingCurbThreshold_ + ) + public + StableSwapV2( + lpToken_, + fund_, + TRANCHE_R, + quoteAddress_, + quoteDecimals_, + ampl_, + feeCollector_, + feeRate_, + adminFeeRate_ + ) + { + tradingCurbThreshold = tradingCurbThreshold_; + 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 ( + uint256 newBase, + uint256 newQuote, + uint256 excessiveQ, + uint256 excessiveB, + uint256 excessiveR, + uint256 excessiveQuote, + bool isRebalanced + ) + { + if (latestVersion == currentVersion) { + return (baseBalance, quoteBalance, 0, 0, 0, 0, false); + } + isRebalanced = true; + + uint256 oldBaseBalance = baseBalance; + uint256 oldQuoteBalance = quoteBalance; + (excessiveQ, newBase, ) = fund.batchRebalance( + 0, + oldBaseBalance, + 0, + currentVersion, + latestVersion + ); + if (newBase < oldBaseBalance) { + // We split all QUEEN from rebalance if the amount of ROOK is smaller than before. + // In almost all cases, the total amount of ROOK after the split is still smaller + // than before. + (excessiveB, excessiveR) = IPrimaryMarketV5(fund.primaryMarket()).getSplit(excessiveQ); + newBase = newBase.add(excessiveR); + } + if (newBase < oldBaseBalance) { + // If ROOK amount is still smaller than before, we remove quote tokens proportionally. + newQuote = oldQuoteBalance.mul(newBase).div(oldBaseBalance); + excessiveQuote = oldQuoteBalance - newQuote; + } else { + // In most cases when we reach here, the ROOK amount remains the same (ratioBR = 1). + newQuote = oldQuoteBalance; + excessiveR = newBase - oldBaseBalance; + newBase = oldBaseBalance; + } + } + + function _handleRebalance( + uint256 latestVersion + ) internal override returns (uint256 newBase, uint256 newQuote) { + uint256 excessiveQ; + uint256 excessiveB; + uint256 excessiveR; + uint256 excessiveQuote; + bool isRebalanced; + ( + newBase, + newQuote, + excessiveQ, + excessiveB, + excessiveR, + excessiveQuote, + isRebalanced + ) = _getRebalanceResult(latestVersion); + if (isRebalanced) { + baseBalance = newBase; + quoteBalance = newQuote; + currentVersion = latestVersion; + emit Rebalanced(newBase, newQuote, latestVersion); + if (excessiveQ > 0) { + if (excessiveR > 0) { + IPrimaryMarketV5(fund.primaryMarket()).split( + address(this), + excessiveQ, + latestVersion + ); + excessiveQ = 0; + } else { + fund.trancheTransfer(TRANCHE_Q, lpToken, excessiveQ, latestVersion); + } + } + if (excessiveB > 0) { + fund.trancheTransfer(TRANCHE_B, lpToken, excessiveB, latestVersion); + } + if (excessiveR > 0) { + fund.trancheTransfer(TRANCHE_R, lpToken, excessiveR, latestVersion); + } + if (excessiveQuote > 0) { + IERC20(quoteAddress).safeTransfer(lpToken, excessiveQuote); + } + ILiquidityGauge(lpToken).distribute( + excessiveQ, + excessiveB, + excessiveR, + excessiveQuote, + latestVersion + ); + } + } + + function getOraclePrice() public view override returns (uint256) { + uint256 price = fund.twapOracle().getLatest(); + (, uint256 navB, uint256 navR) = fund.extrapolateNav(price); + return navR; + } +} From 65312a9065eaef7f410abf1ce8f5c8231f16b127 Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Mon, 27 Nov 2023 08:00:24 +0800 Subject: [PATCH 04/29] wstETH as quote token --- contracts/swap/RookStableSwapV2.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/swap/RookStableSwapV2.sol b/contracts/swap/RookStableSwapV2.sol index 4fe68ec3..68a1a44a 100644 --- a/contracts/swap/RookStableSwapV2.sol +++ b/contracts/swap/RookStableSwapV2.sol @@ -150,8 +150,8 @@ contract RookStableSwapV2 is StableSwapV2, ITrancheIndexV2 { } function getOraclePrice() public view override returns (uint256) { - uint256 price = fund.twapOracle().getLatest(); - (, uint256 navB, uint256 navR) = fund.extrapolateNav(price); - return navR; + uint256 price = fund.twapOracle().getLatest(); // wstETH / stETH + (, uint256 navB, uint256 navR) = fund.extrapolateNav(price); // ETH / ROOK + return navR.multiplyDecimal(price); // wstETH / ROOK } } From d8fd71a48abdf6fc25319572963c1afa6bd715fa Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Mon, 27 Nov 2023 11:39:40 +0800 Subject: [PATCH 05/29] remove protocol feee; wstETH/ROOK --- contracts/fund/FundV5.sol | 291 ++++++---------------------- contracts/swap/RookStableSwapV2.sol | 2 +- 2 files changed, 62 insertions(+), 231 deletions(-) diff --git a/contracts/fund/FundV5.sol b/contracts/fund/FundV5.sol index 61f88b12..2fdd5bc4 100644 --- a/contracts/fund/FundV5.sol +++ b/contracts/fund/FundV5.sol @@ -14,7 +14,6 @@ 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/IVotingEscrow.sol"; @@ -24,7 +23,6 @@ import "./FundRolesV2.sol"; contract FundV5 is IFundV5, IFundForPrimaryMarketV4, - IFundForStrategyV2, Ownable, ReentrancyGuard, FundRolesV2, @@ -35,42 +33,26 @@ contract FundV5 is using SafeDecimalMath for uint256; using SafeERC20 for IERC20; - event ProfitReported(uint256 profit, uint256 totalFee, uint256 totalFeeQ, uint256 strategyFeeQ); event LossReported(uint256 loss); - event DailyProtocolFeeRateUpdated(uint256 newDailyProtocolFeeRate); event TwapOracleUpdated(address newTwapOracle); - event FeeCollectorUpdated(address newFeeCollector); event ActivityDelayTimeUpdated(uint256 delayTime); event SplitRatioUpdated(uint256 newSplitRatio); event TotalDebtUpdated(uint256 newTotalDebt); uint256 private constant UNIT = 1e18; uint256 private constant INTEREST_RATE = 8219178082191780; // 3% yearly - uint256 private constant MAX_DAILY_PROTOCOL_FEE_RATE = 0.05e18; // 5% daily rate uint256 public constant override WEIGHT_B = 9; - /// @notice Upper bound of `NAV_R / NAV_B` to trigger a rebalance. - uint256 public immutable upperRebalanceThreshold; - - /// @notice Lower bound of `NAV_R / NAV_B` to trigger a rebalance. - uint256 public immutable lowerRebalanceThreshold; - /// @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 Daily protocol fee rate. - uint256 public dailyProtocolFeeRate; - /// @notice TwapOracle address for the underlying asset. ITwapOracleV2 public override twapOracle; - /// @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). @@ -136,8 +118,6 @@ contract FundV5 is /// @dev Amount of redemption underlying that the fund owes the primary market uint256 private _totalDebt; - uint256 private _strategyUnderlying; - struct ConstructorParameters { address tokenUnderlying; uint256 underlyingDecimals; @@ -145,12 +125,7 @@ contract FundV5 is address tokenB; address tokenR; address primaryMarket; - address strategy; - uint256 dailyProtocolFeeRate; - uint256 upperRebalanceThreshold; - uint256 lowerRebalanceThreshold; address twapOracle; - address feeCollector; } constructor( @@ -163,17 +138,13 @@ contract FundV5 is params.tokenB, params.tokenR, params.primaryMarket, - params.strategy + address(0) ) { tokenUnderlying = params.tokenUnderlying; require(params.underlyingDecimals <= 18, "Underlying decimals larger than 18"); underlyingDecimalMultiplier = 10 ** (18 - params.underlyingDecimals); - _updateDailyProtocolFeeRate(params.dailyProtocolFeeRate); - upperRebalanceThreshold = params.upperRebalanceThreshold; - lowerRebalanceThreshold = params.lowerRebalanceThreshold; _updateTwapOracle(params.twapOracle); - _updateFeeCollector(params.feeCollector); _updateActivityDelayTime(30 minutes); } @@ -181,25 +152,20 @@ contract FundV5 is uint256 newSplitRatio, uint256 lastNavB, uint256 lastNavR, - uint256 strategyUnderlying ) external onlyOwner { require(splitRatio == 0 && currentDay == 0, "Already initialized"); - require( - newSplitRatio != 0 && lastNavB >= UNIT && !_shouldTriggerRebalance(lastNavB, lastNavR), - "Invalid parameters" - ); + require(newSplitRatio != 0 && lastNavB >= UNIT, "Invalid parameters"); currentDay = endOfDay(block.timestamp); splitRatio = newSplitRatio; _historicalSplitRatio[0] = newSplitRatio; emit SplitRatioUpdated(newSplitRatio); - uint256 lastDay = currentDay - 1 days; - 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; - emit Settled(lastDay, lastNavB, lastNavR, INTEREST_RATE); - fundActivityStartTime = lastDay; + uint256 lastYear = currentDay - 365 days; + uint256 lastYearPrice = twapOracle.getTwap(lastYear); + require(lastYearPrice != 0, "Price not available"); // required to do the first creation + _historicalNavB[lastYear] = lastNavB; + _historicalNavR[lastYear] = lastNavR; + emit Settled(lastYear, lastNavB, lastNavR, INTEREST_RATE); + fundActivityStartTime = lastYear; } /// @notice UTC time of a day when the fund settles. @@ -251,14 +217,6 @@ contract FundV5 is 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 @@ -268,11 +226,7 @@ contract FundV5 is 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; + return hot.sub(_totalDebt); } /// @notice Get the amount of redemption underlying that the fund owes the primary market. @@ -336,10 +290,10 @@ contract FundV5 is } /// @notice Estimate the current NAV of all tranches, considering underlying price change, - /// accrued protocol fee and accrued interest since the previous settlement. + /// and accrued interest since the previous settlement. /// /// The extrapolation uses simple interest instead of daily compound interest in - /// calculating protocol fee and BISHOP's interest. There may be significant error + /// 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 the estimated NAV of BISHOP and ROOK @@ -350,11 +304,6 @@ contract FundV5 is ) external view override returns (uint256 navSum, uint256 navB, uint256 navROrZero) { uint256 settledDay = currentDay - 1 days; uint256 underlying = getTotalUnderlying(); - uint256 protocolFee = underlying - .multiplyDecimal(dailyProtocolFeeRate) - .mul(block.timestamp - settledDay) - .div(1 days); - underlying = underlying.sub(protocolFee); return _extrapolateNav(block.timestamp, settledDay, price, getEquivalentTotalB(), underlying); } @@ -744,19 +693,16 @@ contract FundV5 is /// @notice Settle the current trading day. Settlement includes the following changes /// to the fund. /// - /// 1. Charge protocol fee of the day. - /// 2. Settle all pending creations and redemptions from the primary market. - /// 3. Calculate NAV of the day and trigger rebalance if necessary. - /// 4. Capture new interest rate for BISHOP. + /// 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 { uint256 day = currentDay; require(day != 0, "Not initialized"); - require(block.timestamp >= day, "The current trading day does not end yet"); + require(block.timestamp >= day + 365 days, "The current trading year not end yet"); uint256 price = twapOracle.getTwap(day); require(price != 0, "Underlying price for settlement is not ready yet"); - _collectFee(); - IPrimaryMarketV3(_primaryMarket).settle(day); // Calculate NAV @@ -764,42 +710,28 @@ contract FundV5 is uint256 underlying = getTotalUnderlying(); (uint256 navSum, uint256 navB, uint256 navR) = _extrapolateNav( day, - day - 1 days, + day - 365 days, price, equivalentTotalB, underlying ); - if (_shouldTriggerRebalance(navB, navR)) { - uint256 newSplitRatio = splitRatio.multiplyDecimal(navSum) / 2; - _triggerRebalance(day, navSum, navB, navR, newSplitRatio); - navB = UNIT; - navR = UNIT; - equivalentTotalB = getEquivalentTotalB(); - fundActivityStartTime = day + activityDelayTimeAfterRebalance; - } else { - fundActivityStartTime = day; - } + uint256 newSplitRatio = splitRatio.multiplyDecimal(navSum) / 2; + _triggerRebalance(day, navSum, navB, navR, newSplitRatio); + navB = UNIT; + navR = UNIT; + equivalentTotalB = getEquivalentTotalB(); + fundActivityStartTime = day + activityDelayTimeAfterRebalance; historicalEquivalentTotalB[day] = equivalentTotalB; historicalUnderlying[day] = underlying; _historicalNavB[day] = navB; _historicalNavR[day] = navR; - currentDay = day + 1 days; + currentDay = day + 365 days; emit Settled(day, navB, navR, INTEREST_RATE); } - 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, @@ -822,27 +754,6 @@ contract FundV5 is 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); } @@ -855,28 +766,6 @@ contract FundV5 is _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 _updateDailyProtocolFeeRate(uint256 newDailyProtocolFeeRate) private { - require( - newDailyProtocolFeeRate <= MAX_DAILY_PROTOCOL_FEE_RATE, - "Exceed max protocol fee rate" - ); - dailyProtocolFeeRate = newDailyProtocolFeeRate; - emit DailyProtocolFeeRateUpdated(newDailyProtocolFeeRate); - } - - function updateDailyProtocolFeeRate(uint256 newDailyProtocolFeeRate) external onlyOwner { - _updateDailyProtocolFeeRate(newDailyProtocolFeeRate); - } - function _updateTwapOracle(address newTwapOracle) private { twapOracle = ITwapOracleV2(newTwapOracle); emit TwapOracleUpdated(newTwapOracle); @@ -886,15 +775,6 @@ contract FundV5 is _updateTwapOracle(newTwapOracle); } - 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, @@ -908,29 +788,6 @@ contract FundV5 is _updateActivityDelayTime(delayTime); } - /// @dev Collect protocol fee by minting QUEEN tokens to the fee collector. - function _collectFee() private { - uint256 feeRate = dailyProtocolFeeRate; - if (feeRate == 0) { - return; - } - uint256 feeQ = getEquivalentTotalQ().mul(feeRate) / (1e18 - feeRate); - if (feeQ > 0) { - _mint(TRANCHE_Q, feeCollector, feeQ); - } - } - - /// @dev Check whether a new rebalance should be triggered. Rebalance is triggered if - /// ROOK's NAV over BISHOP's NAV is greater than the upper threshold or - /// less than the lower threshold. - /// @param navB BISHOP's NAV before the rebalance - /// @param navROrZero ROOK's NAV before the rebalance or zero if the NAV is negative - /// @return Whether a new rebalance should be triggered - function _shouldTriggerRebalance(uint256 navB, uint256 navROrZero) private view returns (bool) { - uint256 rOverB = navROrZero.divideDecimal(navB); - return rOverB < lowerRebalanceThreshold || rOverB > upperRebalanceThreshold; - } - /// @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 @@ -945,72 +802,46 @@ contract FundV5 is uint256 navROrZero, uint256 newSplitRatio ) private { - Rebalance memory rebalance = _calculateRebalance(navB, navROrZero, 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, - navROrZero, - 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). - /// - /// Note that ROOK's NAV can be negative before the rebalance when the underlying price - /// drops dramatically in a single trading day, in which case zero should be passed to - /// this function instead of the negative NAV. - /// @param navB BISHOP's NAV before the rebalance - /// @param navROrZero ROOK's NAV before the rebalance or zero if the NAV is negative - /// @param newSplitRatio The new split ratio after this rebalance - /// @return The rebalance matrix - function _calculateRebalance( - uint256 navB, - uint256 navROrZero, - uint256 newSplitRatio - ) private view returns (Rebalance memory) { - uint256 ratioBR; - uint256 ratioB2Q; - uint256 ratioR2Q; - if (navROrZero <= navB) { - // Lower rebalance - ratioBR = UNIT; - ratioB2Q = 0; - ratioR2Q = 0; - } else { + if (navROrZero > navB) { // Upper rebalance - ratioBR = UNIT; - ratioB2Q = (navB - UNIT).divideDecimal(newSplitRatio) / 2; - ratioR2Q = (navROrZero - UNIT).divideDecimal(newSplitRatio) / 2; - } - return - Rebalance({ - ratioB2Q: ratioB2Q, - ratioR2Q: ratioR2Q, - ratioBR: ratioBR, + Rebalance memory rebalance = Rebalance({ + ratioB2Q: (navB - UNIT).divideDecimal(newSplitRatio) / 2, + ratioR2Q: (navROrZero - UNIT).divideDecimal(newSplitRatio) / 2, + ratioBR: UNIT, timestamp: block.timestamp }); + uint256 oldSize = _rebalanceSize; + splitRatio = newSplitRatio; + _historicalSplitRatio[oldSize + 1] = newSplitRatio; + emit SplitRatioUpdated(newSplitRatio); + _rebalances[oldSize] = rebalance; + _rebalanceSize = oldSize + 1; + emit RebalanceTriggered( + oldSize, + day, + navSum, + navB, + navROrZero, + 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); + } else { + // Lower rebalance + splitRatio = 0; + } } function _updateTotalDebt(uint256 newTotalDebt) private { diff --git a/contracts/swap/RookStableSwapV2.sol b/contracts/swap/RookStableSwapV2.sol index 68a1a44a..3c263bd6 100644 --- a/contracts/swap/RookStableSwapV2.sol +++ b/contracts/swap/RookStableSwapV2.sol @@ -151,7 +151,7 @@ contract RookStableSwapV2 is StableSwapV2, ITrancheIndexV2 { function getOraclePrice() public view override returns (uint256) { uint256 price = fund.twapOracle().getLatest(); // wstETH / stETH - (, uint256 navB, uint256 navR) = fund.extrapolateNav(price); // ETH / ROOK + (, , uint256 navR) = fund.extrapolateNav(price); // ETH / ROOK return navR.multiplyDecimal(price); // wstETH / ROOK } } From 2e9db1002f2ae76a39ab367a103785e2fc3b6535 Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Thu, 30 Nov 2023 07:23:05 +0800 Subject: [PATCH 06/29] add back fee collector --- contracts/fund/FundV5.sol | 149 +++++++---------- contracts/fund/PrimaryMarketV5.sol | 35 ++-- contracts/interfaces/IFundV5.sol | 188 +++++++++++++++++++++- contracts/interfaces/IPrimaryMarketV5.sol | 2 +- 4 files changed, 257 insertions(+), 117 deletions(-) diff --git a/contracts/fund/FundV5.sol b/contracts/fund/FundV5.sol index 2fdd5bc4..ee5348de 100644 --- a/contracts/fund/FundV5.sol +++ b/contracts/fund/FundV5.sol @@ -33,11 +33,12 @@ contract FundV5 is using SafeDecimalMath for uint256; using SafeERC20 for IERC20; - event LossReported(uint256 loss); event TwapOracleUpdated(address newTwapOracle); + 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 INTEREST_RATE = 8219178082191780; // 3% yearly @@ -53,6 +54,9 @@ contract FundV5 is /// @notice TwapOracle address for the underlying asset. ITwapOracleV2 public override twapOracle; + /// @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). @@ -70,6 +74,8 @@ contract FundV5 is 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 @@ -103,18 +109,6 @@ contract FundV5 is /// @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; - /// @dev Amount of redemption underlying that the fund owes the primary market uint256 private _totalDebt; @@ -126,6 +120,7 @@ contract FundV5 is address tokenR; address primaryMarket; address twapOracle; + address feeCollector; } constructor( @@ -133,25 +128,20 @@ contract FundV5 is ) public Ownable() - FundRolesV2( - params.tokenQ, - params.tokenB, - params.tokenR, - params.primaryMarket, - address(0) - ) + FundRolesV2(params.tokenQ, params.tokenB, params.tokenR, params.primaryMarket, address(0)) { tokenUnderlying = params.tokenUnderlying; require(params.underlyingDecimals <= 18, "Underlying decimals larger than 18"); underlyingDecimalMultiplier = 10 ** (18 - params.underlyingDecimals); _updateTwapOracle(params.twapOracle); + _updateFeeCollector(params.feeCollector); _updateActivityDelayTime(30 minutes); } function initialize( uint256 newSplitRatio, uint256 lastNavB, - uint256 lastNavR, + uint256 lastNavR ) external onlyOwner { require(splitRatio == 0 && currentDay == 0, "Already initialized"); require(newSplitRatio != 0 && lastNavB >= UNIT, "Invalid parameters"); @@ -332,43 +322,6 @@ contract FundV5 is } } - /// @notice Return the fund's relative income in a trading day. Note that denominators - /// of the returned ratios are the latest value instead of that at the last settlement. - /// If the amount of underlying token increases from 100 to 110 and assume that there's - /// no creation/redemption or underlying price change, return value `incomeOverQ` will - /// be 1/11 rather than 1/10. - /// @param day End timestamp of a trading day - /// @return incomeOverQ The ratio of income to the fund's total value - /// @return incomeOverB The ratio of income to equivalent BISHOP total value if all QUEEN are split - function getRelativeIncome( - uint256 day - ) external view override returns (uint256 incomeOverQ, uint256 incomeOverB) { - uint256 navB = _historicalNavB[day]; - if (navB == 0) { - return (0, 0); - } - uint256 navR = _historicalNavR[day]; - if (navB == UNIT && navR == UNIT) { - return (0, 0); // Rebalance is triggered - } - uint256 lastUnderlying = historicalUnderlying[day - 1 days]; - uint256 lastEquivalentTotalB = historicalEquivalentTotalB[day - 1 days]; - if (lastUnderlying == 0 || lastEquivalentTotalB == 0) { - return (0, 0); - } - uint256 currentUnderlying = historicalUnderlying[day]; - uint256 currentEquivalentTotalB = historicalEquivalentTotalB[day]; - if (currentUnderlying == 0 || currentEquivalentTotalB == 0) { - return (0, 0); - } - { - uint256 ratio = ((lastUnderlying * currentEquivalentTotalB) / currentUnderlying) - .divideDecimal(lastEquivalentTotalB); - incomeOverQ = ratio > 1e18 ? 0 : 1e18 - ratio; - } - incomeOverB = incomeOverQ.mul(navB + navR) / navB; - } - /// @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. @@ -696,7 +649,7 @@ contract FundV5 is /// 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 { + function settle() external nonReentrant onlyNotFrozen { uint256 day = currentDay; require(day != 0, "Not initialized"); require(block.timestamp >= day + 365 days, "The current trading year not end yet"); @@ -723,8 +676,6 @@ contract FundV5 is equivalentTotalB = getEquivalentTotalB(); fundActivityStartTime = day + activityDelayTimeAfterRebalance; - historicalEquivalentTotalB[day] = equivalentTotalB; - historicalUnderlying[day] = underlying; _historicalNavB[day] = navB; _historicalNavR[day] = navR; currentDay = day + 365 days; @@ -775,6 +726,15 @@ contract FundV5 is _updateTwapOracle(newTwapOracle); } + 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, @@ -802,46 +762,44 @@ contract FundV5 is uint256 navROrZero, uint256 newSplitRatio ) private { + Rebalance memory rebalance; if (navROrZero > navB) { // Upper rebalance - Rebalance memory rebalance = Rebalance({ + rebalance = Rebalance({ ratioB2Q: (navB - UNIT).divideDecimal(newSplitRatio) / 2, ratioR2Q: (navROrZero - UNIT).divideDecimal(newSplitRatio) / 2, ratioBR: UNIT, timestamp: block.timestamp }); - uint256 oldSize = _rebalanceSize; - splitRatio = newSplitRatio; - _historicalSplitRatio[oldSize + 1] = newSplitRatio; - emit SplitRatioUpdated(newSplitRatio); - _rebalances[oldSize] = rebalance; - _rebalanceSize = oldSize + 1; - emit RebalanceTriggered( - oldSize, - day, - navSum, - navB, - navROrZero, - 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); - } else { - // Lower rebalance - splitRatio = 0; } + uint256 oldSize = _rebalanceSize; + splitRatio = newSplitRatio; + _historicalSplitRatio[oldSize + 1] = newSplitRatio; + emit SplitRatioUpdated(newSplitRatio); + _rebalances[oldSize] = rebalance; + _rebalanceSize = oldSize + 1; + emit RebalanceTriggered( + oldSize, + day, + navSum, + navB, + navROrZero, + 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); } function _updateTotalDebt(uint256 newTotalDebt) private { @@ -937,4 +895,9 @@ contract FundV5 is require(_rebalanceSize == version, "Only current version"); _; } + + modifier onlyNotFrozen() { + require(!frozen, "Frozen"); + _; + } } diff --git a/contracts/fund/PrimaryMarketV5.sol b/contracts/fund/PrimaryMarketV5.sol index 63d8161b..2e4d1efd 100644 --- a/contracts/fund/PrimaryMarketV5.sol +++ b/contracts/fund/PrimaryMarketV5.sol @@ -209,18 +209,26 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, /// @return inQ QUEEN amount that should be split function getSplitForB(uint256 minOutB) external view override returns (uint256 inQ) { uint256 splitRatio = IFundV5(fund).splitRatio(); - return minOutB.mul(1e18).add(splitRatio.sub(1)).div(splitRatio); + return minOutB.divideDecimal(weightB).add(splitRatio.sub(1)).div(splitRatio); } /// @notice Calculate the result of a merge. - /// @param inB Spent BISHOP amount, which is also spent ROOK amount + /// @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 outQ, uint256 feeQ) { + 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 Calculate the amount of BISHOP and ROOK that can be merged into at least @@ -334,14 +342,6 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, IFundForPrimaryMarketV4(fund).primaryMarketMint(TRANCHE_Q, recipient, outQ, version); _tokenUnderlying.safeTransfer(fund, underlying); emit Created(recipient, underlying, outQ); - - // Call an optional hook in the strategy and ignore errors. - (bool success, ) = IFundV5(fund).strategy().call( - abi.encodeWithSignature("onPrimaryMarketCreate()") - ); - if (!success) { - // ignore - } } /// @notice Redeem QUEEN to get underlying tokens back. Revert if there are still some @@ -395,14 +395,6 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, require(underlying <= _tokenUnderlying.balanceOf(fund), "Not enough underlying in fund"); IFundForPrimaryMarketV4(fund).primaryMarketTransferUnderlying(recipient, underlying, feeQ); emit Redeemed(recipient, inQ, underlying, feeQ); - - // Call an optional hook in the strategy and ignore errors. - (bool success, ) = IFundV5(fund).strategy().call( - abi.encodeWithSignature("onPrimaryMarketRedeem()") - ); - if (!success) { - // ignore - } } /// @notice Redeem QUEEN and wait in the redemption queue. Redeemed underlying tokens will @@ -545,10 +537,11 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, uint256 inB, uint256 version ) external override returns (uint256 outQ) { + uint256 inR; uint256 feeQ; - (outQ, feeQ) = getMerge(inB); + (inR, outQ, feeQ) = getMerge(inB); IFundForPrimaryMarketV4(fund).primaryMarketBurn(TRANCHE_B, msg.sender, inB, version); - IFundForPrimaryMarketV4(fund).primaryMarketBurn(TRANCHE_R, 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); diff --git a/contracts/interfaces/IFundV5.sol b/contracts/interfaces/IFundV5.sol index c4e06c78..611ccc2c 100644 --- a/contracts/interfaces/IFundV5.sol +++ b/contracts/interfaces/IFundV5.sol @@ -1,8 +1,192 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.6.10 <0.8.0; +pragma experimental ABIEncoderV2; -import "./IFundV4.sol"; +import "./ITwapOracleV2.sol"; -interface IFundV5 is IFundV4 { +interface IFundV5 { function WEIGHT_B() external view returns (uint256); + + function frozen() external view returns (bool); + + /// @notice A linear transformation matrix that represents a rebalance. + /// + /// ``` + /// [ 1 0 0 ] + /// R = [ ratioB2Q ratioBR 0 ] + /// [ ratioR2Q 0 ratioBR ] + /// ``` + /// + /// Amounts of the three tranches `q`, `b` and `r` can be rebalanced by multiplying the matrix: + /// + /// ``` + /// [ q', b', r' ] = [ q, b, r ] * R + /// ``` + struct Rebalance { + uint256 ratioB2Q; + uint256 ratioR2Q; + uint256 ratioBR; + uint256 timestamp; + } + + function tokenUnderlying() external view returns (address); + + function tokenQ() external view returns (address); + + function tokenB() external view returns (address); + + function tokenR() external view returns (address); + + function tokenShare(uint256 tranche) external view returns (address); + + function primaryMarket() external view returns (address); + + function primaryMarketUpdateProposal() external view returns (address, uint256); + + function underlyingDecimalMultiplier() external view returns (uint256); + + function twapOracle() external view returns (ITwapOracleV2); + + function feeCollector() external view returns (address); + + function endOfDay(uint256 timestamp) external pure returns (uint256); + + function trancheTotalSupply(uint256 tranche) external view returns (uint256); + + function trancheBalanceOf(uint256 tranche, address account) external view returns (uint256); + + function trancheAllBalanceOf(address account) external view returns (uint256, uint256, uint256); + + function trancheBalanceVersion(address account) external view returns (uint256); + + function trancheAllowance( + uint256 tranche, + address owner, + address spender + ) external view returns (uint256); + + function trancheAllowanceVersion( + address owner, + address spender + ) external view returns (uint256); + + function trancheTransfer( + uint256 tranche, + address recipient, + uint256 amount, + uint256 version + ) external; + + function trancheTransferFrom( + uint256 tranche, + address sender, + address recipient, + uint256 amount, + uint256 version + ) external; + + function trancheApprove( + uint256 tranche, + address spender, + uint256 amount, + uint256 version + ) external; + + function getRebalanceSize() external view returns (uint256); + + function getRebalance(uint256 index) external view returns (Rebalance memory); + + function getRebalanceTimestamp(uint256 index) external view returns (uint256); + + function currentDay() external view returns (uint256); + + function splitRatio() external view returns (uint256); + + function historicalSplitRatio(uint256 version) external view returns (uint256); + + function fundActivityStartTime() external view returns (uint256); + + function isFundActive(uint256 timestamp) external view returns (bool); + + function getEquivalentTotalB() external view returns (uint256); + + function getEquivalentTotalQ() external view returns (uint256); + + function historicalNavs(uint256 timestamp) external view returns (uint256 navB, uint256 navR); + + function extrapolateNav(uint256 price) external view returns (uint256, uint256, uint256); + + function doRebalance( + uint256 amountQ, + uint256 amountB, + uint256 amountR, + uint256 index + ) external view returns (uint256 newAmountQ, uint256 newAmountB, uint256 newAmountR); + + function batchRebalance( + uint256 amountQ, + uint256 amountB, + uint256 amountR, + uint256 fromIndex, + uint256 toIndex + ) external view returns (uint256 newAmountQ, uint256 newAmountB, uint256 newAmountR); + + function refreshBalance(address account, uint256 targetVersion) external; + + function refreshAllowance(address owner, address spender, uint256 targetVersion) external; + + function shareTransfer(address sender, address recipient, uint256 amount) external; + + function shareTransferFrom( + address spender, + address sender, + address recipient, + uint256 amount + ) external returns (uint256 newAllowance); + + function shareIncreaseAllowance( + address sender, + address spender, + uint256 addedValue + ) external returns (uint256 newAllowance); + + function shareDecreaseAllowance( + address sender, + address spender, + uint256 subtractedValue + ) external returns (uint256 newAllowance); + + function shareApprove(address owner, address spender, uint256 amount) external; + + function getTotalUnderlying() external view returns (uint256); + + function getTotalDebt() external view returns (uint256); + + event RebalanceTriggered( + uint256 indexed index, + uint256 indexed day, + uint256 navSum, + uint256 navB, + uint256 navROrZero, + uint256 ratioB2Q, + uint256 ratioR2Q, + uint256 ratioBR + ); + event Settled(uint256 indexed day, uint256 navB, uint256 navR, uint256 interestRate); + event InterestRateUpdated(uint256 baseInterestRate, uint256 floatingInterestRate); + event BalancesRebalanced( + address indexed account, + uint256 version, + uint256 balanceQ, + uint256 balanceB, + uint256 balanceR + ); + event AllowancesRebalanced( + address indexed owner, + address indexed spender, + uint256 version, + uint256 allowanceQ, + uint256 allowanceB, + uint256 allowanceR + ); } diff --git a/contracts/interfaces/IPrimaryMarketV5.sol b/contracts/interfaces/IPrimaryMarketV5.sol index 0be9425d..f6281c2b 100644 --- a/contracts/interfaces/IPrimaryMarketV5.sol +++ b/contracts/interfaces/IPrimaryMarketV5.sol @@ -16,7 +16,7 @@ interface IPrimaryMarketV5 { function getSplitForB(uint256 minOutB) external view returns (uint256 inQ); - function getMerge(uint256 inB) external view returns (uint256 outQ, uint256 feeQ); + function getMerge(uint256 inB) external view returns (uint256 inR, uint256 outQ, uint256 feeQ); function getMergeForQ(uint256 minOutQ) external view returns (uint256 inB); From 38beb71dd494e90eaaf72109c98c581b2c307572 Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Wed, 6 Dec 2023 09:19:25 +0800 Subject: [PATCH 07/29] fix settle day check; add frozen switch --- contracts/fund/FundV5.sol | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/contracts/fund/FundV5.sol b/contracts/fund/FundV5.sol index ee5348de..388fb885 100644 --- a/contracts/fund/FundV5.sol +++ b/contracts/fund/FundV5.sol @@ -292,7 +292,7 @@ contract FundV5 is function extrapolateNav( uint256 price ) external view override returns (uint256 navSum, uint256 navB, uint256 navROrZero) { - uint256 settledDay = currentDay - 1 days; + uint256 settledDay = currentDay - 365 days; uint256 underlying = getTotalUnderlying(); return _extrapolateNav(block.timestamp, settledDay, price, getEquivalentTotalB(), underlying); @@ -652,7 +652,7 @@ contract FundV5 is function settle() external nonReentrant onlyNotFrozen { uint256 day = currentDay; require(day != 0, "Not initialized"); - require(block.timestamp >= day + 365 days, "The current trading year not end yet"); + 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"); @@ -669,6 +669,13 @@ contract FundV5 is underlying ); + // When the NAV of ROOK is zero, freeze and dissolve the fund + if (navR == 0) { + frozen = true; + emit Frozen(); + return; + } + uint256 newSplitRatio = splitRatio.multiplyDecimal(navSum) / 2; _triggerRebalance(day, navSum, navB, navR, newSplitRatio); navB = UNIT; From 18ed12939edccae01da0cc8da542cad1f8355c4e Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Tue, 5 Dec 2023 10:34:44 +0800 Subject: [PATCH 08/29] Remove obsolete comments --- contracts/oracle/WstETHPriceOracle.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/oracle/WstETHPriceOracle.sol b/contracts/oracle/WstETHPriceOracle.sol index f52fc447..6abf63bb 100644 --- a/contracts/oracle/WstETHPriceOracle.sol +++ b/contracts/oracle/WstETHPriceOracle.sol @@ -10,7 +10,6 @@ interface IWstETH { /// @title wstETH Price oracle /// @author Tranchess contract WstETHPriceOracle is ITwapOracleV2 { - /// @notice Chainlink aggregator used as the data source. IWstETH public immutable wstETH; constructor(address wstETH_) public { From b0d5c218625ac43fa9bec0bdcb6f94716764d854 Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Wed, 6 Dec 2023 15:31:45 +0800 Subject: [PATCH 09/29] Update equivalent B/Q --- contracts/fund/FundV5.sol | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/contracts/fund/FundV5.sol b/contracts/fund/FundV5.sol index 388fb885..0f7b7a5f 100644 --- a/contracts/fund/FundV5.sol +++ b/contracts/fund/FundV5.sol @@ -227,17 +227,15 @@ contract FundV5 is /// @notice Equivalent BISHOP supply, as if all QUEEN are split. function getEquivalentTotalB() public view override returns (uint256) { return - _totalSupplies[TRANCHE_Q].multiplyDecimal(splitRatio.mul(WEIGHT_B)).add( - _totalSupplies[TRANCHE_B] - ); + _totalSupplies[TRANCHE_Q] + .multiplyDecimal(splitRatio) + .add(_totalSupplies[TRANCHE_R]) + .mul(WEIGHT_B); } /// @notice Equivalent QUEEN supply, as if all BISHOP and ROOK are merged. function getEquivalentTotalQ() public view override returns (uint256) { - return - _totalSupplies[TRANCHE_B].divideDecimal(splitRatio.mul(WEIGHT_B)).add( - _totalSupplies[TRANCHE_Q] - ); + return _totalSupplies[TRANCHE_R].divideDecimal(splitRatio).add(_totalSupplies[TRANCHE_Q]); } /// @notice Return the rebalance matrix at a given index. A zero struct is returned From a403117e6b16c3c8c10e1047c8736aab5dd20b75 Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Sun, 10 Dec 2023 14:02:03 +0800 Subject: [PATCH 10/29] Add strategy back --- contracts/fund/FundV5.sol | 73 ++++++++++++++++++++++++++++++-- contracts/interfaces/IFundV5.sol | 6 +++ 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/contracts/fund/FundV5.sol b/contracts/fund/FundV5.sol index 0f7b7a5f..d950a6ef 100644 --- a/contracts/fund/FundV5.sol +++ b/contracts/fund/FundV5.sol @@ -14,6 +14,7 @@ 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/IVotingEscrow.sol"; @@ -23,6 +24,7 @@ import "./FundRolesV2.sol"; contract FundV5 is IFundV5, IFundForPrimaryMarketV4, + IFundForStrategyV2, Ownable, ReentrancyGuard, FundRolesV2, @@ -33,6 +35,8 @@ contract FundV5 is 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 FeeCollectorUpdated(address newFeeCollector); event ActivityDelayTimeUpdated(uint256 delayTime); @@ -112,6 +116,8 @@ contract FundV5 is /// @dev Amount of redemption underlying that the fund owes the primary market uint256 private _totalDebt; + uint256 private _strategyUnderlying; + struct ConstructorParameters { address tokenUnderlying; uint256 underlyingDecimals; @@ -119,6 +125,7 @@ contract FundV5 is address tokenB; address tokenR; address primaryMarket; + address strategy; address twapOracle; address feeCollector; } @@ -128,7 +135,13 @@ contract FundV5 is ) public Ownable() - FundRolesV2(params.tokenQ, params.tokenB, params.tokenR, params.primaryMarket, address(0)) + FundRolesV2( + params.tokenQ, + params.tokenB, + params.tokenR, + params.primaryMarket, + params.strategy + ) { tokenUnderlying = params.tokenUnderlying; require(params.underlyingDecimals <= 18, "Underlying decimals larger than 18"); @@ -141,7 +154,8 @@ contract FundV5 is function initialize( uint256 newSplitRatio, uint256 lastNavB, - uint256 lastNavR + uint256 lastNavR, + uint256 strategyUnderlying ) external onlyOwner { require(splitRatio == 0 && currentDay == 0, "Already initialized"); require(newSplitRatio != 0 && lastNavB >= UNIT, "Invalid parameters"); @@ -154,6 +168,7 @@ contract FundV5 is require(lastYearPrice != 0, "Price not available"); // required to do the first creation _historicalNavB[lastYear] = lastNavB; _historicalNavR[lastYear] = lastNavR; + _strategyUnderlying = strategyUnderlying; emit Settled(lastYear, lastNavB, lastNavR, INTEREST_RATE); fundActivityStartTime = lastYear; } @@ -207,6 +222,14 @@ contract FundV5 is 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 @@ -216,7 +239,11 @@ contract FundV5 is function getTotalUnderlying() public view override returns (uint256) { uint256 hot = IERC20(tokenUnderlying).balanceOf(address(this)); - return hot.sub(_totalDebt); + 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. @@ -688,6 +715,16 @@ contract FundV5 is emit Settled(day, navB, navR, INTEREST_RATE); } + 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, @@ -710,6 +747,27 @@ contract FundV5 is 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); } @@ -722,6 +780,15 @@ contract FundV5 is _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); diff --git a/contracts/interfaces/IFundV5.sol b/contracts/interfaces/IFundV5.sol index 611ccc2c..f382c40d 100644 --- a/contracts/interfaces/IFundV5.sol +++ b/contracts/interfaces/IFundV5.sol @@ -43,6 +43,10 @@ interface IFundV5 { function primaryMarketUpdateProposal() external view returns (address, uint256); + function strategy() external view returns (address); + + function strategyUpdateProposal() external view returns (address, uint256); + function underlyingDecimalMultiplier() external view returns (uint256); function twapOracle() external view returns (ITwapOracleV2); @@ -160,6 +164,8 @@ interface IFundV5 { function getTotalUnderlying() external view returns (uint256); + function getStrategyUnderlying() external view returns (uint256); + function getTotalDebt() external view returns (uint256); event RebalanceTriggered( From 423fb2d16a28c76922463ef6753a9d2ac83006fe Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Mon, 11 Dec 2023 10:24:30 +0800 Subject: [PATCH 11/29] Set initial settlement at the last day --- contracts/fund/FundV5.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/fund/FundV5.sol b/contracts/fund/FundV5.sol index d950a6ef..15a07b35 100644 --- a/contracts/fund/FundV5.sol +++ b/contracts/fund/FundV5.sol @@ -159,18 +159,18 @@ contract FundV5 is ) external onlyOwner { require(splitRatio == 0 && currentDay == 0, "Already initialized"); require(newSplitRatio != 0 && lastNavB >= UNIT, "Invalid parameters"); - currentDay = endOfDay(block.timestamp); splitRatio = newSplitRatio; _historicalSplitRatio[0] = newSplitRatio; emit SplitRatioUpdated(newSplitRatio); - uint256 lastYear = currentDay - 365 days; - uint256 lastYearPrice = twapOracle.getTwap(lastYear); - require(lastYearPrice != 0, "Price not available"); // required to do the first creation - _historicalNavB[lastYear] = lastNavB; - _historicalNavR[lastYear] = lastNavR; + uint256 lastDay = endOfDay(block.timestamp) - 1 days; + currentDay = lastDay + 365 days; + 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; - emit Settled(lastYear, lastNavB, lastNavR, INTEREST_RATE); - fundActivityStartTime = lastYear; + emit Settled(lastDay, lastNavB, lastNavR, INTEREST_RATE); + fundActivityStartTime = lastDay; } /// @notice UTC time of a day when the fund settles. From 5aab2f16f01542b25ebe9a004ec845e1162f9d4e Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Mon, 11 Dec 2023 15:22:21 +0800 Subject: [PATCH 12/29] Add interest rate ballot back --- contracts/fund/FundV5.sol | 41 +++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/contracts/fund/FundV5.sol b/contracts/fund/FundV5.sol index 15a07b35..f2dfc6ee 100644 --- a/contracts/fund/FundV5.sol +++ b/contracts/fund/FundV5.sol @@ -17,6 +17,7 @@ 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"; @@ -38,6 +39,7 @@ contract FundV5 is 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); @@ -45,7 +47,7 @@ contract FundV5 is event Frozen(); uint256 private constant UNIT = 1e18; - uint256 private constant INTEREST_RATE = 8219178082191780; // 3% yearly + uint256 private constant MAX_INTEREST_RATE = 0.2e18; // 20% daily uint256 public constant override WEIGHT_B = 9; @@ -58,6 +60,9 @@ contract FundV5 is /// @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; @@ -113,6 +118,12 @@ contract FundV5 is /// @dev Mapping of trading day => NAV of ROOK. mapping(uint256 => uint256) private _historicalNavR; + /// @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; @@ -127,6 +138,7 @@ contract FundV5 is address primaryMarket; address strategy; address twapOracle; + address aprOracle; address feeCollector; } @@ -147,6 +159,7 @@ contract FundV5 is 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); } @@ -169,7 +182,9 @@ contract FundV5 is _historicalNavB[lastDay] = lastNavB; _historicalNavR[lastDay] = lastNavR; _strategyUnderlying = strategyUnderlying; - emit Settled(lastDay, lastNavB, lastNavR, INTEREST_RATE); + uint256 lastInterestRate = _updateInterestRate(lastDay); + historicalInterestRate[lastDay] = lastInterestRate; + emit Settled(lastDay, lastNavB, lastNavR, lastInterestRate); fundActivityStartTime = lastDay; } @@ -334,7 +349,7 @@ contract FundV5 is if (equivalentTotalB > 0) { navSum = price.mul(underlying.mul(underlyingDecimalMultiplier)).div(equivalentTotalB); navB = navB.multiplyDecimal( - INTEREST_RATE.mul(timestamp - settledDay).div(1 days).add(UNIT) + historicalInterestRate[settledDay].mul(timestamp - settledDay).div(1 days).add(UNIT) ); navROrZero = navSum.divideDecimal(splitRatio) >= navB.mul(WEIGHT_B) @@ -710,9 +725,11 @@ contract FundV5 is _historicalNavB[day] = navB; _historicalNavR[day] = navR; + uint256 interestRate = _updateInterestRate(day); + historicalInterestRate[day] = interestRate; currentDay = day + 365 days; - emit Settled(day, navB, navR, INTEREST_RATE); + emit Settled(day, navB, navR, interestRate); } function transferToStrategy(uint256 amount) external override onlyStrategy { @@ -798,6 +815,15 @@ contract FundV5 is _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); @@ -874,6 +900,13 @@ contract FundV5 is _refreshBalance(address(this), oldSize + 1); } + 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); From ae9b05d0a01f6e83b27ecea0ecf8046ac966f0e8 Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Mon, 11 Dec 2023 15:26:18 +0800 Subject: [PATCH 13/29] Change MAX_INTEREST_RATE --- contracts/fund/FundV5.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/fund/FundV5.sol b/contracts/fund/FundV5.sol index f2dfc6ee..bf7f731c 100644 --- a/contracts/fund/FundV5.sol +++ b/contracts/fund/FundV5.sol @@ -47,7 +47,7 @@ contract FundV5 is event Frozen(); uint256 private constant UNIT = 1e18; - uint256 private constant MAX_INTEREST_RATE = 0.2e18; // 20% daily + uint256 private constant MAX_INTEREST_RATE = 0.001e18; // 0.1% daily uint256 public constant override WEIGHT_B = 9; From d26fa6a51388ea0852648242478b0b80275bce2d Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Mon, 11 Dec 2023 15:31:41 +0800 Subject: [PATCH 14/29] Restore historical equivalent B and underlying --- contracts/fund/FundV5.sol | 14 ++++++++++++++ contracts/interfaces/IFundV5.sol | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/contracts/fund/FundV5.sol b/contracts/fund/FundV5.sol index bf7f731c..531d802c 100644 --- a/contracts/fund/FundV5.sol +++ b/contracts/fund/FundV5.sol @@ -118,6 +118,18 @@ contract FundV5 is /// @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 @@ -723,6 +735,8 @@ contract FundV5 is equivalentTotalB = getEquivalentTotalB(); fundActivityStartTime = day + activityDelayTimeAfterRebalance; + historicalEquivalentTotalB[day] = equivalentTotalB; + historicalUnderlying[day] = underlying; _historicalNavB[day] = navB; _historicalNavR[day] = navR; uint256 interestRate = _updateInterestRate(day); diff --git a/contracts/interfaces/IFundV5.sol b/contracts/interfaces/IFundV5.sol index f382c40d..eb6c660c 100644 --- a/contracts/interfaces/IFundV5.sol +++ b/contracts/interfaces/IFundV5.sol @@ -116,6 +116,8 @@ interface IFundV5 { function getEquivalentTotalQ() external view returns (uint256); + function historicalEquivalentTotalB(uint256 timestamp) external view returns (uint256); + function historicalNavs(uint256 timestamp) external view returns (uint256 navB, uint256 navR); function extrapolateNav(uint256 price) external view returns (uint256, uint256, uint256); @@ -162,6 +164,8 @@ interface IFundV5 { function shareApprove(address owner, address spender, uint256 amount) external; + function historicalUnderlying(uint256 timestamp) external view returns (uint256); + function getTotalUnderlying() external view returns (uint256); function getStrategyUnderlying() external view returns (uint256); From c8da2e9f3236f9aa7119098f4cd2ec41616fea23 Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Mon, 11 Dec 2023 15:33:24 +0800 Subject: [PATCH 15/29] Make IFundV5 inherit IFundV3 --- contracts/interfaces/IFundV5.sol | 196 +------------------------------ 1 file changed, 2 insertions(+), 194 deletions(-) diff --git a/contracts/interfaces/IFundV5.sol b/contracts/interfaces/IFundV5.sol index eb6c660c..9088660c 100644 --- a/contracts/interfaces/IFundV5.sol +++ b/contracts/interfaces/IFundV5.sol @@ -1,202 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.6.10 <0.8.0; -pragma experimental ABIEncoderV2; -import "./ITwapOracleV2.sol"; +import "./IFundV3.sol"; -interface IFundV5 { +interface IFundV5 is IFundV3 { function WEIGHT_B() external view returns (uint256); function frozen() external view returns (bool); - - /// @notice A linear transformation matrix that represents a rebalance. - /// - /// ``` - /// [ 1 0 0 ] - /// R = [ ratioB2Q ratioBR 0 ] - /// [ ratioR2Q 0 ratioBR ] - /// ``` - /// - /// Amounts of the three tranches `q`, `b` and `r` can be rebalanced by multiplying the matrix: - /// - /// ``` - /// [ q', b', r' ] = [ q, b, r ] * R - /// ``` - struct Rebalance { - uint256 ratioB2Q; - uint256 ratioR2Q; - uint256 ratioBR; - uint256 timestamp; - } - - function tokenUnderlying() external view returns (address); - - function tokenQ() external view returns (address); - - function tokenB() external view returns (address); - - function tokenR() external view returns (address); - - function tokenShare(uint256 tranche) external view returns (address); - - function primaryMarket() external view returns (address); - - function primaryMarketUpdateProposal() external view returns (address, uint256); - - function strategy() external view returns (address); - - function strategyUpdateProposal() external view returns (address, uint256); - - function underlyingDecimalMultiplier() external view returns (uint256); - - function twapOracle() external view returns (ITwapOracleV2); - - function feeCollector() external view returns (address); - - function endOfDay(uint256 timestamp) external pure returns (uint256); - - function trancheTotalSupply(uint256 tranche) external view returns (uint256); - - function trancheBalanceOf(uint256 tranche, address account) external view returns (uint256); - - function trancheAllBalanceOf(address account) external view returns (uint256, uint256, uint256); - - function trancheBalanceVersion(address account) external view returns (uint256); - - function trancheAllowance( - uint256 tranche, - address owner, - address spender - ) external view returns (uint256); - - function trancheAllowanceVersion( - address owner, - address spender - ) external view returns (uint256); - - function trancheTransfer( - uint256 tranche, - address recipient, - uint256 amount, - uint256 version - ) external; - - function trancheTransferFrom( - uint256 tranche, - address sender, - address recipient, - uint256 amount, - uint256 version - ) external; - - function trancheApprove( - uint256 tranche, - address spender, - uint256 amount, - uint256 version - ) external; - - function getRebalanceSize() external view returns (uint256); - - function getRebalance(uint256 index) external view returns (Rebalance memory); - - function getRebalanceTimestamp(uint256 index) external view returns (uint256); - - function currentDay() external view returns (uint256); - - function splitRatio() external view returns (uint256); - - function historicalSplitRatio(uint256 version) external view returns (uint256); - - function fundActivityStartTime() external view returns (uint256); - - function isFundActive(uint256 timestamp) external view returns (bool); - - function getEquivalentTotalB() external view returns (uint256); - - function getEquivalentTotalQ() external view returns (uint256); - - function historicalEquivalentTotalB(uint256 timestamp) external view returns (uint256); - - function historicalNavs(uint256 timestamp) external view returns (uint256 navB, uint256 navR); - - function extrapolateNav(uint256 price) external view returns (uint256, uint256, uint256); - - function doRebalance( - uint256 amountQ, - uint256 amountB, - uint256 amountR, - uint256 index - ) external view returns (uint256 newAmountQ, uint256 newAmountB, uint256 newAmountR); - - function batchRebalance( - uint256 amountQ, - uint256 amountB, - uint256 amountR, - uint256 fromIndex, - uint256 toIndex - ) external view returns (uint256 newAmountQ, uint256 newAmountB, uint256 newAmountR); - - function refreshBalance(address account, uint256 targetVersion) external; - - function refreshAllowance(address owner, address spender, uint256 targetVersion) external; - - function shareTransfer(address sender, address recipient, uint256 amount) external; - - function shareTransferFrom( - address spender, - address sender, - address recipient, - uint256 amount - ) external returns (uint256 newAllowance); - - function shareIncreaseAllowance( - address sender, - address spender, - uint256 addedValue - ) external returns (uint256 newAllowance); - - function shareDecreaseAllowance( - address sender, - address spender, - uint256 subtractedValue - ) external returns (uint256 newAllowance); - - function shareApprove(address owner, address spender, uint256 amount) external; - - function historicalUnderlying(uint256 timestamp) external view returns (uint256); - - function getTotalUnderlying() external view returns (uint256); - - function getStrategyUnderlying() external view returns (uint256); - - function getTotalDebt() external view returns (uint256); - - event RebalanceTriggered( - uint256 indexed index, - uint256 indexed day, - uint256 navSum, - uint256 navB, - uint256 navROrZero, - uint256 ratioB2Q, - uint256 ratioR2Q, - uint256 ratioBR - ); - event Settled(uint256 indexed day, uint256 navB, uint256 navR, uint256 interestRate); - event InterestRateUpdated(uint256 baseInterestRate, uint256 floatingInterestRate); - event BalancesRebalanced( - address indexed account, - uint256 version, - uint256 balanceQ, - uint256 balanceB, - uint256 balanceR - ); - event AllowancesRebalanced( - address indexed owner, - address indexed spender, - uint256 version, - uint256 allowanceQ, - uint256 allowanceB, - uint256 allowanceR - ); } From 262116d7078a334e68ca6e023d11dac3d88a3aeb Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Wed, 13 Dec 2023 11:27:10 +0800 Subject: [PATCH 16/29] Fix rebalance --- contracts/fund/FundV5.sol | 93 ++++++++++++++++++-------------- contracts/interfaces/IFundV5.sol | 2 + 2 files changed, 54 insertions(+), 41 deletions(-) diff --git a/contracts/fund/FundV5.sol b/contracts/fund/FundV5.sol index 531d802c..63e173a1 100644 --- a/contracts/fund/FundV5.sol +++ b/contracts/fund/FundV5.sol @@ -278,13 +278,14 @@ contract FundV5 is 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 - _totalSupplies[TRANCHE_Q] - .multiplyDecimal(splitRatio) - .add(_totalSupplies[TRANCHE_R]) - .mul(WEIGHT_B); + return getEquivalentTotalR().mul(WEIGHT_B); } /// @notice Equivalent QUEEN supply, as if all BISHOP and ROOK are merged. @@ -332,13 +333,13 @@ contract FundV5 is } /// @notice Estimate the current NAV of all tranches, considering underlying price change, - /// and accrued interest since the previous settlement. + /// 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 the estimated NAV of BISHOP and ROOK + /// @return navSum Sum of navB * WEIGHT_B 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( @@ -347,26 +348,23 @@ contract FundV5 is uint256 settledDay = currentDay - 365 days; uint256 underlying = getTotalUnderlying(); return - _extrapolateNav(block.timestamp, settledDay, price, getEquivalentTotalB(), underlying); + _extrapolateNav(block.timestamp, settledDay, price, getEquivalentTotalR(), underlying); } function _extrapolateNav( uint256 timestamp, uint256 settledDay, uint256 price, - uint256 equivalentTotalB, + uint256 equivalentTotalR, uint256 underlying ) private view returns (uint256 navSum, uint256 navB, uint256 navROrZero) { navB = _historicalNavB[settledDay]; - if (equivalentTotalB > 0) { - navSum = price.mul(underlying.mul(underlyingDecimalMultiplier)).div(equivalentTotalB); + 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.divideDecimal(splitRatio) >= navB.mul(WEIGHT_B) - ? navSum.divideDecimal(splitRatio) - navB.mul(WEIGHT_B) - : 0; + navROrZero = navSum >= navB.mul(WEIGHT_B) ? navSum - navB * WEIGHT_B : 0; } else { // If the fund is empty, use NAV in the last day navROrZero = _historicalNavR[settledDay]; @@ -711,31 +709,23 @@ contract FundV5 is IPrimaryMarketV3(_primaryMarket).settle(day); // Calculate NAV - uint256 equivalentTotalB = getEquivalentTotalB(); uint256 underlying = getTotalUnderlying(); (uint256 navSum, uint256 navB, uint256 navR) = _extrapolateNav( day, day - 365 days, price, - equivalentTotalB, + getEquivalentTotalR(), underlying ); + require(navR > 0, "To be frozen"); - // When the NAV of ROOK is zero, freeze and dissolve the fund - if (navR == 0) { - frozen = true; - emit Frozen(); - return; - } - - uint256 newSplitRatio = splitRatio.multiplyDecimal(navSum) / 2; + uint256 newSplitRatio = splitRatio.multiplyDecimal(navSum) / (WEIGHT_B + 1); _triggerRebalance(day, navSum, navB, navR, newSplitRatio); navB = UNIT; navR = UNIT; - equivalentTotalB = getEquivalentTotalB(); fundActivityStartTime = day + activityDelayTimeAfterRebalance; - historicalEquivalentTotalB[day] = equivalentTotalB; + historicalEquivalentTotalB[day] = getEquivalentTotalB(); historicalUnderlying[day] = underlying; _historicalNavB[day] = navB; _historicalNavR[day] = navR; @@ -863,27 +853,17 @@ contract FundV5 is /// @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 navSum Sum of BISHOP and ROOK's NAV /// @param navB BISHOP's NAV before this rebalance - /// @param navROrZero ROOK's NAV before this rebalance or zero if the NAV is negative + /// @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 navROrZero, + uint256 navR, uint256 newSplitRatio ) private { - Rebalance memory rebalance; - if (navROrZero > navB) { - // Upper rebalance - rebalance = Rebalance({ - ratioB2Q: (navB - UNIT).divideDecimal(newSplitRatio) / 2, - ratioR2Q: (navROrZero - UNIT).divideDecimal(newSplitRatio) / 2, - ratioBR: UNIT, - timestamp: block.timestamp - }); - } + Rebalance memory rebalance = _calculateRebalance(navB, navR, newSplitRatio); uint256 oldSize = _rebalanceSize; splitRatio = newSplitRatio; _historicalSplitRatio[oldSize + 1] = newSplitRatio; @@ -895,7 +875,7 @@ contract FundV5 is day, navSum, navB, - navROrZero, + navR, rebalance.ratioB2Q, rebalance.ratioR2Q, rebalance.ratioBR @@ -914,6 +894,37 @@ contract FundV5 is _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) / (WEIGHT_B + 1); + ratioR2Q = 0; + } else { + ratioBR = navB; + ratioB2Q = 0; + ratioR2Q = (navR - navB).divideDecimal(newSplitRatio) / (WEIGHT_B + 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); diff --git a/contracts/interfaces/IFundV5.sol b/contracts/interfaces/IFundV5.sol index 9088660c..15e633cf 100644 --- a/contracts/interfaces/IFundV5.sol +++ b/contracts/interfaces/IFundV5.sol @@ -6,5 +6,7 @@ import "./IFundV3.sol"; interface IFundV5 is IFundV3 { function WEIGHT_B() external view returns (uint256); + function getEquivalentTotalR() external view returns (uint256); + function frozen() external view returns (bool); } From 1088cf33ef88fb6a4b8ae2ddb2d0ec740c6f6937 Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Wed, 13 Dec 2023 21:20:28 +0800 Subject: [PATCH 17/29] add fixed apr oracle --- contracts/oracle/AprOracleV3.sol | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 contracts/oracle/AprOracleV3.sol diff --git a/contracts/oracle/AprOracleV3.sol b/contracts/oracle/AprOracleV3.sol new file mode 100644 index 00000000..61d098ae --- /dev/null +++ b/contracts/oracle/AprOracleV3.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +import "../interfaces/IAprOracle.sol"; + +contract AprOracleV3 is IAprOracle { + uint256 public immutable dailyRate; + + constructor(uint256 dailyRate_) public { + dailyRate = dailyRate_; + } + + function capture() external override returns (uint256) { + return dailyRate; + } +} From 4da90a9b0698ab20cc46bbd12aa7732c3ce92949 Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Wed, 13 Dec 2023 21:23:42 +0800 Subject: [PATCH 18/29] add queen fee converter --- contracts/governance/FeeConverter.sol | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 contracts/governance/FeeConverter.sol 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); + } +} From 7c062aa1c552a544db3bffe3277763e4ec405de3 Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Wed, 13 Dec 2023 21:36:00 +0800 Subject: [PATCH 19/29] add WstETH PrimaryMarket Router --- contracts/fund/WstETHPrimaryMarketRouter.sol | 117 ++++++++++++++++++ contracts/interfaces/IWstETH.sol | 12 ++ .../interfaces/IWstETHPrimaryMarketRouter.sol | 23 ++++ contracts/oracle/WstETHPriceOracle.sol | 5 +- 4 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 contracts/fund/WstETHPrimaryMarketRouter.sol create mode 100644 contracts/interfaces/IWstETH.sol create mode 100644 contracts/interfaces/IWstETHPrimaryMarketRouter.sol diff --git a/contracts/fund/WstETHPrimaryMarketRouter.sol b/contracts/fund/WstETHPrimaryMarketRouter.sol new file mode 100644 index 00000000..122c5a79 --- /dev/null +++ b/contracts/fund/WstETHPrimaryMarketRouter.sol @@ -0,0 +1,117 @@ +// 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/IWstETHPrimaryMarketRouter.sol"; +import "../interfaces/IPrimaryMarketV5.sol"; +import "../interfaces/IWstETH.sol"; +import "../interfaces/ITrancheIndexV2.sol"; + +contract WstETHPrimaryMarketRouter is IWstETHPrimaryMarketRouter, 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(); + } + + /// @dev Get redemption with StableSwap getQuoteOut interface. + function getQuoteOut(uint256 baseIn) external view override returns (uint256 quoteOut) { + (quoteOut, ) = primaryMarket.getRedemption(baseIn); + } + + /// @dev Get creation for QUEEN with StableSwap getQuoteIn interface. + function getQuoteIn(uint256 baseOut) external view override returns (uint256 quoteIn) { + quoteIn = primaryMarket.getCreationForQ(baseOut); + } + + /// @dev Get creation with StableSwap getBaseOut interface. + function getBaseOut(uint256 quoteIn) external view override returns (uint256 baseOut) { + baseOut = primaryMarket.getCreation(quoteIn); + } + + /// @dev Get redemption for underlying with StableSwap getBaseIn interface. + function getBaseIn(uint256 quoteOut) external view override returns (uint256 baseIn) { + baseIn = primaryMarket.getRedemptionForUnderlying(quoteOut); + } + + /// @dev Create QUEEN with StableSwap buy interface. + /// Underlying should have already been sent to this contract + function buy( + uint256 version, + uint256 baseOut, + address recipient, + bytes calldata + ) external override returns (uint256 realBaseOut) { + uint256 routerQuoteBalance = IERC20(_wstETH).balanceOf(address(this)); + IERC20(_wstETH).safeTransfer(address(primaryMarket), routerQuoteBalance); + realBaseOut = primaryMarket.create(recipient, baseOut, version); + } + + /// @dev Redeem QUEEN with StableSwap sell interface. + /// QUEEN should have already been sent to this contract + function sell( + uint256 version, + uint256 quoteOut, + address recipient, + bytes calldata + ) external override returns (uint256 realQuoteOut) { + uint256 routerBaseBalance = fund.trancheBalanceOf(TRANCHE_Q, address(this)); + realQuoteOut = primaryMarket.redeem(recipient, routerBaseBalance, quoteOut, version); + } + + function create( + address recipient, + bool isWrapped, + uint256 underlying, + uint256 minOutQ, + uint256 version + ) public override 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 override { + // 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/interfaces/IWstETH.sol b/contracts/interfaces/IWstETH.sol new file mode 100644 index 00000000..6cfe09ca --- /dev/null +++ b/contracts/interfaces/IWstETH.sol @@ -0,0 +1,12 @@ +// 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 wrap(uint256 _stETHAmount) external returns (uint256); + + function unwrap(uint256 _wstETHAmount) external returns (uint256); +} diff --git a/contracts/interfaces/IWstETHPrimaryMarketRouter.sol b/contracts/interfaces/IWstETHPrimaryMarketRouter.sol new file mode 100644 index 00000000..bf65b1b7 --- /dev/null +++ b/contracts/interfaces/IWstETHPrimaryMarketRouter.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; +pragma experimental ABIEncoderV2; + +import "../interfaces/IFundV5.sol"; +import "../interfaces/IStableSwap.sol"; + +interface IWstETHPrimaryMarketRouter is IStableSwapCore { + function create( + address recipient, + bool isWrapped, + uint256 underlying, + uint256 minOutQ, + uint256 version + ) external returns (uint256 outQ); + + function createAndSplit( + uint256 underlying, + bool isWrapped, + uint256 minOutQ, + uint256 version + ) external; +} diff --git a/contracts/oracle/WstETHPriceOracle.sol b/contracts/oracle/WstETHPriceOracle.sol index 6abf63bb..6ddd0470 100644 --- a/contracts/oracle/WstETHPriceOracle.sol +++ b/contracts/oracle/WstETHPriceOracle.sol @@ -2,10 +2,7 @@ pragma solidity >=0.6.10 <0.8.0; import "../interfaces/ITwapOracleV2.sol"; - -interface IWstETH { - function stEthPerToken() external view returns (uint256); -} +import "../interfaces/IWstETH.sol"; /// @title wstETH Price oracle /// @author Tranchess From 7f1bd5cfe6b8a3524928c7c3e6c8ee2654cbe072 Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Wed, 13 Dec 2023 21:55:14 +0800 Subject: [PATCH 20/29] add WstETH-stETH swap as a new path for Swap Router --- contracts/fund/WstETHPrimaryMarketRouter.sol | 12 +--- contracts/interfaces/IWstETH.sol | 4 ++ contracts/swap/WstETHtoStETHSwap.sol | 62 ++++++++++++++++++++ 3 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 contracts/swap/WstETHtoStETHSwap.sol diff --git a/contracts/fund/WstETHPrimaryMarketRouter.sol b/contracts/fund/WstETHPrimaryMarketRouter.sol index 122c5a79..4ddfa2ed 100644 --- a/contracts/fund/WstETHPrimaryMarketRouter.sol +++ b/contracts/fund/WstETHPrimaryMarketRouter.sol @@ -81,19 +81,11 @@ contract WstETHPrimaryMarketRouter is IWstETHPrimaryMarketRouter, ITrancheIndexV uint256 version ) public override returns (uint256 outQ) { if (isWrapped) { - IERC20(_stETH).safeTransferFrom( - msg.sender, - address(this), - underlying - ); + 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 - ); + IERC20(_wstETH).safeTransferFrom(msg.sender, address(primaryMarket), underlying); } outQ = primaryMarket.create(recipient, minOutQ, version); diff --git a/contracts/interfaces/IWstETH.sol b/contracts/interfaces/IWstETH.sol index 6cfe09ca..a93157a2 100644 --- a/contracts/interfaces/IWstETH.sol +++ b/contracts/interfaces/IWstETH.sol @@ -6,6 +6,10 @@ interface IWstETH { 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/swap/WstETHtoStETHSwap.sol b/contracts/swap/WstETHtoStETHSwap.sol new file mode 100644 index 00000000..fb16d9a5 --- /dev/null +++ b/contracts/swap/WstETHtoStETHSwap.sol @@ -0,0 +1,62 @@ +// 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 "../utils/SafeDecimalMath.sol"; +import "../interfaces/IWstETH.sol"; + +contract WstETHtoStETHSwap { + 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 returns (uint256 quoteOut) { + quoteOut = IWstETH(wstETH).getStETHByWstETH(baseIn); + } + + function getQuoteIn(uint256 baseOut) external view returns (uint256 quoteIn) { + quoteIn = IWstETH(wstETH).getStETHByWstETH(baseOut); + } + + function getBaseOut(uint256 quoteIn) external view returns (uint256 baseOut) { + baseOut = IWstETH(wstETH).getWstETHByStETH(quoteIn); + } + + function getBaseIn(uint256 quoteOut) external view returns (uint256 baseIn) { + baseIn = IWstETH(wstETH).getWstETHByStETH(quoteOut); + } + + function buy( + uint256, + uint256, + address recipient, + bytes calldata + ) external 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 returns (uint256 realQuoteOut) { + uint256 baseIn = IERC20(wstETH).balanceOf(address(this)); + realQuoteOut = IWstETH(wstETH).unwrap(baseIn); + IERC20(stETH).safeTransfer(recipient, realQuoteOut); + } + + function getOraclePrice() public view returns (uint256) { + return IWstETH(wstETH).stEthPerToken(); + } +} From f64608ad6cb9cee33ce5274cdd119f1619b4ccd7 Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Thu, 14 Dec 2023 12:28:13 +0800 Subject: [PATCH 21/29] handle stETH when adding liquidity --- contracts/swap/SwapRouter.sol | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/contracts/swap/SwapRouter.sol b/contracts/swap/SwapRouter.sol index 18fc8591..8a1d977f 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; // Base + address public immutable stETH; // Quote + + 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); } From 6b11efd6feb41472499e72992408f01df995392b Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Wed, 13 Dec 2023 18:23:20 +0800 Subject: [PATCH 22/29] Add StableSwapV3 --- contracts/swap/BishopStableSwapV3.sol | 89 +++ contracts/swap/StableSwapV3.sol | 817 ++++++++++++++++++++++++++ 2 files changed, 906 insertions(+) create mode 100644 contracts/swap/BishopStableSwapV3.sol create mode 100644 contracts/swap/StableSwapV3.sol diff --git a/contracts/swap/BishopStableSwapV3.sol b/contracts/swap/BishopStableSwapV3.sol new file mode 100644 index 00000000..a4c57bf7 --- /dev/null +++ b/contracts/swap/BishopStableSwapV3.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +import "../interfaces/IPrimaryMarketV3.sol"; +import "../interfaces/ITrancheIndexV2.sol"; +import "./StableSwapV3.sol"; + +contract BishopStableSwapV3 is StableSwapV3, ITrancheIndexV2 { + event Rebalanced(uint256 base, uint256 quote, uint256 version); + + uint256 public currentVersion; + + constructor( + address lpToken_, + address fund_, + address quoteAddress_, + uint256 quoteDecimals_, + uint256 ampl_, + address feeCollector_, + uint256 feeRate_, + uint256 adminFeeRate_ + ) + public + StableSwapV3( + lpToken_, + fund_, + TRANCHE_B, + quoteAddress_, + quoteDecimals_, + ampl_, + feeCollector_, + feeRate_, + adminFeeRate_ + ) + { + 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; + } +} 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); +} From 458bae1af1c7783d1221d279c5fc9981011bc811 Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Fri, 15 Dec 2023 14:39:17 +0800 Subject: [PATCH 23/29] Update StableSwap for WstETH fund --- contracts/swap/RookStableSwapV2.sol | 157 ------------------ contracts/swap/WstETHBishopStableSwap.sol | 46 +++++ contracts/swap/WstETHRookStableSwap.sol | 46 +++++ ...pStableSwapV3.sol => WstETHStableSwap.sol} | 21 ++- 4 files changed, 109 insertions(+), 161 deletions(-) delete mode 100644 contracts/swap/RookStableSwapV2.sol create mode 100644 contracts/swap/WstETHBishopStableSwap.sol create mode 100644 contracts/swap/WstETHRookStableSwap.sol rename contracts/swap/{BishopStableSwapV3.sol => WstETHStableSwap.sol} (80%) diff --git a/contracts/swap/RookStableSwapV2.sol b/contracts/swap/RookStableSwapV2.sol deleted file mode 100644 index 3c263bd6..00000000 --- a/contracts/swap/RookStableSwapV2.sol +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.6.10 <0.8.0; - -import "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol"; - -import "../interfaces/IPrimaryMarketV5.sol"; -import "../interfaces/ITrancheIndexV2.sol"; -import "./StableSwapV2.sol"; - -contract RookStableSwapV2 is StableSwapV2, ITrancheIndexV2 { - event Rebalanced(uint256 base, uint256 quote, uint256 version); - - uint256 public immutable tradingCurbThreshold; - - uint256 public currentVersion; - - constructor( - address lpToken_, - address fund_, - address quoteAddress_, - uint256 quoteDecimals_, - uint256 ampl_, - address feeCollector_, - uint256 feeRate_, - uint256 adminFeeRate_, - uint256 tradingCurbThreshold_ - ) - public - StableSwapV2( - lpToken_, - fund_, - TRANCHE_R, - quoteAddress_, - quoteDecimals_, - ampl_, - feeCollector_, - feeRate_, - adminFeeRate_ - ) - { - tradingCurbThreshold = tradingCurbThreshold_; - 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 ( - uint256 newBase, - uint256 newQuote, - uint256 excessiveQ, - uint256 excessiveB, - uint256 excessiveR, - uint256 excessiveQuote, - bool isRebalanced - ) - { - if (latestVersion == currentVersion) { - return (baseBalance, quoteBalance, 0, 0, 0, 0, false); - } - isRebalanced = true; - - uint256 oldBaseBalance = baseBalance; - uint256 oldQuoteBalance = quoteBalance; - (excessiveQ, newBase, ) = fund.batchRebalance( - 0, - oldBaseBalance, - 0, - currentVersion, - latestVersion - ); - if (newBase < oldBaseBalance) { - // We split all QUEEN from rebalance if the amount of ROOK is smaller than before. - // In almost all cases, the total amount of ROOK after the split is still smaller - // than before. - (excessiveB, excessiveR) = IPrimaryMarketV5(fund.primaryMarket()).getSplit(excessiveQ); - newBase = newBase.add(excessiveR); - } - if (newBase < oldBaseBalance) { - // If ROOK amount is still smaller than before, we remove quote tokens proportionally. - newQuote = oldQuoteBalance.mul(newBase).div(oldBaseBalance); - excessiveQuote = oldQuoteBalance - newQuote; - } else { - // In most cases when we reach here, the ROOK amount remains the same (ratioBR = 1). - newQuote = oldQuoteBalance; - excessiveR = newBase - oldBaseBalance; - newBase = oldBaseBalance; - } - } - - function _handleRebalance( - uint256 latestVersion - ) internal override returns (uint256 newBase, uint256 newQuote) { - uint256 excessiveQ; - uint256 excessiveB; - uint256 excessiveR; - uint256 excessiveQuote; - bool isRebalanced; - ( - newBase, - newQuote, - excessiveQ, - excessiveB, - excessiveR, - excessiveQuote, - isRebalanced - ) = _getRebalanceResult(latestVersion); - if (isRebalanced) { - baseBalance = newBase; - quoteBalance = newQuote; - currentVersion = latestVersion; - emit Rebalanced(newBase, newQuote, latestVersion); - if (excessiveQ > 0) { - if (excessiveR > 0) { - IPrimaryMarketV5(fund.primaryMarket()).split( - address(this), - excessiveQ, - latestVersion - ); - excessiveQ = 0; - } else { - fund.trancheTransfer(TRANCHE_Q, lpToken, excessiveQ, latestVersion); - } - } - if (excessiveB > 0) { - fund.trancheTransfer(TRANCHE_B, lpToken, excessiveB, latestVersion); - } - if (excessiveR > 0) { - fund.trancheTransfer(TRANCHE_R, lpToken, excessiveR, latestVersion); - } - if (excessiveQuote > 0) { - IERC20(quoteAddress).safeTransfer(lpToken, excessiveQuote); - } - ILiquidityGauge(lpToken).distribute( - excessiveQ, - excessiveB, - excessiveR, - excessiveQuote, - latestVersion - ); - } - } - - function getOraclePrice() public view override returns (uint256) { - uint256 price = fund.twapOracle().getLatest(); // wstETH / stETH - (, , uint256 navR) = fund.extrapolateNav(price); // ETH / ROOK - return navR.multiplyDecimal(price); // wstETH / ROOK - } -} 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/BishopStableSwapV3.sol b/contracts/swap/WstETHStableSwap.sol similarity index 80% rename from contracts/swap/BishopStableSwapV3.sol rename to contracts/swap/WstETHStableSwap.sol index a4c57bf7..5deda7ec 100644 --- a/contracts/swap/BishopStableSwapV3.sol +++ b/contracts/swap/WstETHStableSwap.sol @@ -1,18 +1,21 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.6.10 <0.8.0; -import "../interfaces/IPrimaryMarketV3.sol"; import "../interfaces/ITrancheIndexV2.sol"; +import "../interfaces/IWstETH.sol"; import "./StableSwapV3.sol"; -contract BishopStableSwapV3 is StableSwapV3, ITrancheIndexV2 { +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_, @@ -24,7 +27,7 @@ contract BishopStableSwapV3 is StableSwapV3, ITrancheIndexV2 { StableSwapV3( lpToken_, fund_, - TRANCHE_B, + baseTranche_, quoteAddress_, quoteDecimals_, ampl_, @@ -33,6 +36,8 @@ contract BishopStableSwapV3 is StableSwapV3, ITrancheIndexV2 { adminFeeRate_ ) { + require(quoteAddress_ == IFundV3(fund_).tokenUnderlying()); + wstETH = quoteAddress_; currentVersion = IFundV3(fund_).getRebalanceSize(); } @@ -84,6 +89,14 @@ contract BishopStableSwapV3 is StableSwapV3, ITrancheIndexV2 { function getOraclePrice() public view override returns (uint256) { uint256 price = fund.twapOracle().getLatest(); (, uint256 navB, ) = fund.extrapolateNav(price); - return navB; + 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); } From 1b4b90b44c1b7ee7e8e8ba33d28dcdd68562d021 Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Fri, 15 Dec 2023 15:00:00 +0800 Subject: [PATCH 24/29] Change weightB from constant to immutable --- contracts/fund/FundV5.sol | 18 ++++++++++-------- contracts/fund/PrimaryMarketV5.sol | 2 +- contracts/interfaces/IFundV5.sol | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/contracts/fund/FundV5.sol b/contracts/fund/FundV5.sol index 63e173a1..4d9cd854 100644 --- a/contracts/fund/FundV5.sol +++ b/contracts/fund/FundV5.sol @@ -49,7 +49,7 @@ contract FundV5 is uint256 private constant UNIT = 1e18; uint256 private constant MAX_INTEREST_RATE = 0.001e18; // 0.1% daily - uint256 public constant override WEIGHT_B = 9; + uint256 public immutable override weightB; /// @notice Address of the underlying token. address public immutable override tokenUnderlying; @@ -142,6 +142,7 @@ contract FundV5 is uint256 private _strategyUnderlying; struct ConstructorParameters { + uint256 weightB; address tokenUnderlying; uint256 underlyingDecimals; address tokenQ; @@ -167,6 +168,7 @@ contract FundV5 is params.strategy ) { + weightB = params.weightB; tokenUnderlying = params.tokenUnderlying; require(params.underlyingDecimals <= 18, "Underlying decimals larger than 18"); underlyingDecimalMultiplier = 10 ** (18 - params.underlyingDecimals); @@ -285,7 +287,7 @@ contract FundV5 is /// @notice Equivalent BISHOP supply, as if all QUEEN are split. function getEquivalentTotalB() public view override returns (uint256) { - return getEquivalentTotalR().mul(WEIGHT_B); + return getEquivalentTotalR().mul(weightB); } /// @notice Equivalent QUEEN supply, as if all BISHOP and ROOK are merged. @@ -339,7 +341,7 @@ contract FundV5 is /// 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 * WEIGHT_B and the estimated NAV of ROOK + /// @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( @@ -364,11 +366,11 @@ contract FundV5 is navB = navB.multiplyDecimal( historicalInterestRate[settledDay].mul(timestamp - settledDay).div(1 days).add(UNIT) ); - navROrZero = navSum >= navB.mul(WEIGHT_B) ? navSum - navB * WEIGHT_B : 0; + 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(WEIGHT_B) + navROrZero; + navSum = navB.mul(weightB) + navROrZero; } } @@ -719,7 +721,7 @@ contract FundV5 is ); require(navR > 0, "To be frozen"); - uint256 newSplitRatio = splitRatio.multiplyDecimal(navSum) / (WEIGHT_B + 1); + uint256 newSplitRatio = splitRatio.multiplyDecimal(navSum) / (weightB + 1); _triggerRebalance(day, navSum, navB, navR, newSplitRatio); navB = UNIT; navR = UNIT; @@ -909,12 +911,12 @@ contract FundV5 is uint256 ratioR2Q; if (navR <= navB) { ratioBR = navR; - ratioB2Q = (navB - navR).divideDecimal(newSplitRatio) / (WEIGHT_B + 1); + ratioB2Q = (navB - navR).divideDecimal(newSplitRatio) / (weightB + 1); ratioR2Q = 0; } else { ratioBR = navB; ratioB2Q = 0; - ratioR2Q = (navR - navB).divideDecimal(newSplitRatio) / (WEIGHT_B + 1); + ratioR2Q = (navR - navB).divideDecimal(newSplitRatio) / (weightB + 1); } return Rebalance({ diff --git a/contracts/fund/PrimaryMarketV5.sol b/contracts/fund/PrimaryMarketV5.sol index 2e4d1efd..754d1c02 100644 --- a/contracts/fund/PrimaryMarketV5.sol +++ b/contracts/fund/PrimaryMarketV5.sol @@ -91,7 +91,7 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, _updateRedemptionFeeRate(redemptionFeeRate_); _updateMergeFeeRate(mergeFeeRate_); _updateFundCap(fundCap_); - weightB = IFundV5(fund_).WEIGHT_B(); + weightB = IFundV5(fund_).weightB(); redemptionFlag = redemptionFlag_; } diff --git a/contracts/interfaces/IFundV5.sol b/contracts/interfaces/IFundV5.sol index 15e633cf..956d18c9 100644 --- a/contracts/interfaces/IFundV5.sol +++ b/contracts/interfaces/IFundV5.sol @@ -4,7 +4,7 @@ pragma solidity >=0.6.10 <0.8.0; import "./IFundV3.sol"; interface IFundV5 is IFundV3 { - function WEIGHT_B() external view returns (uint256); + function weightB() external view returns (uint256); function getEquivalentTotalR() external view returns (uint256); From 5de07f85e03fe668c4806696f43c07826b47eca2 Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Fri, 15 Dec 2023 15:04:35 +0800 Subject: [PATCH 25/29] Change weightB in primary market to private --- contracts/fund/PrimaryMarketV5.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/fund/PrimaryMarketV5.sol b/contracts/fund/PrimaryMarketV5.sol index 754d1c02..7dca4c6a 100644 --- a/contracts/fund/PrimaryMarketV5.sol +++ b/contracts/fund/PrimaryMarketV5.sol @@ -50,7 +50,7 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, address public immutable override fund; bool public immutable redemptionFlag; - uint256 public immutable weightB; + uint256 private immutable _weightB; IERC20 private immutable _tokenUnderlying; uint256 public redemptionFeeRate; @@ -91,7 +91,7 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, _updateRedemptionFeeRate(redemptionFeeRate_); _updateMergeFeeRate(mergeFeeRate_); _updateFundCap(fundCap_); - weightB = IFundV5(fund_).weightB(); + _weightB = IFundV5(fund_).weightB(); redemptionFlag = redemptionFlag_; } @@ -110,7 +110,7 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, uint256 underlyingPrice = IFundV5(fund).twapOracle().getTwap(settledDay); (uint256 navB, uint256 navR) = IFundV5(fund).historicalNavs(settledDay); outQ = outQ.mul(underlyingPrice).div(splitRatio).divideDecimal( - navB.mul(weightB).add(navR) + navB.mul(_weightB).add(navR) ); } else { require( @@ -200,7 +200,7 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, /// @return outB Received BISHOP amount, which is also 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); + outB = outR.mul(_weightB); } /// @notice Calculate the amount of QUEEN that can be split into at least the given amount of @@ -209,7 +209,7 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, /// @return inQ QUEEN amount that should be split function getSplitForB(uint256 minOutB) external view override returns (uint256 inQ) { uint256 splitRatio = IFundV5(fund).splitRatio(); - return minOutB.divideDecimal(weightB).add(splitRatio.sub(1)).div(splitRatio); + return minOutB.divideDecimal(_weightB).add(splitRatio.sub(1)).div(splitRatio); } /// @notice Calculate the result of a merge. @@ -221,7 +221,7 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, uint256 inB ) public view override returns (uint256 inR, uint256 outQ, uint256 feeQ) { uint256 splitRatio = IFundV5(fund).splitRatio(); - uint256 outQBeforeFee = inB.divideDecimal(splitRatio.mul(weightB)); + uint256 outQBeforeFee = inB.divideDecimal(splitRatio.mul(_weightB)); feeQ = outQBeforeFee.multiplyDecimal(mergeFeeRate); outQ = outQBeforeFee.sub(feeQ); if (IFundV5(fund).frozen()) { @@ -252,7 +252,7 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, // = minOutQ uint256 splitRatio = IFundV5(fund).splitRatio(); uint256 outQBeforeFee = minOutQ.divideDecimal(1e18 - mergeFeeRate); - inB = outQBeforeFee.mul(splitRatio.mul(weightB)).add(1e18 - 1).div(1e18); + inB = outQBeforeFee.mul(splitRatio.mul(_weightB)).add(1e18 - 1).div(1e18); } /// @notice Return index of the first queued redemption that cannot be claimed now. From 982520d5634e437d0c1a63bba0579c6af7db4981 Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Sun, 17 Dec 2023 16:36:54 +0800 Subject: [PATCH 26/29] Simplify PrimaryMarketV5 --- contracts/fund/FundV5.sol | 16 ++- contracts/fund/PrimaryMarketV5.sol | 113 ++---------------- contracts/fund/WstETHPrimaryMarketRouter.sol | 53 +------- contracts/interfaces/IFundV5.sol | 2 + contracts/interfaces/IPrimaryMarketV5.sol | 8 -- .../interfaces/IWstETHPrimaryMarketRouter.sol | 23 ---- 6 files changed, 30 insertions(+), 185 deletions(-) delete mode 100644 contracts/interfaces/IWstETHPrimaryMarketRouter.sol diff --git a/contracts/fund/FundV5.sol b/contracts/fund/FundV5.sol index 4d9cd854..ee85ce47 100644 --- a/contracts/fund/FundV5.sol +++ b/contracts/fund/FundV5.sol @@ -50,6 +50,7 @@ contract FundV5 is 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; @@ -143,6 +144,7 @@ contract FundV5 is struct ConstructorParameters { uint256 weightB; + uint256 settlementPeriod; address tokenUnderlying; uint256 underlyingDecimals; address tokenQ; @@ -169,6 +171,8 @@ contract FundV5 is ) { 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); @@ -190,7 +194,7 @@ contract FundV5 is _historicalSplitRatio[0] = newSplitRatio; emit SplitRatioUpdated(newSplitRatio); uint256 lastDay = endOfDay(block.timestamp) - 1 days; - currentDay = lastDay + 365 days; + currentDay = lastDay + settlementPeriod; uint256 lastDayPrice = twapOracle.getTwap(lastDay); require(lastDayPrice != 0, "Price not available"); // required to do the first creation _historicalNavB[lastDay] = lastNavB; @@ -316,6 +320,10 @@ contract FundV5 is 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 @@ -347,7 +355,7 @@ contract FundV5 is function extrapolateNav( uint256 price ) external view override returns (uint256 navSum, uint256 navB, uint256 navROrZero) { - uint256 settledDay = currentDay - 365 days; + uint256 settledDay = getSettledDay(); uint256 underlying = getTotalUnderlying(); return _extrapolateNav(block.timestamp, settledDay, price, getEquivalentTotalR(), underlying); @@ -714,7 +722,7 @@ contract FundV5 is uint256 underlying = getTotalUnderlying(); (uint256 navSum, uint256 navB, uint256 navR) = _extrapolateNav( day, - day - 365 days, + day - settlementPeriod, price, getEquivalentTotalR(), underlying @@ -733,7 +741,7 @@ contract FundV5 is _historicalNavR[day] = navR; uint256 interestRate = _updateInterestRate(day); historicalInterestRate[day] = interestRate; - currentDay = day + 365 days; + currentDay = day + settlementPeriod; emit Settled(day, navB, navR, interestRate); } diff --git a/contracts/fund/PrimaryMarketV5.sol b/contracts/fund/PrimaryMarketV5.sol index 7dca4c6a..0bb92cb3 100644 --- a/contracts/fund/PrimaryMarketV5.sol +++ b/contracts/fund/PrimaryMarketV5.sol @@ -87,7 +87,7 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, bool redemptionFlag_ ) public Ownable() { fund = fund_; - _tokenUnderlying = IERC20(IFundV5(fund_).tokenUnderlying()); + _tokenUnderlying = IERC20(IFundV3(fund_).tokenUnderlying()); _updateRedemptionFeeRate(redemptionFeeRate_); _updateMergeFeeRate(mergeFeeRate_); _updateFundCap(fundCap_); @@ -99,16 +99,16 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, /// @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 = IFundV5(fund).getTotalUnderlying(); - uint256 fundEquivalentTotalQ = IFundV5(fund).getEquivalentTotalQ(); + uint256 fundUnderlying = IFundV3(fund).getTotalUnderlying(); + uint256 fundEquivalentTotalQ = IFundV3(fund).getEquivalentTotalQ(); require(fundUnderlying.add(underlying) <= fundCap, "Exceed fund cap"); if (fundEquivalentTotalQ == 0) { - outQ = underlying.mul(IFundV5(fund).underlyingDecimalMultiplier()); - uint256 splitRatio = IFundV5(fund).splitRatio(); + outQ = underlying.mul(IFundV3(fund).underlyingDecimalMultiplier()); + uint256 splitRatio = IFundV3(fund).splitRatio(); require(splitRatio != 0, "Fund is not initialized"); - uint256 settledDay = IFundV5(fund).currentDay() - 1 days; - uint256 underlyingPrice = IFundV5(fund).twapOracle().getTwap(settledDay); - (uint256 navB, uint256 navR) = IFundV5(fund).historicalNavs(settledDay); + 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) ); @@ -121,33 +121,9 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, } } - /// @notice Calculate the amount of underlying tokens to create at least the given QUEEN amount. - /// This only works with non-empty fund for simplicity. - /// @param minOutQ Minimum received QUEEN amount - /// @return underlying Underlying amount that should be used for creation - function getCreationForQ(uint256 minOutQ) external view override returns (uint256 underlying) { - // Assume: - // minOutQ * fundUnderlying = a * fundEquivalentTotalQ - b - // where a and b are integers and 0 <= b < fundEquivalentTotalQ - // Then - // underlying = a - // getCreation(underlying) - // = floor(a * fundEquivalentTotalQ / fundUnderlying) - // >= floor((a * fundEquivalentTotalQ - b) / fundUnderlying) - // = minOutQ - // getCreation(underlying - 1) - // = floor((a * fundEquivalentTotalQ - fundEquivalentTotalQ) / fundUnderlying) - // < (a * fundEquivalentTotalQ - b) / fundUnderlying - // = minOutQ - uint256 fundUnderlying = IFundV5(fund).getTotalUnderlying(); - uint256 fundEquivalentTotalQ = IFundV5(fund).getEquivalentTotalQ(); - require(fundEquivalentTotalQ > 0, "Cannot calculate creation for empty fund"); - return minOutQ.mul(fundUnderlying).add(fundEquivalentTotalQ - 1).div(fundEquivalentTotalQ); - } - function _getRedemption(uint256 inQ) private view returns (uint256 underlying) { - uint256 fundUnderlying = IFundV5(fund).getTotalUnderlying(); - uint256 fundEquivalentTotalQ = IFundV5(fund).getEquivalentTotalQ(); + uint256 fundUnderlying = IFundV3(fund).getTotalUnderlying(); + uint256 fundEquivalentTotalQ = IFundV3(fund).getEquivalentTotalQ(); underlying = inQ.mul(fundUnderlying).div(fundEquivalentTotalQ); } @@ -162,56 +138,15 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, underlying = _getRedemption(inQ - feeQ); } - /// @notice Calculate the amount of QUEEN that can be redeemed for at least the given amount - /// of underlying tokens. - /// @dev The return value may not be the minimum solution due to rounding errors. - /// @param minUnderlying Minimum received underlying amount - /// @return inQ QUEEN amount that should be redeemed - function getRedemptionForUnderlying( - uint256 minUnderlying - ) external view override returns (uint256 inQ) { - // Assume: - // minUnderlying * fundEquivalentTotalQ = a * fundUnderlying - b - // a * 1e18 = c * (1e18 - redemptionFeeRate) + d - // where - // a, b, c, d are integers - // 0 <= b < fundUnderlying - // 0 <= d < 1e18 - redemeptionFeeRate - // Then - // inQAfterFee = a - // inQ = c - // getRedemption(inQ).underlying - // = floor((c - floor(c * redemptionFeeRate / 1e18)) * fundUnderlying / fundEquivalentTotalQ) - // = floor(ceil(c * (1e18 - redemptionFeeRate) / 1e18) * fundUnderlying / fundEquivalentTotalQ) - // = floor(((c * (1e18 - redemptionFeeRate) + d) / 1e18) * fundUnderlying / fundEquivalentTotalQ) - // = floor(a * fundUnderlying / fundEquivalentTotalQ) - // => floor((a * fundUnderlying - b) / fundEquivalentTotalQ) - // = minUnderlying - uint256 fundUnderlying = IFundV5(fund).getTotalUnderlying(); - uint256 fundEquivalentTotalQ = IFundV5(fund).getEquivalentTotalQ(); - uint256 inQAfterFee = minUnderlying.mul(fundEquivalentTotalQ).add(fundUnderlying - 1).div( - fundUnderlying - ); - return inQAfterFee.divideDecimal(1e18 - redemptionFeeRate); - } - /// @notice Calculate the result of a split. /// @param inQ QUEEN amount to be split - /// @return outB Received BISHOP amount, which is also received ROOK amount + /// @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 amount of QUEEN that can be split into at least the given amount of - /// BISHOP and ROOK. - /// @param minOutB Received BISHOP amount, which is also received ROOK amount - /// @return inQ QUEEN amount that should be split - function getSplitForB(uint256 minOutB) external view override returns (uint256 inQ) { - uint256 splitRatio = IFundV5(fund).splitRatio(); - return minOutB.divideDecimal(_weightB).add(splitRatio.sub(1)).div(splitRatio); - } - /// @notice Calculate the result of a merge. /// @param inB Spent BISHOP amount /// @return inR Spent ROOK amount @@ -231,30 +166,6 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, } } - /// @notice Calculate the amount of BISHOP and ROOK that can be merged into at least - /// the given amount of QUEEN. - /// @dev The return value may not be the minimum solution due to rounding errors. - /// @param minOutQ Minimum received QUEEN amount - /// @return inB BISHOP amount that should be merged, which is also spent ROOK amount - function getMergeForQ(uint256 minOutQ) external view override returns (uint256 inB) { - // Assume: - // minOutQ * 1e18 = a * (1e18 - mergeFeeRate) + b - // c = ceil(a * splitRatio / 1e18) - // where a and b are integers and 0 <= b < 1e18 - mergeFeeRate - // Then - // outQBeforeFee = a - // inB = c - // getMerge(inB).outQ - // = c * 1e18 / splitRatio - floor(c * 1e18 / splitRatio * mergeFeeRate / 1e18) - // = ceil(c * 1e18 / splitRatio * (1e18 - mergeFeeRate) / 1e18) - // >= ceil(a * (1e18 - mergeFeeRate) / 1e18) - // = (a * (1e18 - mergeFeeRate) + b) / 1e18 // because b < 1e18 - // = minOutQ - uint256 splitRatio = IFundV5(fund).splitRatio(); - uint256 outQBeforeFee = minOutQ.divideDecimal(1e18 - mergeFeeRate); - inB = outQBeforeFee.mul(splitRatio.mul(_weightB)).add(1e18 - 1).div(1e18); - } - /// @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()`. diff --git a/contracts/fund/WstETHPrimaryMarketRouter.sol b/contracts/fund/WstETHPrimaryMarketRouter.sol index 4ddfa2ed..021e61f8 100644 --- a/contracts/fund/WstETHPrimaryMarketRouter.sol +++ b/contracts/fund/WstETHPrimaryMarketRouter.sol @@ -5,12 +5,12 @@ pragma experimental ABIEncoderV2; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; -import "../interfaces/IWstETHPrimaryMarketRouter.sol"; import "../interfaces/IPrimaryMarketV5.sol"; +import "../interfaces/IFundV3.sol"; import "../interfaces/IWstETH.sol"; import "../interfaces/ITrancheIndexV2.sol"; -contract WstETHPrimaryMarketRouter is IWstETHPrimaryMarketRouter, ITrancheIndexV2 { +contract WstETHPrimaryMarketRouter is ITrancheIndexV2 { using SafeERC20 for IERC20; IPrimaryMarketV5 public immutable primaryMarket; @@ -28,58 +28,13 @@ contract WstETHPrimaryMarketRouter is IWstETHPrimaryMarketRouter, ITrancheIndexV _tokenB = fund_.tokenB(); } - /// @dev Get redemption with StableSwap getQuoteOut interface. - function getQuoteOut(uint256 baseIn) external view override returns (uint256 quoteOut) { - (quoteOut, ) = primaryMarket.getRedemption(baseIn); - } - - /// @dev Get creation for QUEEN with StableSwap getQuoteIn interface. - function getQuoteIn(uint256 baseOut) external view override returns (uint256 quoteIn) { - quoteIn = primaryMarket.getCreationForQ(baseOut); - } - - /// @dev Get creation with StableSwap getBaseOut interface. - function getBaseOut(uint256 quoteIn) external view override returns (uint256 baseOut) { - baseOut = primaryMarket.getCreation(quoteIn); - } - - /// @dev Get redemption for underlying with StableSwap getBaseIn interface. - function getBaseIn(uint256 quoteOut) external view override returns (uint256 baseIn) { - baseIn = primaryMarket.getRedemptionForUnderlying(quoteOut); - } - - /// @dev Create QUEEN with StableSwap buy interface. - /// Underlying should have already been sent to this contract - function buy( - uint256 version, - uint256 baseOut, - address recipient, - bytes calldata - ) external override returns (uint256 realBaseOut) { - uint256 routerQuoteBalance = IERC20(_wstETH).balanceOf(address(this)); - IERC20(_wstETH).safeTransfer(address(primaryMarket), routerQuoteBalance); - realBaseOut = primaryMarket.create(recipient, baseOut, version); - } - - /// @dev Redeem QUEEN with StableSwap sell interface. - /// QUEEN should have already been sent to this contract - function sell( - uint256 version, - uint256 quoteOut, - address recipient, - bytes calldata - ) external override returns (uint256 realQuoteOut) { - uint256 routerBaseBalance = fund.trancheBalanceOf(TRANCHE_Q, address(this)); - realQuoteOut = primaryMarket.redeem(recipient, routerBaseBalance, quoteOut, version); - } - function create( address recipient, bool isWrapped, uint256 underlying, uint256 minOutQ, uint256 version - ) public override returns (uint256 outQ) { + ) public returns (uint256 outQ) { if (isWrapped) { IERC20(_stETH).safeTransferFrom(msg.sender, address(this), underlying); underlying = IWstETH(_wstETH).wrap(underlying); @@ -96,7 +51,7 @@ contract WstETHPrimaryMarketRouter is IWstETHPrimaryMarketRouter, ITrancheIndexV bool isWrapped, uint256 minOutQ, uint256 version - ) external override { + ) external { // Create QUEEN uint256 outQ = create(address(this), isWrapped, underlying, minOutQ, version); diff --git a/contracts/interfaces/IFundV5.sol b/contracts/interfaces/IFundV5.sol index 956d18c9..77da7075 100644 --- a/contracts/interfaces/IFundV5.sol +++ b/contracts/interfaces/IFundV5.sol @@ -6,6 +6,8 @@ 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 index f6281c2b..f59a7dac 100644 --- a/contracts/interfaces/IPrimaryMarketV5.sol +++ b/contracts/interfaces/IPrimaryMarketV5.sol @@ -6,20 +6,12 @@ interface IPrimaryMarketV5 { function getCreation(uint256 underlying) external view returns (uint256 outQ); - function getCreationForQ(uint256 minOutQ) external view returns (uint256 underlying); - function getRedemption(uint256 inQ) external view returns (uint256 underlying, uint256 fee); - function getRedemptionForUnderlying(uint256 minUnderlying) external view returns (uint256 inQ); - function getSplit(uint256 inQ) external view returns (uint256 outB, uint256 outQ); - function getSplitForB(uint256 minOutB) external view returns (uint256 inQ); - function getMerge(uint256 inB) external view returns (uint256 inR, uint256 outQ, uint256 feeQ); - function getMergeForQ(uint256 minOutQ) external view returns (uint256 inB); - function canBeRemovedFromFund() external view returns (bool); function create( diff --git a/contracts/interfaces/IWstETHPrimaryMarketRouter.sol b/contracts/interfaces/IWstETHPrimaryMarketRouter.sol deleted file mode 100644 index bf65b1b7..00000000 --- a/contracts/interfaces/IWstETHPrimaryMarketRouter.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.6.10 <0.8.0; -pragma experimental ABIEncoderV2; - -import "../interfaces/IFundV5.sol"; -import "../interfaces/IStableSwap.sol"; - -interface IWstETHPrimaryMarketRouter is IStableSwapCore { - function create( - address recipient, - bool isWrapped, - uint256 underlying, - uint256 minOutQ, - uint256 version - ) external returns (uint256 outQ); - - function createAndSplit( - uint256 underlying, - bool isWrapped, - uint256 minOutQ, - uint256 version - ) external; -} From 4c6dd725d2286e3861c42292694b431c03d9bf3e Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Sun, 17 Dec 2023 18:03:42 +0800 Subject: [PATCH 27/29] Rename AprOracleV3 to ConstAprOracle --- contracts/oracle/AprOracleV3.sol | 16 ---------------- contracts/oracle/ConstAprOracle.sol | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 16 deletions(-) delete mode 100644 contracts/oracle/AprOracleV3.sol create mode 100644 contracts/oracle/ConstAprOracle.sol diff --git a/contracts/oracle/AprOracleV3.sol b/contracts/oracle/AprOracleV3.sol deleted file mode 100644 index 61d098ae..00000000 --- a/contracts/oracle/AprOracleV3.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.6.10 <0.8.0; - -import "../interfaces/IAprOracle.sol"; - -contract AprOracleV3 is IAprOracle { - uint256 public immutable dailyRate; - - constructor(uint256 dailyRate_) public { - dailyRate = dailyRate_; - } - - function capture() external override returns (uint256) { - return dailyRate; - } -} 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; + } +} From f234a41251ac95950e15a1b809c3839195863f94 Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Sun, 17 Dec 2023 18:05:00 +0800 Subject: [PATCH 28/29] Remove obsolete comments --- contracts/swap/SwapRouter.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/swap/SwapRouter.sol b/contracts/swap/SwapRouter.sol index 8a1d977f..3cd8f35c 100644 --- a/contracts/swap/SwapRouter.sol +++ b/contracts/swap/SwapRouter.sol @@ -16,8 +16,8 @@ contract SwapRouter is ISwapRouter, ITrancheIndexV2, Ownable { using SafeMath for uint256; using SafeERC20 for IERC20; - address public immutable wstETH; // Base - address public immutable stETH; // Quote + address public immutable wstETH; + address public immutable stETH; constructor(address wstETH_) public { wstETH = wstETH_; From 52fd7788a53f5ceb87eb1d2e2335481cf9ab3127 Mon Sep 17 00:00:00 2001 From: Terry Pawn Date: Sun, 17 Dec 2023 23:54:22 +0800 Subject: [PATCH 29/29] Rename WstETHtoStETHSwap to WstETHWrappingSwap --- contracts/swap/WstETHWrappingSwap.sol | 137 ++++++++++++++++++++++++++ contracts/swap/WstETHtoStETHSwap.sol | 62 ------------ 2 files changed, 137 insertions(+), 62 deletions(-) create mode 100644 contracts/swap/WstETHWrappingSwap.sol delete mode 100644 contracts/swap/WstETHtoStETHSwap.sol 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"); + } +} diff --git a/contracts/swap/WstETHtoStETHSwap.sol b/contracts/swap/WstETHtoStETHSwap.sol deleted file mode 100644 index fb16d9a5..00000000 --- a/contracts/swap/WstETHtoStETHSwap.sol +++ /dev/null @@ -1,62 +0,0 @@ -// 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 "../utils/SafeDecimalMath.sol"; -import "../interfaces/IWstETH.sol"; - -contract WstETHtoStETHSwap { - 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 returns (uint256 quoteOut) { - quoteOut = IWstETH(wstETH).getStETHByWstETH(baseIn); - } - - function getQuoteIn(uint256 baseOut) external view returns (uint256 quoteIn) { - quoteIn = IWstETH(wstETH).getStETHByWstETH(baseOut); - } - - function getBaseOut(uint256 quoteIn) external view returns (uint256 baseOut) { - baseOut = IWstETH(wstETH).getWstETHByStETH(quoteIn); - } - - function getBaseIn(uint256 quoteOut) external view returns (uint256 baseIn) { - baseIn = IWstETH(wstETH).getWstETHByStETH(quoteOut); - } - - function buy( - uint256, - uint256, - address recipient, - bytes calldata - ) external 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 returns (uint256 realQuoteOut) { - uint256 baseIn = IERC20(wstETH).balanceOf(address(this)); - realQuoteOut = IWstETH(wstETH).unwrap(baseIn); - IERC20(stETH).safeTransfer(recipient, realQuoteOut); - } - - function getOraclePrice() public view returns (uint256) { - return IWstETH(wstETH).stEthPerToken(); - } -}