diff --git a/.solhint.json b/.solhint.json index c780156..a512d9b 100644 --- a/.solhint.json +++ b/.solhint.json @@ -21,7 +21,7 @@ "code-complexity": ["error", 15], "function-max-lines": ["error", 80], "max-line-length": ["warn", 120], - "max-states-count": ["error", 15], + "max-states-count": ["error", 16], "no-empty-blocks": "warn", "no-unused-vars": "error", "payable-fallback": "off", diff --git a/script/Deploy.sol b/script/Deploy.sol index 514a55c..4ec0574 100644 --- a/script/Deploy.sol +++ b/script/Deploy.sol @@ -44,6 +44,7 @@ contract Deploy is Config { 0, 0, 0, + 0, 100, selfPeggingAssetBeacon, lpTokenBeacon, diff --git a/script/Pool.sol b/script/Pool.sol index 90d792b..6d7b89c 100644 --- a/script/Pool.sol +++ b/script/Pool.sol @@ -20,10 +20,12 @@ contract Pool is Config { tokenB: usdt, tokenAType: SelfPeggingAssetFactory.TokenType.Standard, tokenAOracle: address(0), - tokenAFunctionSig: "", + tokenARateFunctionSig: "", + tokenADecimalsFunctionSig: "", tokenBType: SelfPeggingAssetFactory.TokenType.Standard, tokenBOracle: address(0), - tokenBFunctionSig: "" + tokenBRateFunctionSig: "", + tokenBDecimalsFunctionSig: "" }); vm.recordLogs(); diff --git a/src/LPToken.sol b/src/LPToken.sol index cc08d7a..ac53903 100644 --- a/src/LPToken.sol +++ b/src/LPToken.sol @@ -33,6 +33,11 @@ contract LPToken is Initializable, OwnableUpgradeable, ILPToken { */ uint256 public constant BUFFER_DENOMINATOR = 10 ** 10; + /** + * @dev Constant value representing the number of dead shares. + */ + uint256 public constant NUMBER_OF_DEAD_SHARES = 1000; + /** * @dev The total amount of shares. */ @@ -56,7 +61,7 @@ contract LPToken is Initializable, OwnableUpgradeable, ILPToken { /** * @dev The mapping of account allowances. */ - mapping(address => mapping(address => uint256)) public allowances; + mapping(address => mapping(address => uint256)) private allowances; /** * @dev The mapping of pools. @@ -83,6 +88,11 @@ contract LPToken is Initializable, OwnableUpgradeable, ILPToken { */ string internal tokenSymbol; + /** + * @dev The bad debt of the buffer. + */ + uint256 public bufferBadDebt; + /** * @notice Emitted when shares are transferred. */ @@ -128,6 +138,11 @@ contract LPToken is Initializable, OwnableUpgradeable, ILPToken { */ event BufferDecreased(uint256, uint256); + /** + * @notice Emitted when there is negative rebase. + */ + event NegativelyRebased(uint256, uint256); + /** * @notice Emitted when the symbol is modified. */ @@ -172,6 +187,9 @@ contract LPToken is Initializable, OwnableUpgradeable, ILPToken { /// @notice Error thrown when the pool is not found. error PoolNotFound(); + /// @notice Error thrown when the supply is insufficient. + error InsufficientSupply(); + function initialize(string memory _name, string memory _symbol) public initializer { tokenName = _name; tokenSymbol = _symbol; @@ -338,6 +356,20 @@ contract LPToken is Initializable, OwnableUpgradeable, ILPToken { function addTotalSupply(uint256 _amount) external { require(pools[msg.sender], NoPool()); require(_amount != 0, InvalidAmount()); + + if (bufferBadDebt >= _amount) { + bufferBadDebt -= _amount; + bufferAmount += _amount; + emit BufferIncreased(_amount, bufferAmount); + return; + } + + uint256 prevAmount = _amount; + uint256 prevBufferBadDebt = bufferBadDebt; + _amount = _amount - bufferBadDebt; + bufferAmount += bufferBadDebt; + bufferBadDebt = 0; + uint256 _deltaBuffer = (bufferPercent * _amount) / BUFFER_DENOMINATOR; uint256 actualAmount = _amount - _deltaBuffer; @@ -345,22 +377,33 @@ contract LPToken is Initializable, OwnableUpgradeable, ILPToken { totalRewards += actualAmount; bufferAmount += _deltaBuffer; - emit BufferIncreased(_deltaBuffer, bufferAmount); - emit RewardsMinted(_amount, actualAmount); + emit BufferIncreased(_deltaBuffer + prevBufferBadDebt, bufferAmount); + emit RewardsMinted(prevAmount, actualAmount); } /** * @notice This function is called only by a stableSwap pool to decrease * the total supply of LPToken by lost amount. + * @param _amount The amount of lost tokens. + * @param isBuffer The flag to indicate whether to use the buffer or not. + * @param withDebt The flag to indicate whether to add the lost amount to the buffer bad debt or not. */ - function removeTotalSupply(uint256 _amount) external { + function removeTotalSupply(uint256 _amount, bool isBuffer, bool withDebt) external { require(pools[msg.sender], NoPool()); require(_amount != 0, InvalidAmount()); - require(_amount <= bufferAmount, InsufficientBuffer()); - bufferAmount -= _amount; - - emit BufferDecreased(_amount, bufferAmount); + if (isBuffer) { + require(_amount <= bufferAmount, InsufficientBuffer()); + bufferAmount -= _amount; + if (withDebt) { + bufferBadDebt += _amount; + } + emit BufferDecreased(_amount, bufferAmount); + } else { + require(_amount <= totalSupply, InsufficientSupply()); + totalSupply -= _amount; + emit NegativelyRebased(_amount, totalSupply); + } } /** @@ -537,7 +580,9 @@ contract LPToken is Initializable, OwnableUpgradeable, ILPToken { if (totalSupply != 0 && totalShares != 0) { _sharesAmount = getSharesByPeggedToken(_tokenAmount); } else { - _sharesAmount = _tokenAmount; + _sharesAmount = totalSupply + _tokenAmount - NUMBER_OF_DEAD_SHARES; + shares[address(0)] = NUMBER_OF_DEAD_SHARES; + totalShares += NUMBER_OF_DEAD_SHARES; } shares[_recipient] += _sharesAmount; totalShares += _sharesAmount; diff --git a/src/SelfPeggingAsset.sol b/src/SelfPeggingAsset.sol index cb1c154..faf4f02 100644 --- a/src/SelfPeggingAsset.sol +++ b/src/SelfPeggingAsset.sol @@ -85,6 +85,12 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU */ uint256 public redeemFee; + /** + * @dev This is the off peg fee multiplier. + * offPegFeeMultiplier = offPegFeeMultiplier * FEE_DENOMINATOR + */ + uint256 public offPegFeeMultiplier; + /** * @dev This is the address of the ERC20 token contract that represents the SelfPeggingAsset pool token. */ @@ -175,11 +181,10 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU /** * @dev This event is emitted when yield is collected by the SelfPeggingAsset contract. - * @param amounts is an array containing the amounts of each token the yield receives. * @param feeAmount is the amount of yield collected. * @param totalSupply is the total supply of LP token. */ - event YieldCollected(uint256[] amounts, uint256 feeAmount, uint256 totalSupply); + event YieldCollected(uint256 feeAmount, uint256 totalSupply); /** * @dev This event is emitted when the A parameter is modified. @@ -205,6 +210,12 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU */ event RedeemFeeModified(uint256 redeemFee); + /** + * @dev This event is emitted when the off peg fee multiplier is modified. + * @param offPegFeeMultiplier is the new value of the off peg fee multiplier. + */ + event OffPegFeeMultiplierModified(uint256 offPegFeeMultiplier); + /** * @dev This event is emitted when the fee margin is modified. * @param margin is the new value of the margin. @@ -286,9 +297,6 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU /// @notice Error thrown when the block number is an past block error PastBlock(); - /// @notice Error thrown when the pool is imbalanced - error PoolImbalanced(); - /// @notice Error thrown when there is no loss error NoLosses(); @@ -321,6 +329,7 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU * @param _tokens The tokens in the pool. * @param _precisions The precisions of each token (10 ** (18 - token decimals)). * @param _fees The fees for minting, swapping, and redeeming. + * @param _offPegFeeMultiplier The off peg fee multiplier. * @param _poolToken The address of the pool token. * @param _A The initial value of the amplification coefficient A for the pool. */ @@ -328,6 +337,7 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU address[] memory _tokens, uint256[] memory _precisions, uint256[] memory _fees, + uint256 _offPegFeeMultiplier, ILPToken _poolToken, uint256 _A, IExchangeRateProvider[] memory _exchangeRateProviders @@ -369,6 +379,7 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU redeemFee = _fees[2]; poolToken = _poolToken; exchangeRateProviders = _exchangeRateProviders; + offPegFeeMultiplier = _offPegFeeMultiplier; A = _A; feeErrorMargin = DEFAULT_FEE_ERROR_MARGIN; @@ -412,7 +423,8 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU uint256 feeAmount = 0; if (mintFee > 0) { - feeAmount = (mintAmount * mintFee) / FEE_DENOMINATOR; + uint256 dynamicFee = oldD == 0 ? mintFee : _dynamicFee(oldD, newD, mintFee); + feeAmount = (mintAmount * dynamicFee) / FEE_DENOMINATOR; mintAmount = mintAmount - feeAmount; } if (mintAmount < _minMintAmount) { @@ -453,6 +465,7 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU collectFeeOrYield(false); uint256[] memory _balances = balances; + uint256 prevBalanceI = _balances[_i]; uint256 balanceAmount = _dx; balanceAmount = (balanceAmount * exchangeRateProviders[_i].exchangeRate()) / (10 ** exchangeRateProviders[_i].exchangeRateDecimals()); @@ -467,7 +480,8 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU uint256 feeAmount = 0; if (swapFee > 0) { - feeAmount = (dy * swapFee) / FEE_DENOMINATOR; + uint256 dynamicFee = _dynamicFee(prevBalanceI, _balances[_j], swapFee); + feeAmount = (dy * dynamicFee) / FEE_DENOMINATOR; dy = dy - feeAmount; } _minDy = (_minDy * exchangeRateProviders[_j].exchangeRate()) @@ -648,7 +662,8 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU uint256 redeemAmount = oldD - newD; uint256 feeAmount = 0; if (redeemFee > 0) { - redeemAmount = (redeemAmount * FEE_DENOMINATOR) / (FEE_DENOMINATOR - redeemFee); + uint256 dynamicFee = _dynamicFee(oldD, newD, redeemFee); + redeemAmount = (redeemAmount * FEE_DENOMINATOR) / (FEE_DENOMINATOR - dynamicFee); feeAmount = redeemAmount - (oldD - newD); } if (redeemAmount > _maxRedeemAmount) { @@ -699,6 +714,15 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU emit RedeemFeeModified(_redeemFee); } + /** + * @dev Updates the off peg fee multiplier. + * @param _offPegFeeMultiplier The new off peg fee multiplier. + */ + function setOffPegFeeMultiplier(uint256 _offPegFeeMultiplier) external onlyOwner { + offPegFeeMultiplier = _offPegFeeMultiplier; + emit OffPegFeeMultiplierModified(_offPegFeeMultiplier); + } + /** * @dev Pause mint/swap/redeem actions. Can unpause later. */ @@ -742,7 +766,7 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU if (totalSupply > newD) { // A decreased - poolToken.removeTotalSupply(totalSupply - newD); + poolToken.removeTotalSupply(totalSupply - newD, true, false); } if (newD > totalSupply) { @@ -817,7 +841,7 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU } /** - * @dev Distribute losses + * @dev Distribute losses by rebasing negatively */ function distributeLoss() external onlyOwner { require(paused, NotPaused()); @@ -834,7 +858,8 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU uint256 newD = _getD(_balances); require(newD < oldD, NoLosses()); - poolToken.removeTotalSupply(oldD - newD); + poolToken.removeTotalSupply(oldD - newD, false, false); + balances = _balances; totalSupply = newD; } @@ -855,7 +880,10 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU } uint256 newD = _getD(_balances); - if (oldD > newD) { + if (oldD == newD) { + return 0; + } else if (oldD > newD) { + poolToken.removeTotalSupply(oldD - newD, true, true); return 0; } else { balances = _balances; @@ -876,7 +904,7 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU function getRedeemSingleAmount(uint256 _amount, uint256 _i) external view returns (uint256, uint256) { uint256[] memory _balances; uint256 _totalSupply; - (_balances, _totalSupply) = getPendingYieldAmount(); + (_balances, _totalSupply) = getUpdatedBalancesAndD(); require(_amount > 0, ZeroAmount()); require(_i < _balances.length, InvalidToken()); @@ -908,7 +936,7 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU function getRedeemMultiAmount(uint256[] calldata _amounts) external view returns (uint256, uint256) { uint256[] memory _balances; uint256 _totalSupply; - (_balances, _totalSupply) = getPendingYieldAmount(); + (_balances, _totalSupply) = getUpdatedBalancesAndD(); require(_amounts.length == balances.length, InputMismatch()); uint256 oldD = _totalSupply; @@ -942,7 +970,7 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU function getMintAmount(uint256[] calldata _amounts) external view returns (uint256, uint256) { uint256[] memory _balances; uint256 _totalSupply; - (_balances, _totalSupply) = getPendingYieldAmount(); + (_balances, _totalSupply) = getUpdatedBalancesAndD(); require(_amounts.length == _balances.length, InvalidAmount()); uint256 oldD = _totalSupply; @@ -961,7 +989,8 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU uint256 feeAmount = 0; if (mintFee > 0) { - feeAmount = (mintAmount * mintFee) / FEE_DENOMINATOR; + uint256 dynamicFee = _dynamicFee(oldD, newD, mintFee); + feeAmount = (mintAmount * dynamicFee) / FEE_DENOMINATOR; mintAmount = mintAmount - feeAmount; } @@ -979,12 +1008,13 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU function getSwapAmount(uint256 _i, uint256 _j, uint256 _dx) external view returns (uint256, uint256) { uint256[] memory _balances; uint256 _totalSupply; - (_balances, _totalSupply) = getPendingYieldAmount(); + (_balances, _totalSupply) = getUpdatedBalancesAndD(); require(_i != _j, SameToken()); require(_i < _balances.length, InvalidIn()); require(_j < _balances.length, InvalidOut()); require(_dx > 0, InvalidAmount()); + uint256 prevBalanceI = _balances[_i]; uint256 D = _totalSupply; uint256 balanceAmount = _dx; balanceAmount = (balanceAmount * exchangeRateProviders[_i].exchangeRate()) @@ -997,7 +1027,8 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU uint256 feeAmount = 0; if (swapFee > 0) { - feeAmount = (dy * swapFee) / FEE_DENOMINATOR; + uint256 dynamicFee = _dynamicFee(prevBalanceI, _balances[_j], swapFee); + feeAmount = (dy * dynamicFee) / FEE_DENOMINATOR; dy = dy - feeAmount; } @@ -1020,7 +1051,7 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU function getRedeemProportionAmount(uint256 _amount) external view returns (uint256[] memory, uint256) { uint256[] memory _balances; uint256 _totalSupply; - (_balances, _totalSupply) = getPendingYieldAmount(); + (_balances, _totalSupply) = getUpdatedBalancesAndD(); require(_amount != 0, ZeroAmount()); uint256 D = _totalSupply; @@ -1058,7 +1089,6 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU * @return The amount of fee or yield collected. */ function collectFeeOrYield(bool isFee) internal returns (uint256) { - uint256[] memory oldBalances = balances; uint256[] memory _balances = balances; uint256 oldD = totalSupply; @@ -1073,19 +1103,19 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU balances = _balances; totalSupply = newD; - if (isFee) { - if (oldD > newD && (oldD - newD) < feeErrorMargin) { - return 0; - } else if (oldD > newD) { - revert ImbalancedPool(oldD, newD); - } - } else { - if (oldD > newD && (oldD - newD) < yieldErrorMargin) { + if (oldD > newD) { + uint256 delta = oldD - newD; + uint256 margin = isFee ? feeErrorMargin : yieldErrorMargin; + + if (delta < margin) { return 0; - } else if (oldD > newD) { - revert ImbalancedPool(oldD, newD); } + + // Cover losses using the buffer + poolToken.removeTotalSupply(delta, true, true); + return 0; } + uint256 feeAmount = newD - oldD; if (feeAmount == 0) { return 0; @@ -1095,14 +1125,7 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU if (isFee) { emit FeeCollected(feeAmount, totalSupply); } else { - uint256[] memory amounts = new uint256[](_balances.length); - for (uint256 i = 0; i < _balances.length; i++) { - uint256 amount = _balances[i] - oldBalances[i]; - amount = (amount * (10 ** exchangeRateProviders[i].exchangeRateDecimals())) - / exchangeRateProviders[i].exchangeRate(); - amounts[i] = amount / precisions[i]; - } - emit YieldCollected(amounts, feeAmount, totalSupply); + emit YieldCollected(feeAmount, totalSupply); } return feeAmount; } @@ -1112,7 +1135,7 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU * @return The balances of underlying tokens. * @return The total supply of pool tokens. */ - function getPendingYieldAmount() internal view returns (uint256[] memory, uint256) { + function getUpdatedBalancesAndD() internal view returns (uint256[] memory, uint256) { uint256[] memory _balances = balances; for (uint256 i = 0; i < _balances.length; i++) { @@ -1212,4 +1235,23 @@ contract SelfPeggingAsset is Initializable, ReentrancyGuardUpgradeable, OwnableU } return y; } + + /** + * @dev Calculates the dynamic fee based on liquidity imbalances. + * @param xpi The liquidity before or first asset liqidity. + * @param xpj The liqduity after or second asset liquidity. + * @param _fee The base fee value. + * @return The dynamically adjusted fee. + */ + function _dynamicFee(uint256 xpi, uint256 xpj, uint256 _fee) internal view returns (uint256) { + uint256 _offpegFeeMultiplier = offPegFeeMultiplier; + + if (_offpegFeeMultiplier <= FEE_DENOMINATOR) { + return _fee; + } + + uint256 xps2 = (xpi + xpj) * (xpi + xpj); + return (_offpegFeeMultiplier * _fee) + / (((_offpegFeeMultiplier - FEE_DENOMINATOR) * 4 * xpi * xpj) / xps2 + FEE_DENOMINATOR); + } } diff --git a/src/SelfPeggingAssetFactory.sol b/src/SelfPeggingAssetFactory.sol index eab4eba..2428deb 100644 --- a/src/SelfPeggingAssetFactory.sol +++ b/src/SelfPeggingAssetFactory.sol @@ -45,14 +45,18 @@ contract SelfPeggingAssetFactory is UUPSUpgradeable, OwnableUpgradeable { TokenType tokenAType; /// @notice Address of the oracle for token A address tokenAOracle; - /// @notice Function signature for token A - string tokenAFunctionSig; + /// @notice Rate function signature for token A + bytes tokenARateFunctionSig; + /// @notice Decimals function signature for token A + bytes tokenADecimalsFunctionSig; /// @notice Type of token B TokenType tokenBType; /// @notice Address of the oracle for token B address tokenBOracle; - /// @notice Function signature for token B - string tokenBFunctionSig; + /// @notice Rate function signature for token B + bytes tokenBRateFunctionSig; + /// @notice Decimals function signature for token B + bytes tokenBDecimalsFunctionSig; } /** @@ -75,6 +79,11 @@ contract SelfPeggingAssetFactory is UUPSUpgradeable, OwnableUpgradeable { */ uint256 public redeemFee; + /** + * @dev Default off peg fee multiplier for the pool. + */ + uint256 public offPegFeeMultiplier; + /** * @dev Default A parameter for the pool. */ @@ -130,6 +139,12 @@ contract SelfPeggingAssetFactory is UUPSUpgradeable, OwnableUpgradeable { */ event RedeemFeeModified(uint256 redeemFee); + /** + * @dev This event is emitted when the off peg fee multiplier is updated. + * @param offPegFeeMultiplier is the new value of the off peg fee multiplier. + */ + event OffPegFeeMultiplierModified(uint256 offPegFeeMultiplier); + /** * @dev This event is emitted when the A parameter is updated. * @param A is the new value of the A parameter. @@ -156,6 +171,7 @@ contract SelfPeggingAssetFactory is UUPSUpgradeable, OwnableUpgradeable { uint256 _mintFee, uint256 _swapFee, uint256 _redeemFee, + uint256 _offPegFeeMultiplier, uint256 _A, address _selfPeggingAssetBeacon, address _lpTokenBeacon, @@ -185,6 +201,7 @@ contract SelfPeggingAssetFactory is UUPSUpgradeable, OwnableUpgradeable { swapFee = _swapFee; redeemFee = _redeemFee; A = _A; + offPegFeeMultiplier = _offPegFeeMultiplier; } /** @@ -220,6 +237,14 @@ contract SelfPeggingAssetFactory is UUPSUpgradeable, OwnableUpgradeable { emit RedeemFeeModified(_redeemFee); } + /** + * @dev Set the off peg fee multiplier. + */ + function setOffPegFeeMultiplier(uint256 _offPegFeeMultiplier) external onlyOwner { + offPegFeeMultiplier = _offPegFeeMultiplier; + emit OffPegFeeMultiplierModified(_offPegFeeMultiplier); + } + /** * @dev Set the A parameter. */ @@ -261,9 +286,11 @@ contract SelfPeggingAssetFactory is UUPSUpgradeable, OwnableUpgradeable { exchangeRateProviders[0] = IExchangeRateProvider(constantExchangeRateProvider); } else if (argument.tokenAType == TokenType.Oracle) { require(argument.tokenAOracle != address(0), InvalidOracle()); - require(bytes(argument.tokenAFunctionSig).length > 0, InvalidFunctionSig()); - OracleExchangeRate oracleExchangeRate = - new OracleExchangeRate(argument.tokenAOracle, argument.tokenAFunctionSig); + require(bytes(argument.tokenARateFunctionSig).length > 0, InvalidFunctionSig()); + require(bytes(argument.tokenADecimalsFunctionSig).length > 0, InvalidFunctionSig()); + OracleExchangeRate oracleExchangeRate = new OracleExchangeRate( + argument.tokenAOracle, argument.tokenARateFunctionSig, argument.tokenADecimalsFunctionSig + ); exchangeRateProviders[0] = IExchangeRateProvider(oracleExchangeRate); } else if (argument.tokenAType == TokenType.ERC4626) { ERC4626ExchangeRate erc4626ExchangeRate = new ERC4626ExchangeRate(IERC4626(argument.tokenA)); @@ -274,9 +301,11 @@ contract SelfPeggingAssetFactory is UUPSUpgradeable, OwnableUpgradeable { exchangeRateProviders[1] = IExchangeRateProvider(constantExchangeRateProvider); } else if (argument.tokenBType == TokenType.Oracle) { require(argument.tokenBOracle != address(0), InvalidOracle()); - require(bytes(argument.tokenBFunctionSig).length > 0, InvalidFunctionSig()); - OracleExchangeRate oracleExchangeRate = - new OracleExchangeRate(argument.tokenBOracle, argument.tokenBFunctionSig); + require(bytes(argument.tokenBRateFunctionSig).length > 0, InvalidFunctionSig()); + require(bytes(argument.tokenBDecimalsFunctionSig).length > 0, InvalidFunctionSig()); + OracleExchangeRate oracleExchangeRate = new OracleExchangeRate( + argument.tokenBOracle, argument.tokenBRateFunctionSig, argument.tokenBDecimalsFunctionSig + ); exchangeRateProviders[1] = IExchangeRateProvider(oracleExchangeRate); } else if (argument.tokenBType == TokenType.ERC4626) { ERC4626ExchangeRate erc4626ExchangeRate = new ERC4626ExchangeRate(IERC4626(argument.tokenB)); @@ -285,7 +314,7 @@ contract SelfPeggingAssetFactory is UUPSUpgradeable, OwnableUpgradeable { bytes memory selfPeggingAssetInit = abi.encodeCall( SelfPeggingAsset.initialize, - (tokens, precisions, fees, LPToken(address(lpTokenProxy)), A, exchangeRateProviders) + (tokens, precisions, fees, offPegFeeMultiplier, LPToken(address(lpTokenProxy)), A, exchangeRateProviders) ); BeaconProxy selfPeggingAssetProxy = new BeaconProxy(selfPeggingAssetBeacon, selfPeggingAssetInit); SelfPeggingAsset selfPeggingAsset = SelfPeggingAsset(address(selfPeggingAssetProxy)); diff --git a/src/WLPToken.sol b/src/WLPToken.sol index b40c416..715424c 100644 --- a/src/WLPToken.sol +++ b/src/WLPToken.sol @@ -25,9 +25,10 @@ contract WLPToken is ERC4626Upgradeable { error InsufficientAllowance(); function initialize(ILPToken _lpToken) public initializer { - __ERC20_init("Wrapped LP Token", "wlpToken"); - __ERC4626_init(IERC20(address(_lpToken))); lpToken = _lpToken; + + __ERC20_init(name(), symbol()); + __ERC4626_init(IERC20(address(_lpToken))); } /** @@ -100,6 +101,22 @@ contract WLPToken is ERC4626Upgradeable { lpToken.transfer(receiver, assets); } + /** + * @dev Returns the name of the token. + * @return The name of the token. + */ + function name() public view override(ERC20Upgradeable, IERC20Metadata) returns (string memory) { + return string(abi.encodePacked("Wrapped ", lpToken.name())); + } + + /** + * @dev Returns the symbol of the token. + * @return The symbol of the token. + */ + function symbol() public view override(ERC20Upgradeable, IERC20Metadata) returns (string memory) { + return string(abi.encodePacked("w", lpToken.symbol())); + } + /** * @dev Converts an amount of lpToken to the equivalent amount of shares. * @param assets Amount of lpToken. diff --git a/src/interfaces/ILPToken.sol b/src/interfaces/ILPToken.sol index 181dc36..c6f68df 100644 --- a/src/interfaces/ILPToken.sol +++ b/src/interfaces/ILPToken.sol @@ -25,7 +25,7 @@ interface ILPToken is IERC20 { function addTotalSupply(uint256 _amount) external; /// @dev Remove the amount from the total supply - function removeTotalSupply(uint256 _amount) external; + function removeTotalSupply(uint256 _amount, bool isBuffer, bool withDebt) external; /// @dev Transfer the shares to the recipient function transferShares(address _recipient, uint256 _sharesAmount) external returns (uint256); @@ -51,6 +51,12 @@ interface ILPToken is IERC20 { // @dev Add to buffer function addBuffer(uint256 _amount) external; + /// @dev Name of the token + function name() external view returns (string memory); + + /// @dev Symbol of the token + function symbol() external view returns (string memory); + /// @dev Get the total amount of shares function totalShares() external view returns (uint256); diff --git a/src/misc/OracleExchangeRate.sol b/src/misc/OracleExchangeRate.sol index 9e43fa3..16f114b 100644 --- a/src/misc/OracleExchangeRate.sol +++ b/src/misc/OracleExchangeRate.sol @@ -10,23 +10,25 @@ contract OracleExchangeRate is IExchangeRateProvider { /// @dev Oracle address address public oracle; - /// @dev Function signature - string public func; + /// @dev Rate function signature + bytes public rateFunc; + + /// @dev Decimals function signature + bytes public decimalsFunc; /// @dev Error thrown when the internal call failed error InternalCallFailed(); /// @dev Initialize the contract - constructor(address _oracle, string memory _func) { + constructor(address _oracle, bytes memory _rateFunc, bytes memory _decimalsFunc) { oracle = _oracle; - func = _func; + rateFunc = _rateFunc; + decimalsFunc = _decimalsFunc; } /// @dev Get the exchange rate function exchangeRate() external view returns (uint256) { - bytes memory data = abi.encodeWithSignature(string(abi.encodePacked(func, "()"))); - - (bool success, bytes memory result) = oracle.staticcall(data); + (bool success, bytes memory result) = oracle.staticcall(rateFunc); require(success, InternalCallFailed()); uint256 decodedResult = abi.decode(result, (uint256)); @@ -35,7 +37,12 @@ contract OracleExchangeRate is IExchangeRateProvider { } /// @dev Get the exchange rate decimals - function exchangeRateDecimals() external pure returns (uint256) { - return 18; + function exchangeRateDecimals() external view returns (uint256) { + (bool success, bytes memory result) = oracle.staticcall(decimalsFunc); + require(success, InternalCallFailed()); + + uint256 decodedResult = abi.decode(result, (uint256)); + + return decodedResult; } } diff --git a/src/mock/MockOracle.sol b/src/mock/MockOracle.sol index a9e29f1..e376a9a 100644 --- a/src/mock/MockOracle.sol +++ b/src/mock/MockOracle.sol @@ -2,7 +2,17 @@ pragma solidity ^0.8.28; contract MockOracle { - function rate() external pure returns (uint256) { - return 1e18; + uint256 internal _rate = 1e18; + + function setRate(uint256 newRate) external { + _rate = newRate; + } + + function rate() external view returns (uint256) { + return _rate; + } + + function decimals() external pure returns (uint256) { + return 18; } } diff --git a/test/Factory.t.sol b/test/Factory.t.sol index 9f15bff..2ba07ee 100644 --- a/test/Factory.t.sol +++ b/test/Factory.t.sol @@ -42,6 +42,7 @@ contract FactoryTest is Test { 0, 0, 0, + 0, 100, selfPeggingAssetBeacon, lpTokenBeacon, @@ -59,10 +60,12 @@ contract FactoryTest is Test { tokenB: address(tokenB), tokenAType: SelfPeggingAssetFactory.TokenType.Standard, tokenAOracle: address(0), - tokenAFunctionSig: "", + tokenARateFunctionSig: new bytes(0), + tokenADecimalsFunctionSig: new bytes(0), tokenBType: SelfPeggingAssetFactory.TokenType.Standard, tokenBOracle: address(0), - tokenBFunctionSig: "" + tokenBRateFunctionSig: new bytes(0), + tokenBDecimalsFunctionSig: new bytes(0) }); vm.recordLogs(); @@ -102,7 +105,7 @@ contract FactoryTest is Test { selfPeggingAsset.mint(amounts, 0); - assertEq(poolToken.balanceOf(initialMinter), 200e18); + assertEq(poolToken.balanceOf(initialMinter), 200e18 - 1000 wei); assertNotEq(address(wrappedPoolToken), address(0)); } @@ -121,10 +124,12 @@ contract FactoryTest is Test { tokenB: address(vaultTokenB), tokenAType: SelfPeggingAssetFactory.TokenType.ERC4626, tokenAOracle: address(0), - tokenAFunctionSig: "", + tokenARateFunctionSig: new bytes(0), + tokenADecimalsFunctionSig: new bytes(0), tokenBType: SelfPeggingAssetFactory.TokenType.ERC4626, tokenBOracle: address(0), - tokenBFunctionSig: "" + tokenBRateFunctionSig: new bytes(0), + tokenBDecimalsFunctionSig: new bytes(0) }); vm.recordLogs(); @@ -170,7 +175,7 @@ contract FactoryTest is Test { selfPeggingAsset.mint(amounts, 0); - assertEq(poolToken.balanceOf(initialMinter), 200e18); + assertEq(poolToken.balanceOf(initialMinter), 200e18 - 1000 wei); assertNotEq(address(wrappedPoolToken), address(0)); } @@ -185,10 +190,12 @@ contract FactoryTest is Test { tokenB: address(tokenB), tokenAType: SelfPeggingAssetFactory.TokenType.Oracle, tokenAOracle: address(oracle), - tokenAFunctionSig: "rate", + tokenARateFunctionSig: abi.encodePacked(MockOracle.rate.selector), + tokenADecimalsFunctionSig: abi.encodePacked(MockOracle.decimals.selector), tokenBType: SelfPeggingAssetFactory.TokenType.Oracle, tokenBOracle: address(oracle), - tokenBFunctionSig: "rate" + tokenBRateFunctionSig: abi.encodePacked(MockOracle.rate.selector), + tokenBDecimalsFunctionSig: abi.encodePacked(MockOracle.decimals.selector) }); vm.recordLogs(); @@ -228,7 +235,7 @@ contract FactoryTest is Test { selfPeggingAsset.mint(amounts, 0); - assertEq(poolToken.balanceOf(initialMinter), 200e18); + assertEq(poolToken.balanceOf(initialMinter), 200e18 - 1000 wei); assertNotEq(address(wrappedPoolToken), address(0)); } } diff --git a/test/LPToken.t.sol b/test/LPToken.t.sol index d602e7a..4880b0d 100644 --- a/test/LPToken.t.sol +++ b/test/LPToken.t.sol @@ -53,8 +53,8 @@ contract LPTokenTest is Test { assertEq(lpToken.totalSupply(), amount); assertEq(lpToken.totalShares(), amount); - assertEq(lpToken.sharesOf(user1), amount); - assertEq(lpToken.balanceOf(user1), amount); + assertEq(lpToken.sharesOf(user1), amount - lpToken.NUMBER_OF_DEAD_SHARES()); + assertEq(lpToken.balanceOf(user1), amount - lpToken.NUMBER_OF_DEAD_SHARES()); } function test_MintSharesMultipleUsers() public { @@ -75,8 +75,8 @@ contract LPTokenTest is Test { uint256 totalAmount = amount1 + amount2 + amount3; assertEq(lpToken.totalSupply(), totalAmount); assertEq(lpToken.totalShares(), totalAmount); - assertEq(lpToken.sharesOf(user1), amount1); - assertEq(lpToken.balanceOf(user1), amount1); + assertEq(lpToken.sharesOf(user1), amount1 - lpToken.NUMBER_OF_DEAD_SHARES()); + assertEq(lpToken.balanceOf(user1), amount1 - lpToken.NUMBER_OF_DEAD_SHARES()); assertEq(lpToken.sharesOf(user2), amount2); assertEq(lpToken.balanceOf(user2), amount2); assertEq(lpToken.sharesOf(user3), amount3); @@ -99,8 +99,8 @@ contract LPTokenTest is Test { uint256 deltaAmount = amount1 - amount2; assertEq(lpToken.totalSupply(), deltaAmount); assertEq(lpToken.totalShares(), deltaAmount); - assertEq(lpToken.sharesOf(user1), deltaAmount); - assertEq(lpToken.balanceOf(user1), deltaAmount); + assertEq(lpToken.sharesOf(user1), deltaAmount - lpToken.NUMBER_OF_DEAD_SHARES()); + assertEq(lpToken.balanceOf(user1), deltaAmount - lpToken.NUMBER_OF_DEAD_SHARES()); } function test_AddTotalSupply() public { @@ -121,8 +121,11 @@ contract LPTokenTest is Test { assertEq(lpToken.totalSupply(), totalAmount); assertEq(lpToken.totalShares(), amount1); assertEq(lpToken.totalRewards(), amount2); - assertEq(lpToken.sharesOf(user), amount1); - assertEq(lpToken.balanceOf(user), totalAmount); + assertEq(lpToken.sharesOf(user), amount1 - lpToken.NUMBER_OF_DEAD_SHARES()); + + /// 1000 shares worth of supply goes to address(0) when amount 1 was minted + /// + assertEq(lpToken.balanceOf(user), (amount1 - lpToken.NUMBER_OF_DEAD_SHARES()) + (amount2 - 500 wei)); } function testApprove() public { @@ -210,9 +213,9 @@ contract LPTokenTest is Test { // Assertions assertEq(lpToken.totalSupply(), amount1); assertEq(lpToken.totalShares(), amount1); - assertEq(lpToken.sharesOf(user1), deltaAmount); + assertEq(lpToken.sharesOf(user1), deltaAmount - lpToken.NUMBER_OF_DEAD_SHARES()); assertEq(lpToken.sharesOf(user2), amount2); - assertEq(lpToken.balanceOf(user1), deltaAmount); + assertEq(lpToken.balanceOf(user1), deltaAmount - lpToken.NUMBER_OF_DEAD_SHARES()); assertEq(lpToken.balanceOf(user2), amount2); } @@ -242,9 +245,9 @@ contract LPTokenTest is Test { // Assertions assertEq(lpToken.totalSupply(), amount1); assertEq(lpToken.totalShares(), amount1); - assertEq(lpToken.sharesOf(user1), deltaAmount); + assertEq(lpToken.sharesOf(user1), deltaAmount - lpToken.NUMBER_OF_DEAD_SHARES()); assertEq(lpToken.sharesOf(user2), amount2); - assertEq(lpToken.balanceOf(user1), deltaAmount); + assertEq(lpToken.balanceOf(user1), deltaAmount - lpToken.NUMBER_OF_DEAD_SHARES()); assertEq(lpToken.balanceOf(user2), amount2); assertEq(lpToken.allowance(user1, spender), deltaAmount); } diff --git a/test/SelfPeggingAsset.t.sol b/test/SelfPeggingAsset.t.sol index 367eb2b..f322229 100644 --- a/test/SelfPeggingAsset.t.sol +++ b/test/SelfPeggingAsset.t.sol @@ -58,7 +58,7 @@ contract SelfPeggingAssetTest is Test { exchangeRateProviders[0] = exchangeRateProvider; exchangeRateProviders[1] = exchangeRateProvider; - pool.initialize(tokens, precisions, fees, lpToken, A, exchangeRateProviders); + pool.initialize(tokens, precisions, fees, 0, lpToken, A, exchangeRateProviders); pool.transferOwnership(owner); vm.prank(owner); @@ -97,8 +97,8 @@ contract SelfPeggingAssetTest is Test { assertEq(0, WETH.balanceOf(user)); assertEq(0, frxETH.balanceOf(user)); - assertEq(totalAmount, lpToken.balanceOf(user)); - assertEq(lpTokensMinted, lpToken.sharesOf(user)); + assertIsCloseTo(totalAmount, lpToken.balanceOf(user) + lpToken.NUMBER_OF_DEAD_SHARES(), 2 wei); + assertEq(lpTokensMinted, lpToken.sharesOf(user) + lpToken.NUMBER_OF_DEAD_SHARES()); assertEq(totalAmount, lpToken.totalSupply()); } @@ -142,7 +142,7 @@ contract SelfPeggingAssetTest is Test { _precisions[0] = 1; _precisions[1] = 1; - _pool.initialize(_tokens, _precisions, _fees, _lpToken, A, exchangeRateProviders); + _pool.initialize(_tokens, _precisions, _fees, 0, _lpToken, A, exchangeRateProviders); vm.prank(owner); _lpToken.addPool(address(_pool)); @@ -499,6 +499,155 @@ contract SelfPeggingAssetTest is Test { assertEq(pool.A(), 90); } + function testDynamicFeeForSwap() external { + WETH.mint(user, 105e18); + frxETH.mint(user, 85e18); + + vm.startPrank(user); + WETH.approve(address(pool), 105e18); + frxETH.approve(address(pool), 85e18); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 105e18; + amounts[1] = 85e18; + + pool.mint(amounts, 0); + vm.stopPrank(); + + frxETH.mint(user2, 8e18); + vm.startPrank(user2); + frxETH.approve(address(pool), 8e18); + vm.stopPrank(); + + (uint256 exchangeAmount,) = pool.getSwapAmount(1, 0, 8e18); + + vm.prank(owner); + pool.setOffPegFeeMultiplier(2e10); + + assertEq(WETH.balanceOf(user2), 0); + assertEq(frxETH.balanceOf(user2), 8e18); + + assertEq(WETH.balanceOf(address(pool)), 105e18); + assertEq(frxETH.balanceOf(address(pool)), 85e18); + + assertEq(pool.balances(0), 105e18); + assertEq(pool.balances(1), 85e18); + + assertEq(pool.totalSupply(), 189.994704791049550806e18); + + assertEq(pool.totalSupply(), lpToken.totalSupply()); + + vm.prank(user2); + pool.swap(1, 0, 8e18, 0); + + assertLt(WETH.balanceOf(user2), exchangeAmount); + } + + function test_LossHandling() external { + MockExchangeRateProvider rETHExchangeRateProvider = new MockExchangeRateProvider(1e18, 18); + MockExchangeRateProvider wstETHExchangeRateProvider = new MockExchangeRateProvider(1e18, 18); + + MockToken rETH = new MockToken("rETH", "rETH", 18); + MockToken wstETH = new MockToken("wstETH", "wstETH", 18); + + address[] memory _tokens = new address[](2); + _tokens[0] = address(rETH); + _tokens[1] = address(wstETH); + + IExchangeRateProvider[] memory exchangeRateProviders = new IExchangeRateProvider[](2); + exchangeRateProviders[0] = IExchangeRateProvider(rETHExchangeRateProvider); + exchangeRateProviders[1] = IExchangeRateProvider(wstETHExchangeRateProvider); + + SelfPeggingAsset _pool = new SelfPeggingAsset(); + + LPToken _lpToken = new LPToken(); + _lpToken.initialize("LP Token", "LPT"); + _lpToken.transferOwnership(owner); + + uint256[] memory _fees = new uint256[](3); + _fees[0] = 0; + _fees[1] = 0; + _fees[2] = 0; + + uint256[] memory _precisions = new uint256[](2); + _precisions[0] = 1; + _precisions[1] = 1; + + _pool.initialize(_tokens, _precisions, _fees, 0, _lpToken, A, exchangeRateProviders); + _pool.transferOwnership(owner); + + vm.prank(owner); + _lpToken.addPool(address(_pool)); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 100e18; + amounts[1] = 100e18; + + // Mint Liquidity + rETH.mint(user, 100e18); + wstETH.mint(user, 100e18); + + vm.startPrank(user); + rETH.approve(address(_pool), 100e18); + wstETH.approve(address(_pool), 100e18); + + _pool.mint(amounts, 0); + vm.stopPrank(); + + // swap 1 rETH to wstETH + rETH.mint(user2, 1e18); + + vm.startPrank(user2); + rETH.approve(address(_pool), 1e18); + _pool.swap(0, 1, 1e18, 0); + vm.stopPrank(); + + uint256 rETHBalance = rETH.balanceOf(user2); + uint256 wstETHBalance = wstETH.balanceOf(user2); + + assertEq(rETHBalance, 0); + assertIsCloseTo(wstETHBalance, 1e18, 0.00005 ether); + + // Set buffer percentage to 5% + vm.prank(owner); + _lpToken.setBuffer(0.05e10); + vm.stopPrank(); + + // Add yield + rETHExchangeRateProvider.newRate(2e18); + _pool.rebase(); + + assertIsCloseTo(_lpToken.bufferAmount(), 5e18, 0.05 ether); + assertEq(_lpToken.bufferBadDebt(), 0); + + // Drop the exchange rate by 1% so that the pool is in loss and buffer can cover the loss + rETHExchangeRateProvider.newRate(1.98e18); + _pool.rebase(); + + assertIsCloseTo(_lpToken.bufferAmount(), 3e18, 0.03 ether); + assertIsCloseTo(_lpToken.bufferBadDebt(), 2e18, 0.02 ether); + + // Drop the exchange rate by 90% so that the pool is in loss and buffer can't cover the loss + rETHExchangeRateProvider.newRate(0.2e18); + vm.expectRevert(); + _pool.rebase(); + + // Trigger negative rebase + assertIsCloseTo(_lpToken.totalSupply(), 295e18, 0.9e18); + vm.startPrank(owner); + _pool.setAdmin(owner, true); + _pool.pause(); + _pool.distributeLoss(); + + assertIsCloseTo(_lpToken.totalSupply(), 113e18, 1e18); + + // Recover bad debt + assertNotEq(_lpToken.bufferBadDebt(), 0); + rETHExchangeRateProvider.newRate(1e18); + _pool.rebase(); + assertEq(_lpToken.bufferBadDebt(), 0); + } + function assertFee(uint256 totalAmount, uint256 feeAmount, uint256 fee) internal view { uint256 expectedFee = totalAmount * fee / feeDenominator; assertEq(feeAmount, expectedFee); diff --git a/test/WLPToken.t.sol b/test/WLPToken.t.sol index 0fbf51f..ae5bacd 100644 --- a/test/WLPToken.t.sol +++ b/test/WLPToken.t.sol @@ -65,7 +65,7 @@ contract WLPTokenTest is Test { // Assertions assertEq(lpToken.totalSupply(), targetTotalSupply); assertEq(lpToken.totalShares(), amount1); - assertEq(lpToken.sharesOf(user), amount1 - wlpTokenTargetAmount); + assertEq(lpToken.sharesOf(user), amount1 - wlpTokenTargetAmount - lpToken.NUMBER_OF_DEAD_SHARES()); assertEq(lpToken.sharesOf(address(wlpToken)), wlpTokenTargetAmount); assertEq(lpToken.balanceOf(address(wlpToken)), amountToWrap); assertEq(wlpToken.balanceOf(user), wlpTokenTargetAmount); @@ -106,7 +106,7 @@ contract WLPTokenTest is Test { // Assertions assertEq(lpToken.totalSupply(), targetTotalSupply); assertEq(lpToken.totalShares(), amount1); - assertEq(lpToken.sharesOf(user), amount1); + assertEq(lpToken.sharesOf(user), amount1 - lpToken.NUMBER_OF_DEAD_SHARES()); assertEq(lpToken.sharesOf(address(wlpToken)), 0); assertEq(lpToken.balanceOf(address(wlpToken)), 0); assertEq(wlpToken.balanceOf(user), 0); @@ -149,7 +149,7 @@ contract WLPTokenTest is Test { // Assertions assertEq(lpToken.totalSupply(), targetTotalSupply); assertEq(lpToken.totalShares(), amount1); - assertEq(lpToken.sharesOf(user), amount1); + assertEq(lpToken.sharesOf(user), amount1 - lpToken.NUMBER_OF_DEAD_SHARES()); assertEq(lpToken.sharesOf(address(wlpToken)), 0); assertEq(lpToken.balanceOf(address(wlpToken)), 0); assertEq(wlpToken.balanceOf(user), 0);