From db4d113b7d693b7b0109b9923893eaffe71979e1 Mon Sep 17 00:00:00 2001 From: sherlock-admin Date: Wed, 4 Dec 2024 15:54:00 +0000 Subject: [PATCH 1/2] Fix Review --- .../goodDollar/BancorExchangeProvider.sol | 100 ++- .../goodDollar/GoodDollarExchangeProvider.sol | 40 +- .../GoodDollarExpansionController.sol | 61 +- .../IGoodDollarExchangeProvider.sol | 3 +- .../IGoodDollarExpansionController.sol | 19 +- mento-core/contracts/interfaces/IReserve.sol | 2 + .../contracts/libraries/TradingLimits.sol | 8 +- mento-core/test/fork/BaseForkTest.sol | 13 +- mento-core/test/fork/ForkTests.t.sol | 8 +- .../goodDollar/BancorExchangeProvider.t.sol | 728 ++++++++++++++---- .../GoodDollarExchangeProvider.t.sol | 113 +-- .../GoodDollarExpansionController.t.sol | 183 ++++- .../test/unit/libraries/TradingLimits.t.sol | 30 +- .../GoodDollarExpansionControllerHarness.sol | 8 +- 14 files changed, 1056 insertions(+), 260 deletions(-) diff --git a/mento-core/contracts/goodDollar/BancorExchangeProvider.sol b/mento-core/contracts/goodDollar/BancorExchangeProvider.sol index 9539115..6f2ebde 100644 --- a/mento-core/contracts/goodDollar/BancorExchangeProvider.sol +++ b/mento-core/contracts/goodDollar/BancorExchangeProvider.sol @@ -122,6 +122,13 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B ) external view virtual returns (uint256 amountOut) { PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledAmountIn = amountIn * tokenPrecisionMultipliers[tokenIn]; + + if (tokenIn == exchange.tokenAddress) { + require(scaledAmountIn < exchange.tokenSupply, "amountIn is greater than tokenSupply"); + // apply exit contribution + scaledAmountIn = (scaledAmountIn * (MAX_WEIGHT - exchange.exitContribution)) / MAX_WEIGHT; + } + uint256 scaledAmountOut = _getScaledAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); amountOut = scaledAmountOut / tokenPrecisionMultipliers[tokenOut]; return amountOut; @@ -137,19 +144,27 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledAmountOut = amountOut * tokenPrecisionMultipliers[tokenOut]; uint256 scaledAmountIn = _getScaledAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); - amountIn = scaledAmountIn / tokenPrecisionMultipliers[tokenIn]; + + if (tokenIn == exchange.tokenAddress) { + // apply exit contribution + scaledAmountIn = (scaledAmountIn * MAX_WEIGHT) / (MAX_WEIGHT - exchange.exitContribution); + require(scaledAmountIn < exchange.tokenSupply, "amountIn is greater than tokenSupply"); + } + + amountIn = divAndRoundUp(scaledAmountIn, tokenPrecisionMultipliers[tokenIn]); return amountIn; } /// @inheritdoc IBancorExchangeProvider function currentPrice(bytes32 exchangeId) public view returns (uint256 price) { // calculates: reserveBalance / (tokenSupply * reserveRatio) - require(exchanges[exchangeId].reserveAsset != address(0), "Exchange does not exist"); PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledReserveRatio = uint256(exchange.reserveRatio) * 1e10; + UD60x18 denominator = wrap(exchange.tokenSupply).mul(wrap(scaledReserveRatio)); - price = unwrap(wrap(exchange.reserveBalance).div(denominator)); - return price; + uint256 priceScaled = unwrap(wrap(exchange.reserveBalance).div(denominator)); + + price = priceScaled / tokenPrecisionMultipliers[exchange.reserveAsset]; } /* ============================================================ */ @@ -165,9 +180,9 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B /// @inheritdoc IBancorExchangeProvider function setReserve(address _reserve) public onlyOwner { - require(address(_reserve) != address(0), "Reserve address must be set"); + require(_reserve != address(0), "Reserve address must be set"); reserve = IReserve(_reserve); - emit ReserveUpdated(address(_reserve)); + emit ReserveUpdated(_reserve); } /// @inheritdoc IBancorExchangeProvider @@ -197,8 +212,21 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B ) public virtual onlyBroker returns (uint256 amountOut) { PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledAmountIn = amountIn * tokenPrecisionMultipliers[tokenIn]; + uint256 exitContribution = 0; + + if (tokenIn == exchange.tokenAddress) { + require(scaledAmountIn < exchange.tokenSupply, "amountIn is greater than tokenSupply"); + // apply exit contribution + exitContribution = (scaledAmountIn * exchange.exitContribution) / MAX_WEIGHT; + scaledAmountIn -= exitContribution; + } + uint256 scaledAmountOut = _getScaledAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); + executeSwap(exchangeId, tokenIn, scaledAmountIn, scaledAmountOut); + if (exitContribution > 0) { + _accountExitContribution(exchangeId, exitContribution); + } amountOut = scaledAmountOut / tokenPrecisionMultipliers[tokenOut]; return amountOut; @@ -214,9 +242,26 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledAmountOut = amountOut * tokenPrecisionMultipliers[tokenOut]; uint256 scaledAmountIn = _getScaledAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); + + uint256 exitContribution = 0; + uint256 scaledAmountInWithExitContribution = scaledAmountIn; + + if (tokenIn == exchange.tokenAddress) { + // apply exit contribution + scaledAmountInWithExitContribution = (scaledAmountIn * MAX_WEIGHT) / (MAX_WEIGHT - exchange.exitContribution); + require( + scaledAmountInWithExitContribution < exchange.tokenSupply, + "amountIn required is greater than tokenSupply" + ); + exitContribution = scaledAmountInWithExitContribution - scaledAmountIn; + } + executeSwap(exchangeId, tokenIn, scaledAmountIn, scaledAmountOut); + if (exitContribution > 0) { + _accountExitContribution(exchangeId, exitContribution); + } - amountIn = scaledAmountIn / tokenPrecisionMultipliers[tokenIn]; + amountIn = divAndRoundUp(scaledAmountInWithExitContribution, tokenPrecisionMultipliers[tokenIn]); return amountIn; } @@ -242,6 +287,9 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B tokenPrecisionMultipliers[exchange.reserveAsset] = 10 ** (18 - uint256(reserveAssetDecimals)); tokenPrecisionMultipliers[exchange.tokenAddress] = 10 ** (18 - uint256(tokenDecimals)); + exchange.reserveBalance = exchange.reserveBalance * tokenPrecisionMultipliers[exchange.reserveAsset]; + exchange.tokenSupply = exchange.tokenSupply * tokenPrecisionMultipliers[exchange.tokenAddress]; + exchanges[exchangeId] = exchange; exchangeIds.push(exchangeId); emit ExchangeCreated(exchangeId, exchange.reserveAsset, exchange.tokenAddress); @@ -262,7 +310,7 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B function _setExitContribution(bytes32 exchangeId, uint32 exitContribution) internal { require(exchanges[exchangeId].reserveAsset != address(0), "Exchange does not exist"); - require(exitContribution <= MAX_WEIGHT, "Exit contribution is too high"); + require(exitContribution < MAX_WEIGHT, "Exit contribution is too high"); PoolExchange storage exchange = exchanges[exchangeId]; exchange.exitContribution = exitContribution; @@ -290,6 +338,37 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B exchanges[exchangeId].tokenSupply = exchange.tokenSupply; } + /** + * @notice Accounting of exit contribution on a swap. + * @dev Accounting of exit contribution without changing the current price of an exchange. + * this is done by updating the reserve ratio and subtracting the exit contribution from the token supply. + * Formula: newRatio = (Supply * oldRatio) / (Supply - exitContribution) + * @param exchangeId The ID of the pool + * @param exitContribution The amount of the token to be removed from the pool, scaled to 18 decimals + */ + function _accountExitContribution(bytes32 exchangeId, uint256 exitContribution) internal { + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledReserveRatio = uint256(exchange.reserveRatio) * 1e10; + UD60x18 nominator = wrap(exchange.tokenSupply).mul(wrap(scaledReserveRatio)); + UD60x18 denominator = wrap(exchange.tokenSupply - exitContribution); + UD60x18 newRatioScaled = nominator.div(denominator); + + uint256 newRatio = unwrap(newRatioScaled) / 1e10; + + exchanges[exchangeId].reserveRatio = uint32(newRatio); + exchanges[exchangeId].tokenSupply -= exitContribution; + } + + /** + * @notice Division and rounding up if there is a remainder + * @param a The dividend + * @param b The divisor + * @return The result of the division rounded up + */ + function divAndRoundUp(uint256 a, uint256 b) internal pure returns (uint256) { + return (a / b) + (a % b > 0 ? 1 : 0); + } + /** * @notice Calculate the scaledAmountIn of tokenIn for a given scaledAmountOut of tokenOut * @param exchange The pool exchange to operate on @@ -307,8 +386,6 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B if (tokenIn == exchange.reserveAsset) { scaledAmountIn = fundCost(exchange.tokenSupply, exchange.reserveBalance, exchange.reserveRatio, scaledAmountOut); } else { - // apply exit contribution - scaledAmountOut = (scaledAmountOut * MAX_WEIGHT) / (MAX_WEIGHT - exchange.exitContribution); scaledAmountIn = saleCost(exchange.tokenSupply, exchange.reserveBalance, exchange.reserveRatio, scaledAmountOut); } } @@ -341,8 +418,6 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B exchange.reserveRatio, scaledAmountIn ); - // apply exit contribution - scaledAmountOut = (scaledAmountOut * (MAX_WEIGHT - exchange.exitContribution)) / MAX_WEIGHT; } } @@ -362,6 +437,7 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B require(exchange.reserveRatio > 1, "Reserve ratio is too low"); require(exchange.reserveRatio <= MAX_WEIGHT, "Reserve ratio is too high"); require(exchange.exitContribution <= MAX_WEIGHT, "Exit contribution is too high"); + require(exchange.reserveBalance > 0, "Reserve balance must be greater than 0"); } /** diff --git a/mento-core/contracts/goodDollar/GoodDollarExchangeProvider.sol b/mento-core/contracts/goodDollar/GoodDollarExchangeProvider.sol index b744fcc..088e770 100644 --- a/mento-core/contracts/goodDollar/GoodDollarExchangeProvider.sol +++ b/mento-core/contracts/goodDollar/GoodDollarExchangeProvider.sol @@ -144,7 +144,12 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan PoolExchange memory exchange = getPoolExchange(exchangeId); UD60x18 scaledRatio = wrap(uint256(exchange.reserveRatio) * 1e10); - UD60x18 newRatio = scaledRatio.mul(wrap(reserveRatioScalar)); + + // The division and multiplication by 1e10 here ensures that the new ratio used for calculating the amount to mint + // is the same as the one set in the exchange but only scaled to 18 decimals. + // Ignored, because the division and multiplication by 1e10 is needed see comment above. + // slither-disable-next-line divide-before-multiply + UD60x18 newRatio = wrap((unwrap(scaledRatio.mul(wrap(reserveRatioScalar))) / 1e10) * 1e10); uint32 newRatioUint = uint32(unwrap(newRatio) / 1e10); require(newRatioUint > 0, "New ratio must be greater than 0"); @@ -175,14 +180,13 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan ) external onlyExpansionController whenNotPaused returns (uint256 amountToMint) { PoolExchange memory exchange = getPoolExchange(exchangeId); - uint256 reserveinterestScaled = reserveInterest * tokenPrecisionMultipliers[exchange.reserveAsset]; uint256 amountToMintScaled = unwrap( - wrap(reserveinterestScaled).mul(wrap(exchange.tokenSupply)).div(wrap(exchange.reserveBalance)) + wrap(reserveInterest).mul(wrap(exchange.tokenSupply)).div(wrap(exchange.reserveBalance)) ); amountToMint = amountToMintScaled / tokenPrecisionMultipliers[exchange.tokenAddress]; exchanges[exchangeId].tokenSupply += amountToMintScaled; - exchanges[exchangeId].reserveBalance += reserveinterestScaled; + exchanges[exchangeId].reserveBalance += reserveInterest; return amountToMint; } @@ -190,21 +194,31 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan /** * @inheritdoc IGoodDollarExchangeProvider * @dev Calculates the new reserve ratio needed to mint the G$ reward while keeping the current price the same. - * calculation: newRatio = reserveBalance / (tokenSupply + reward) * currentPrice + * calculation: newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward) */ - function updateRatioForReward(bytes32 exchangeId, uint256 reward) external onlyExpansionController whenNotPaused { + function updateRatioForReward( + bytes32 exchangeId, + uint256 reward, + uint256 maxSlippagePercentage + ) external onlyExpansionController whenNotPaused { PoolExchange memory exchange = getPoolExchange(exchangeId); - uint256 currentPriceScaled = currentPrice(exchangeId) * tokenPrecisionMultipliers[exchange.reserveAsset]; - uint256 rewardScaled = reward * tokenPrecisionMultipliers[exchange.tokenAddress]; + uint256 scaledRatio = uint256(exchange.reserveRatio) * 1e10; + uint256 scaledReward = reward * tokenPrecisionMultipliers[exchange.tokenAddress]; + + UD60x18 numerator = wrap(exchange.tokenSupply).mul(wrap(scaledRatio)); + UD60x18 denominator = wrap(exchange.tokenSupply).add(wrap(scaledReward)); + uint256 newScaledRatio = unwrap(numerator.div(denominator)); + + uint32 newRatioUint = uint32(newScaledRatio / 1e10); + + require(newRatioUint > 0, "New ratio must be greater than 0"); - UD60x18 numerator = wrap(exchange.reserveBalance); - UD60x18 denominator = wrap(exchange.tokenSupply + rewardScaled).mul(wrap(currentPriceScaled)); - uint256 newRatioScaled = unwrap(numerator.div(denominator)); + uint256 allowedSlippage = (exchange.reserveRatio * maxSlippagePercentage) / MAX_WEIGHT; + require(exchange.reserveRatio - newRatioUint <= allowedSlippage, "Slippage exceeded"); - uint32 newRatioUint = uint32(newRatioScaled / 1e10); exchanges[exchangeId].reserveRatio = newRatioUint; - exchanges[exchangeId].tokenSupply += rewardScaled; + exchanges[exchangeId].tokenSupply += scaledReward; emit ReserveRatioUpdated(exchangeId, newRatioUint); } diff --git a/mento-core/contracts/goodDollar/GoodDollarExpansionController.sol b/mento-core/contracts/goodDollar/GoodDollarExpansionController.sol index f753d06..a78c1cb 100644 --- a/mento-core/contracts/goodDollar/GoodDollarExpansionController.sol +++ b/mento-core/contracts/goodDollar/GoodDollarExpansionController.sol @@ -5,10 +5,10 @@ import { IGoodDollarExpansionController } from "contracts/interfaces/IGoodDollar import { IGoodDollarExchangeProvider } from "contracts/interfaces/IGoodDollarExchangeProvider.sol"; import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; import { IERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "openzeppelin-contracts-next/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { IGoodDollar } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; import { IDistributionHelper } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; -import { PausableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/security/PausableUpgradeable.sol"; import { OwnableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; import { unwrap, wrap, powu } from "prb/math/UD60x18.sol"; @@ -16,13 +16,16 @@ import { unwrap, wrap, powu } from "prb/math/UD60x18.sol"; * @title GoodDollarExpansionController * @notice Provides functionality to expand the supply of GoodDollars. */ -contract GoodDollarExpansionController is IGoodDollarExpansionController, PausableUpgradeable, OwnableUpgradeable { +contract GoodDollarExpansionController is IGoodDollarExpansionController, OwnableUpgradeable { /* ========================================================= */ /* ==================== State Variables ==================== */ /* ========================================================= */ - // MAX_WEIGHT is the max rate that can be assigned to an exchange - uint256 public constant MAX_WEIGHT = 1e18; + // EXPANSION_MAX_WEIGHT is the max rate that can be assigned to an exchange + uint256 public constant EXPANSION_MAX_WEIGHT = 1e18; + + // BANCOR_MAX_WEIGHT is used for BPS calculations in GoodDollarExchangeProvider + uint32 public constant BANCOR_MAX_WEIGHT = 1e8; // Address of the distribution helper contract IDistributionHelper public distributionHelper; @@ -63,7 +66,6 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab address _reserve, address _avatar ) public initializer { - __Pausable_init(); __Ownable_init(); setGoodDollarExchangeProvider(_goodDollarExchangeProvider); @@ -123,7 +125,7 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab /// @inheritdoc IGoodDollarExpansionController function setExpansionConfig(bytes32 exchangeId, uint64 expansionRate, uint32 expansionFrequency) external onlyAvatar { - require(expansionRate < MAX_WEIGHT, "Expansion rate must be less than 100%"); + require(expansionRate < EXPANSION_MAX_WEIGHT, "Expansion rate must be less than 100%"); require(expansionRate > 0, "Expansion rate must be greater than 0"); require(expansionFrequency > 0, "Expansion frequency must be greater than 0"); @@ -134,19 +136,21 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab } /// @inheritdoc IGoodDollarExpansionController - function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external { + function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external returns (uint256 amountMinted) { require(reserveInterest > 0, "Reserve interest must be greater than 0"); IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) .getPoolExchange(exchangeId); - uint256 amountToMint = goodDollarExchangeProvider.mintFromInterest(exchangeId, reserveInterest); - require(IERC20(exchange.reserveAsset).transferFrom(msg.sender, reserve, reserveInterest), "Transfer failed"); - IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountToMint); + + uint256 reserveInterestScaled = reserveInterest * (10 ** (18 - IERC20Metadata(exchange.reserveAsset).decimals())); + amountMinted = goodDollarExchangeProvider.mintFromInterest(exchangeId, reserveInterestScaled); + + IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountMinted); // Ignored, because contracts only interacts with trusted contracts and tokens // slither-disable-next-line reentrancy-events - emit InterestUBIMinted(exchangeId, amountToMint); + emit InterestUBIMinted(exchangeId, amountMinted); } /// @inheritdoc IGoodDollarExpansionController @@ -154,7 +158,9 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) .getPoolExchange(exchangeId); - uint256 contractReserveBalance = IERC20(exchange.reserveAsset).balanceOf(reserve); + uint256 contractReserveBalance = IERC20(exchange.reserveAsset).balanceOf(reserve) * + (10 ** (18 - IERC20Metadata(exchange.reserveAsset).decimals())); + uint256 additionalReserveBalance = contractReserveBalance - exchange.reserveBalance; if (additionalReserveBalance > 0) { amountMinted = goodDollarExchangeProvider.mintFromInterest(exchangeId, additionalReserveBalance); @@ -172,11 +178,10 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab .getPoolExchange(exchangeId); ExchangeExpansionConfig memory config = getExpansionConfig(exchangeId); - bool shouldExpand = block.timestamp > config.lastExpansion + config.expansionFrequency; + bool shouldExpand = block.timestamp >= config.lastExpansion + config.expansionFrequency; if (shouldExpand || config.lastExpansion == 0) { - uint256 reserveRatioScalar = _getReserveRatioScalar(config); + uint256 reserveRatioScalar = _getReserveRatioScalar(exchangeId); - exchangeExpansionConfigs[exchangeId].lastExpansion = uint32(block.timestamp); amountMinted = goodDollarExchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar); IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountMinted); @@ -190,12 +195,24 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab /// @inheritdoc IGoodDollarExpansionController function mintRewardFromReserveRatio(bytes32 exchangeId, address to, uint256 amount) external onlyAvatar { + // Defaults to no slippage protection + mintRewardFromReserveRatio(exchangeId, to, amount, BANCOR_MAX_WEIGHT); + } + + /// @inheritdoc IGoodDollarExpansionController + function mintRewardFromReserveRatio( + bytes32 exchangeId, + address to, + uint256 amount, + uint256 maxSlippagePercentage + ) public onlyAvatar { require(to != address(0), "Recipient address must be set"); require(amount > 0, "Amount must be greater than 0"); + require(maxSlippagePercentage <= BANCOR_MAX_WEIGHT, "Max slippage percentage cannot be greater than 100%"); IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) .getPoolExchange(exchangeId); - goodDollarExchangeProvider.updateRatioForReward(exchangeId, amount); + goodDollarExchangeProvider.updateRatioForReward(exchangeId, amount, maxSlippagePercentage); IGoodDollar(exchange.tokenAddress).mint(to, amount); // Ignored, because contracts only interacts with trusted contracts and tokens @@ -219,20 +236,26 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab /** * @notice Calculates the reserve ratio scalar for the given expansion config. - * @param config The expansion config. + * @param exchangeId The ID of the exchange. * @return reserveRatioScalar The reserve ratio scalar. */ - function _getReserveRatioScalar(ExchangeExpansionConfig memory config) internal view returns (uint256) { + function _getReserveRatioScalar(bytes32 exchangeId) internal returns (uint256) { + ExchangeExpansionConfig memory config = getExpansionConfig(exchangeId); uint256 numberOfExpansions; // If there was no previous expansion, we expand once. if (config.lastExpansion == 0) { numberOfExpansions = 1; + exchangeExpansionConfigs[exchangeId].lastExpansion = uint32(block.timestamp); } else { numberOfExpansions = (block.timestamp - config.lastExpansion) / config.expansionFrequency; + // slither-disable-next-line divide-before-multiply + exchangeExpansionConfigs[exchangeId].lastExpansion = uint32( + config.lastExpansion + numberOfExpansions * config.expansionFrequency + ); } - uint256 stepReserveRatioScalar = MAX_WEIGHT - config.expansionRate; + uint256 stepReserveRatioScalar = EXPANSION_MAX_WEIGHT - config.expansionRate; return unwrap(powu(wrap(stepReserveRatioScalar), numberOfExpansions)); } diff --git a/mento-core/contracts/interfaces/IGoodDollarExchangeProvider.sol b/mento-core/contracts/interfaces/IGoodDollarExchangeProvider.sol index 54328ae..6ad4751 100644 --- a/mento-core/contracts/interfaces/IGoodDollarExchangeProvider.sol +++ b/mento-core/contracts/interfaces/IGoodDollarExchangeProvider.sol @@ -72,8 +72,9 @@ interface IGoodDollarExchangeProvider { * @notice Calculates the reserve ratio needed to mint the given G$ reward. * @param exchangeId The ID of the pool the G$ reward is minted from. * @param reward The amount of G$ tokens to be minted as a reward. + * @param maxSlippagePercentage Maximum allowed percentage difference between new and old reserve ratio (0-1e8). */ - function updateRatioForReward(bytes32 exchangeId, uint256 reward) external; + function updateRatioForReward(bytes32 exchangeId, uint256 reward, uint256 maxSlippagePercentage) external; /** * @notice Pauses the Exchange, disabling minting. diff --git a/mento-core/contracts/interfaces/IGoodDollarExpansionController.sol b/mento-core/contracts/interfaces/IGoodDollarExpansionController.sol index 2268ef9..2f48228 100644 --- a/mento-core/contracts/interfaces/IGoodDollarExpansionController.sol +++ b/mento-core/contracts/interfaces/IGoodDollarExpansionController.sol @@ -130,8 +130,9 @@ interface IGoodDollarExpansionController { * @notice Mints UBI as G$ tokens for a given pool from collected reserve interest. * @param exchangeId The ID of the pool to mint UBI for. * @param reserveInterest The amount of reserve tokens collected from interest. + * @return amountMinted The amount of G$ tokens minted. */ - function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external; + function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external returns (uint256 amountMinted); /** * @notice Mints UBI as G$ tokens for a given pool by comparing the contract's reserve balance to the virtual balance. @@ -148,10 +149,24 @@ interface IGoodDollarExpansionController { function mintUBIFromExpansion(bytes32 exchangeId) external returns (uint256 amountMinted); /** - * @notice Mints a reward of G$ tokens for a given pool. + * @notice Mints a reward of G$ tokens for a given pool. Defaults to no slippage protection. * @param exchangeId The ID of the pool to mint a G$ reward for. * @param to The address of the recipient. * @param amount The amount of G$ tokens to mint. */ function mintRewardFromReserveRatio(bytes32 exchangeId, address to, uint256 amount) external; + + /** + * @notice Mints a reward of G$ tokens for a given pool. + * @param exchangeId The ID of the pool to mint a G$ reward for. + * @param to The address of the recipient. + * @param amount The amount of G$ tokens to mint. + * @param maxSlippagePercentage Maximum allowed percentage difference between new and old reserve ratio (0-100). + */ + function mintRewardFromReserveRatio( + bytes32 exchangeId, + address to, + uint256 amount, + uint256 maxSlippagePercentage + ) external; } diff --git a/mento-core/contracts/interfaces/IReserve.sol b/mento-core/contracts/interfaces/IReserve.sol index 553e28b..8dd31f0 100644 --- a/mento-core/contracts/interfaces/IReserve.sol +++ b/mento-core/contracts/interfaces/IReserve.sol @@ -125,4 +125,6 @@ interface IReserve { function removeOtherReserveAddress(address otherReserveAddress, uint256 index) external returns (bool); function collateralAssets(uint256 index) external view returns (address); + + function collateralAssetLastSpendingDay(address collateralAsset) external view returns (uint256); } diff --git a/mento-core/contracts/libraries/TradingLimits.sol b/mento-core/contracts/libraries/TradingLimits.sol index 14e5954..e9c410d 100644 --- a/mento-core/contracts/libraries/TradingLimits.sol +++ b/mento-core/contracts/libraries/TradingLimits.sol @@ -45,6 +45,7 @@ library TradingLimits { uint8 private constant L1 = 2; // 0b010 Limit1 uint8 private constant LG = 4; // 0b100 LimitGlobal int48 private constant MAX_INT48 = type(int48).max; + int48 private constant MIN_INT48 = type(int48).min; /** * @notice Validate a trading limit configuration. @@ -127,8 +128,13 @@ library TradingLimits { int256 _deltaFlow, uint8 decimals ) internal view returns (ITradingLimits.State memory) { + if (_deltaFlow == 0) { + return self; + } + int256 _deltaFlowUnits = _deltaFlow / int256((10 ** uint256(decimals))); require(_deltaFlowUnits <= MAX_INT48, "dFlow too large"); + require(_deltaFlowUnits >= MIN_INT48, "dFlow too small"); int48 deltaFlowUnits = int48(_deltaFlowUnits); if (deltaFlowUnits == 0) { @@ -166,7 +172,7 @@ library TradingLimits { */ function safeINT48Add(int48 a, int48 b) internal pure returns (int48) { int256 c = int256(a) + int256(b); - require(c >= -1 * MAX_INT48 && c <= MAX_INT48, "int48 addition overflow"); + require(c >= MIN_INT48 && c <= MAX_INT48, "int48 addition overflow"); return int48(c); } } diff --git a/mento-core/test/fork/BaseForkTest.sol b/mento-core/test/fork/BaseForkTest.sol index 3bda514..51f804a 100644 --- a/mento-core/test/fork/BaseForkTest.sol +++ b/mento-core/test/fork/BaseForkTest.sol @@ -9,6 +9,7 @@ import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; // Interfaces import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; import { IBroker } from "contracts/interfaces/IBroker.sol"; import { ICeloProxy } from "contracts/interfaces/ICeloProxy.sol"; import { IOwnable } from "contracts/interfaces/IOwnable.sol"; @@ -119,14 +120,16 @@ abstract contract BaseForkTest is Test { function mint(address asset, address to, uint256 amount, bool updateSupply) public { if (asset == lookup("GoldToken")) { - if (!updateSupply) { - revert("BaseForkTest: can't mint GoldToken without updating supply"); - } - vm.prank(address(0)); - IMint(asset).mint(to, amount); + // with L2 Celo, we need to transfer GoldToken to the user manually from the reserve + transferCeloFromReserve(to, amount); return; } deal(asset, to, amount, updateSupply); } + + function transferCeloFromReserve(address to, uint256 amount) internal { + vm.prank(address(mentoReserve)); + IERC20(lookup("GoldToken")).transfer(to, amount); + } } diff --git a/mento-core/test/fork/ForkTests.t.sol b/mento-core/test/fork/ForkTests.t.sol index d3ab0b4..c47de51 100644 --- a/mento-core/test/fork/ForkTests.t.sol +++ b/mento-core/test/fork/ForkTests.t.sol @@ -45,7 +45,7 @@ import { GoodDollarTradingLimitsForkTest } from "./GoodDollar/TradingLimitsForkT import { GoodDollarSwapForkTest } from "./GoodDollar/SwapForkTest.sol"; import { GoodDollarExpansionForkTest } from "./GoodDollar/ExpansionForkTest.sol"; -contract Alfajores_ChainForkTest is ChainForkTest(ALFAJORES_ID, 1, uints(15)) {} +contract Alfajores_ChainForkTest is ChainForkTest(ALFAJORES_ID, 1, uints(16)) {} contract Alfajores_P0E00_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 0) {} @@ -77,7 +77,9 @@ contract Alfajores_P0E13_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 1 contract Alfajores_P0E14_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 14) {} -contract Celo_ChainForkTest is ChainForkTest(CELO_ID, 1, uints(15)) {} +contract Alfajores_P0E15_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 15) {} + +contract Celo_ChainForkTest is ChainForkTest(CELO_ID, 1, uints(16)) {} contract Celo_P0E00_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 0) {} @@ -109,6 +111,8 @@ contract Celo_P0E13_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 13) {} contract Celo_P0E14_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 14) {} +contract Celo_P0E15_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 15) {} + contract Celo_BancorExchangeProviderForkTest is BancorExchangeProviderForkTest(CELO_ID) {} contract Celo_GoodDollarTradingLimitsForkTest is GoodDollarTradingLimitsForkTest(CELO_ID) {} diff --git a/mento-core/test/unit/goodDollar/BancorExchangeProvider.t.sol b/mento-core/test/unit/goodDollar/BancorExchangeProvider.t.sol index e3d19b5..d39a90d 100644 --- a/mento-core/test/unit/goodDollar/BancorExchangeProvider.t.sol +++ b/mento-core/test/unit/goodDollar/BancorExchangeProvider.t.sol @@ -5,6 +5,8 @@ pragma solidity 0.8.18; import { Test } from "forge-std/Test.sol"; import { ERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/ERC20.sol"; +import { ERC20DecimalsMock } from "openzeppelin-contracts-next/contracts/mocks/ERC20DecimalsMock.sol"; + import { BancorExchangeProvider } from "contracts/goodDollar/BancorExchangeProvider.sol"; import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; @@ -30,16 +32,23 @@ contract BancorExchangeProviderTest is Test { ERC20 public reserveToken; ERC20 public token; ERC20 public token2; + ERC20DecimalsMock public reserveTokenWith6Decimals; + ERC20DecimalsMock public tokenWith6Decimals; address public reserveAddress; address public brokerAddress; IBancorExchangeProvider.PoolExchange public poolExchange1; IBancorExchangeProvider.PoolExchange public poolExchange2; + IBancorExchangeProvider.PoolExchange public poolExchange3; + IBancorExchangeProvider.PoolExchange public poolExchange4; + IBancorExchangeProvider.PoolExchange public poolExchange5; function setUp() public virtual { reserveToken = new ERC20("cUSD", "cUSD"); token = new ERC20("Good$", "G$"); token2 = new ERC20("Good2$", "G2$"); + reserveTokenWith6Decimals = new ERC20DecimalsMock("Reserve Token", "RES", 6); + tokenWith6Decimals = new ERC20DecimalsMock("Token", "TKN", 6); brokerAddress = makeAddr("Broker"); reserveAddress = makeAddr("Reserve"); @@ -62,6 +71,33 @@ contract BancorExchangeProviderTest is Test { exitContribution: 1e8 * 0.01 }); + poolExchange3 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveTokenWith6Decimals), + tokenAddress: address(token), + tokenSupply: 300_000 * 1e18, + reserveBalance: 60_000 * 1e6, + reserveRatio: 1e8 * 0.2, + exitContribution: 1e8 * 0.01 + }); + + poolExchange4 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(tokenWith6Decimals), + tokenSupply: 300_000 * 1e6, + reserveBalance: 60_000 * 1e18, + reserveRatio: 1e8 * 0.2, + exitContribution: 1e8 * 0.01 + }); + + poolExchange5 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveTokenWith6Decimals), + tokenAddress: address(tokenWith6Decimals), + tokenSupply: 300_000 * 1e6, + reserveBalance: 60_000 * 1e6, + reserveRatio: 1e8 * 0.2, + exitContribution: 1e8 * 0.01 + }); + vm.mockCall( reserveAddress, abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token)), @@ -72,11 +108,22 @@ contract BancorExchangeProviderTest is Test { abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token2)), abi.encode(true) ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(tokenWith6Decimals)), + abi.encode(true) + ); vm.mockCall( reserveAddress, abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken)), abi.encode(true) ); + + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveTokenWith6Decimals)), + abi.encode(true) + ); } function initializeBancorExchangeProvider() internal returns (BancorExchangeProvider) { @@ -164,12 +211,12 @@ contract BancorExchangeProviderTest_initilizerSettersGetters is BancorExchangePr bancorExchangeProvider.setExitContribution(exchangeId, 1e5); } - function test_setExitContribution_whenExitContributionAbove100Percent_shouldRevert() public { + function test_setExitContribution_whenExitContributionIsNotLessThan100Percent_shouldRevert() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint32 maxWeight = bancorExchangeProvider.MAX_WEIGHT(); vm.expectRevert("Exit contribution is too high"); - bancorExchangeProvider.setExitContribution(exchangeId, maxWeight + 1); + bancorExchangeProvider.setExitContribution(exchangeId, maxWeight); } function test_setExitContribution_whenSenderIsOwner_shouldUpdateAndEmit() public { @@ -253,6 +300,12 @@ contract BancorExchangeProviderTest_createExchange is BancorExchangeProviderTest bancorExchangeProvider.createExchange(poolExchange1); } + function test_createExchange_whenReserveBalanceIsZero_shouldRevert() public { + poolExchange1.reserveBalance = 0; + vm.expectRevert("Reserve balance must be greater than 0"); + bancorExchangeProvider.createExchange(poolExchange1); + } + function test_createExchange_whenReserveAssetIsNotCollateral_shouldRevert() public { vm.mockCall( reserveAddress, @@ -340,6 +393,23 @@ contract BancorExchangeProviderTest_createExchange is BancorExchangeProviderTest assertEq(bancorExchangeProvider.tokenPrecisionMultipliers(address(reserveToken)), 1); assertEq(bancorExchangeProvider.tokenPrecisionMultipliers(address(token)), 1); } + + function test_createExchange_whenTokensHasLessThan18Decimals_shouldCreateExchangeWithCorrectSupplyAndBalance() + public + { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange5); + + IBancorExchangeProvider.PoolExchange memory poolExchange = bancorExchangeProvider.getPoolExchange(exchangeId); + assertEq(poolExchange.reserveAsset, poolExchange5.reserveAsset); + assertEq(poolExchange.tokenAddress, poolExchange5.tokenAddress); + assertEq(poolExchange.tokenSupply, poolExchange5.tokenSupply * 1e12); + assertEq(poolExchange.reserveBalance, poolExchange5.reserveBalance * 1e12); + assertEq(poolExchange.reserveRatio, poolExchange5.reserveRatio); + assertEq(poolExchange.exitContribution, poolExchange5.exitContribution); + + assertEq(bancorExchangeProvider.tokenPrecisionMultipliers(address(reserveTokenWith6Decimals)), 1e12); + assertEq(bancorExchangeProvider.tokenPrecisionMultipliers(address(tokenWith6Decimals)), 1e12); + } } contract BancorExchangeProviderTest_destroyExchange is BancorExchangeProviderTest { @@ -452,19 +522,6 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { }); } - function test_getAmountIn_whenTokenInIsTokenAndReserveBalanceIsZero_shouldRevert() public { - poolExchange1.reserveBalance = 0; - bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - - vm.expectRevert("ERR_INVALID_RESERVE_BALANCE"); - bancorExchangeProvider.getAmountIn({ - exchangeId: exchangeId, - tokenIn: address(token), - tokenOut: address(reserveToken), - amountOut: 1e18 - }); - } - function test_getAmountIn_whenTokenInIsTokenAndAmountOutLargerThanReserveBalance_shouldRevert() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); vm.expectRevert("ERR_INVALID_AMOUNT"); @@ -487,19 +544,15 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { assertEq(amountIn, 0); } - function test_getAmountIn_whenTokenInIsTokenAndAmountOutEqualReserveBalance_shouldReturnSupply() public { - // need to set exit contribution to 0 to make the formula work otherwise amountOut would need to be adjusted - // to be equal to reserveBalance after exit contribution is applied - poolExchange1.exitContribution = 0; + function test_getAmountIn_whenTokenInIsTokenAndAmountInIsLargerOrEqualSupply_shouldRevert() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - uint256 expectedAmountIn = poolExchange1.tokenSupply; - uint256 amountIn = bancorExchangeProvider.getAmountIn({ + vm.expectRevert("amountIn is greater than tokenSupply"); + bancorExchangeProvider.getAmountIn({ exchangeId: exchangeId, tokenIn: address(token), tokenOut: address(reserveToken), amountOut: poolExchange1.reserveBalance }); - assertEq(amountIn, expectedAmountIn); } function test_getAmountIn_whenTokenInIsTokenAndReserveRatioIs100Percent_shouldReturnCorrectAmount() public { @@ -533,19 +586,6 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { }); } - function test_getAmountIn_whenTokenInIsReserveAssetAndReserveBalanceIsZero_shouldRevert() public { - poolExchange1.reserveBalance = 0; - bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - - vm.expectRevert("ERR_INVALID_RESERVE_BALANCE"); - bancorExchangeProvider.getAmountIn({ - exchangeId: exchangeId, - tokenIn: address(reserveToken), - tokenOut: address(token), - amountOut: 1e18 - }); - } - function test_getAmountIn_whenTokenInIsReserveAssetAndAmountOutIsZero_shouldReturnZero() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountIn = bancorExchangeProvider.getAmountIn({ @@ -591,12 +631,12 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { function test_getAmountIn_whenTokenInIsToken_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - // formula: = tokenSupply * (-1 + (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ) - // formula: amountIn = ------------------------------------------------------------------------------------------------ this is a fractional line - // formula: = (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio + // formula: = tokenSupply * (-1 + (reserveBalance/(reserveBalance - amountOut))^reserveRatio ) + // formula: amountIn = -------------------------------------------------------------------------------- ÷ (1-e) + // formula: = (reserveBalance / (reserveBalance - amountOut) )^reserveRatio - // calculation: (300000 * ( -1 + (60000 / (60000-(1/0.99)))^0.2))/(60000 / (60000-(1/0.99)))^0.2 = 1.010107812196722301 - uint256 expectedAmountIn = 1010107812196722302; + // calculation: (300000 * ( -1 + (60000 / (60000-1))^0.2))/(60000 / (60000-1))^0.2 ÷ 0.99 = 1.010107744175084961 + uint256 expectedAmountIn = 1010107744175084961; uint256 amountIn = bancorExchangeProvider.getAmountIn({ exchangeId: exchangeId, tokenIn: address(token), @@ -624,11 +664,12 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { function test_getAmountIn_whenTokenInIsTokenAndAmountOutIsSmall_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountOut = 1e12; // 0.000001 token - // formula: = tokenSupply * (-1 + (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ) - // formula: amountIn = ------------------------------------------------------------------------------------------------ this is a fractional line - // formula: = (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio + // formula: = tokenSupply * (-1 + (reserveBalance/(reserveBalance - amountOut))^reserveRatio ) + // formula: amountIn = -------------------------------------------------------------------------------- ÷ (1-e) + // formula: = (reserveBalance / (reserveBalance - amountOut) )^reserveRatio - // calculation: (300000 * ( -1 + (60000 / (60000-(0.000001/0.99)))^0.2))/(60000 / (60000-(0.000001/0.99)))^0.2 ≈ 0.000001010101010107 + // calculation: (300000 * ( -1 + (60000 / (60000-0.000001))^0.2))/(60000 / (60000-0.000001))^0.2 ÷ 0.99 ≈ 0.000001010101010107 + // 1 wei difference due to precision loss uint256 expectedAmountIn = 1010101010108; uint256 amountIn = bancorExchangeProvider.getAmountIn({ exchangeId: exchangeId, @@ -658,12 +699,12 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { function test_getAmountIn_whenTokenInIsTokenAndAmountOutIsLarge_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountOut = 59000e18; // 59_000 since total reserve is 60k - // formula: = tokenSupply * (-1 + (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ) - // formula: amountIn = ------------------------------------------------------------------------------------------------ this is a fractional line - // formula: = (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio + // formula: = tokenSupply * (-1 + (reserveBalance/(reserveBalance - amountOut))^reserveRatio ) + // formula: amountIn = -------------------------------------------------------------------------------- ÷ (1-e) + // formula: = (reserveBalance / (reserveBalance - amountOut) )^reserveRatio - // calculation: (300000 * ( -1 + (60000 / (60000-(59000/0.99)))^0.2))/(60000 / (60000-(59000/0.99)))^0.2 = 189649.078540006525698460 - uint256 expectedAmountIn = 189649078540006525698460; + // calculation: (300000 * ( -1 + (60000 / (60000-59000))^0.2))/(60000 / (60000-59000))^0.2 ÷ 0.99 ≈ 169415.120269436288420151 + uint256 expectedAmountIn = 169415120269436288420151; uint256 amountIn = bancorExchangeProvider.getAmountIn({ exchangeId: exchangeId, tokenIn: address(token), @@ -681,9 +722,8 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { bancorExchangeProvider.setExitContribution(exchangeId, 1e6); bytes32 exchangeId2 = bancorExchangeProvider.createExchange(poolExchange2); bancorExchangeProvider.setExitContribution(exchangeId2, 0); - uint256 amountOut = 116e18; - // formula: amountIn = (tokenSupply * (( (amountOut + reserveBalance) / reserveBalance) ^ (reserveRatio) - 1)) / exitContribution + uint256 amountIn = bancorExchangeProvider.getAmountIn({ exchangeId: exchangeId, tokenIn: address(token), @@ -691,17 +731,13 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { amountOut: amountOut }); - // exit contribution is 1% - uint256 amountOut2 = (amountOut * 100) / 99; - assertTrue(amountOut < amountOut2); - uint256 amountIn2 = bancorExchangeProvider.getAmountIn({ exchangeId: exchangeId2, tokenIn: address(token2), tokenOut: address(reserveToken), - amountOut: amountOut2 + amountOut: amountOut }); - assertEq(amountIn, amountIn2); + assertEq(amountIn, (amountIn2 * 100) / 99); } function test_getAmountIn_whenDifferentTokenDecimals_shouldReturnCorrectAmount() public { @@ -728,7 +764,7 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { reserveAsset: address(reserveToken6), tokenAddress: address(stableToken18), tokenSupply: 100_000 * 1e18, // 100,000 - reserveBalance: 50_000 * 1e18, // 50,000 + reserveBalance: 50_000 * 1e6, // 50,000 reserveRatio: 1e8 * 0.5, // 50% exitContribution: 0 }); @@ -932,6 +968,88 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { // we allow up to 1% difference due to precision loss assertApproxEqRel(reversedAmountOut, amountOut, 1e18 * 0.01); } + + function test_getAmountIn_whenTokenInIsTokenWith6TokenDecimals_shouldRoundUpInFavorOfReserve() public { + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange4); + + uint256 amountOut = 55e18; + uint256 amountIn6Decimals = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId6Decimals, + tokenIn: address(tokenWith6Decimals), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); + bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); + + uint256 amountIn18Decimals = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId18Decimals, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + assertTrue(amountIn18Decimals <= amountIn6Decimals * 1e12); + assertEq(amountIn6Decimals, (amountIn18Decimals / 1e12) + 1); + } + + function test_getAmountIn_whenTokenInIsReserveAssetWith6TokenDecimals_shouldRoundUpInFavorOfReserve() public { + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange3); + + uint256 amountOut = 55e18; + uint256 amountIn6Decimals = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId6Decimals, + tokenIn: address(reserveTokenWith6Decimals), + tokenOut: address(token), + amountOut: amountOut + }); + + bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); + bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); + + uint256 amountIn18Decimals = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId18Decimals, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + + assertTrue(amountIn18Decimals <= amountIn6Decimals * 1e12); + assertEq(amountIn6Decimals, (amountIn18Decimals / 1e12) + 1); + } + + function test_getAmountIn_whenTokenInHas6DecimalsButNoRoundingNeeded_shouldNotRoundUp() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: 15e17 + }); + + uint256 amountIn18Decimals = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + assertEq(amountIn18Decimals, 15e17); + + bancorExchangeProvider.destroyExchange(exchangeId, 0); + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange4); + + uint256 amountIn6Decimals = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId6Decimals, + tokenIn: address(tokenWith6Decimals), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + assertEq(amountIn6Decimals, amountIn18Decimals / 1e12); + } } contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { @@ -998,18 +1116,6 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { }); } - function test_getAmountOut_whenTokenInIsReserveAssetAndReserveBalanceIsZero_shouldRevert() public { - poolExchange1.reserveBalance = 0; - bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - vm.expectRevert("ERR_INVALID_RESERVE_BALANCE"); - bancorExchangeProvider.getAmountOut({ - exchangeId: exchangeId, - tokenIn: address(reserveToken), - tokenOut: address(token), - amountIn: 1e18 - }); - } - function test_getAmountOut_whenTokenInIsReserveAssetAndAmountInIsZero_shouldReturnZero() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountOut = bancorExchangeProvider.getAmountOut({ @@ -1037,41 +1143,6 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { assertEq(amountOut, expectedAmountOut); } - function test_getAmountOut_whenTokenInIsTokenAndSupplyIsZero_shouldRevert() public { - poolExchange1.tokenSupply = 0; - bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - vm.expectRevert("ERR_INVALID_SUPPLY"); - bancorExchangeProvider.getAmountOut({ - exchangeId: exchangeId, - tokenIn: address(token), - tokenOut: address(reserveToken), - amountIn: 1e18 - }); - } - - function test_getAmountOut_whenTokenInIsTokenAndReserveBalanceIsZero_shouldRevert() public { - poolExchange1.reserveBalance = 0; - bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - vm.expectRevert("ERR_INVALID_RESERVE_BALANCE"); - bancorExchangeProvider.getAmountOut({ - exchangeId: exchangeId, - tokenIn: address(token), - tokenOut: address(reserveToken), - amountIn: 1e18 - }); - } - - function test_getAmountOut_whenTokenInIsTokenAndAmountLargerSupply_shouldRevert() public { - bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - vm.expectRevert("ERR_INVALID_AMOUNT"); - bancorExchangeProvider.getAmountOut({ - exchangeId: exchangeId, - tokenIn: address(token), - tokenOut: address(reserveToken), - amountIn: poolExchange1.tokenSupply + 1 - }); - } - function test_getAmountOut_whenTokenInIsTokenAndAmountIsZero_shouldReturnZero() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountOut = bancorExchangeProvider.getAmountOut({ @@ -1083,25 +1154,23 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { assertEq(amountOut, 0); } - function test_getAmountOut_whenTokenInIsTokenAndAmountIsSupply_shouldReturnReserveBalanceMinusExitContribution() - public - { + function test_getAmountOut_whenTokenInIsTokenAndAmountInIsLargerOrEqualSupply_shouldRevert() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - uint256 amountOut = bancorExchangeProvider.getAmountOut({ + vm.expectRevert("amountIn is greater than tokenSupply"); + bancorExchangeProvider.getAmountOut({ exchangeId: exchangeId, tokenIn: address(token), tokenOut: address(reserveToken), - amountIn: poolExchange1.tokenSupply + amountIn: (poolExchange1.tokenSupply) }); - assertEq(amountOut, (poolExchange1.reserveBalance * (1e8 - poolExchange1.exitContribution)) / 1e8); } function test_getAmountOut_whenTokenInIsTokenAndReserveRatioIs100Percent_shouldReturnCorrectAmount() public { poolExchange1.reserveRatio = 1e8; bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountIn = 1e18; - // formula: amountOut = (reserveBalance * amountIn / tokenSupply) * (1-e) - // calculation: (60_000 * 1 / 300_000) * 0.99 = 0.198 + // formula: amountOut = (reserveBalance * amountIn * (1-e)) / tokenSupply + // calculation: (60_000 * 1 * 0.99) / 300_000 = 0.198 uint256 expectedAmountOut = 198000000000000000; uint256 amountOut = bancorExchangeProvider.getAmountOut({ exchangeId: exchangeId, @@ -1109,6 +1178,7 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { tokenOut: address(reserveToken), amountIn: amountIn }); + assertEq(amountOut, expectedAmountOut); } @@ -1128,13 +1198,13 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { function test_getAmountOut_whenTokenInIsToken_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio)) - // formula: amountOut = ---------------------------------------------------------------------------------- * (1 - e) - // formula: = (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio) + // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn*(1-e)))^(1/reserveRatio)) + // formula: amountOut = ---------------------------------------------------------------------------------- + // formula: = (tokenSupply/(tokenSupply - amountIn*(1-e)))^(1/reserveRatio) - // calculation: ((60_000 *(-1+(300_000/(300_000-1))^5) ) / (300_000/(300_000-1))^5)*0.99 = 0.989993400021999963 + // calculation: ((60_000 *(-1+(300_000/(300_000-1*0.99))^5) ) / (300_000/(300_000-1*0.99))^5) = 0.989993466021562164 // 1 wei difference due to precision loss - uint256 expectedAmountOut = 989993400021999962; + uint256 expectedAmountOut = 989993466021562164; uint256 amountOut = bancorExchangeProvider.getAmountOut({ exchangeId: exchangeId, tokenIn: address(token), @@ -1162,11 +1232,11 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { function test_getAmountOut_whenTokenInIsTokenAndAmountOutIsSmall_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountIn = 1e12; // 0.000001 token - // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio)) - // formula: amountOut = ---------------------------------------------------------------------------------- * (1 - e) - // formula: = (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio) + // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn * (1-e)))^(1/reserveRatio)) + // formula: amountOut = ---------------------------------------------------------------------------------- + // formula: = (tokenSupply/(tokenSupply - amountIn * (1-e)))^(1/reserveRatio) - // calculation: ((60_000 *(-1+(300_000/(300_000-0.000001))^5) )/(300_000/(300_000-0.000001))^5)*0.99 ≈ 0.0000009899999999934 + // calculation: ((60_000 *(-1+(300_000/(300_000-0.000001*0.99))^5) )/(300_000/(300_000-0.000001*0.99))^5) = 0.0000009899999999934 uint256 expectedAmountOut = 989999999993; uint256 amountOut = bancorExchangeProvider.getAmountOut({ exchangeId: exchangeId, @@ -1195,12 +1265,13 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { function test_getAmountOut_whenTokenInIsTokenAndAmountInIsLarge_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountIn = 299_000 * 1e18; // 299,000 tokens only 300k supply - // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio)) - // formula: amountOut = ---------------------------------------------------------------------------------- * (1 - e) - // formula: = (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio) + // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn * (1-e)))^(1/reserveRatio)) + // formula: amountOut = ---------------------------------------------------------------------------------- + // formula: = (tokenSupply/(tokenSupply - amountIn * (1-e)))^(1/reserveRatio) - // calculation: ((60_000 *(-1+(300_000/(300_000-299_000))^5) ) / (300_000/(300_000-299_000))^5)*0.99 ≈ 59399.999999975555555555 - uint256 expectedAmountOut = 59399999999975555555555; + // calculation: ((60_000 *(-1+(300_000/(300_000-299_000*0.99))^5) ) / (300_000/(300_000-299_000 *0.99))^5) = 59999999975030522464200 + // 1 wei difference due to precision loss + uint256 expectedAmountOut = 59999999975030522464200; uint256 amountOut = bancorExchangeProvider.getAmountOut({ exchangeId: exchangeId, tokenIn: address(token), @@ -1208,8 +1279,8 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { amountIn: amountIn }); - // we allow up to 1% difference due to precision loss - assertApproxEqRel(amountOut, expectedAmountOut, 1e18 * 0.01); + // we allow up to 0.1% difference due to precision loss + assertApproxEqRel(amountOut, expectedAmountOut, 1e18 * 0.001); } function test_getAmountOut_whenTokenInIsTokenAndExitContributionIsNonZero_shouldReturnCorrectAmount( @@ -1223,6 +1294,7 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { bancorExchangeProvider.setExitContribution(exchangeId2, 0); amountIn = bound(amountIn, 100, 299_000 * 1e18); + uint256 amountOut = bancorExchangeProvider.getAmountOut({ exchangeId: exchangeId, tokenIn: address(token), @@ -1233,9 +1305,9 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { exchangeId: exchangeId2, tokenIn: address(token2), tokenOut: address(reserveToken), - amountIn: amountIn + amountIn: (amountIn * 99) / 100 }); - assertEq(amountOut, (amountOut2 * 99) / 100); + assertEq(amountOut, amountOut2); } function test_getAmountOut_whenDifferentTokenDecimals_shouldReturnCorrectAmount() public { @@ -1262,7 +1334,7 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { reserveAsset: address(reserveToken6), tokenAddress: address(stableToken18), tokenSupply: 100_000 * 1e18, // 100,000 - reserveBalance: 50_000 * 1e18, // 50,000 + reserveBalance: 50_000 * 1e6, // 50,000 reserveRatio: 1e8 * 0.5, // 50% exitContribution: 0 }); @@ -1465,6 +1537,56 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { // we allow up to 1% difference due to precision loss assertApproxEqRel(reversedAmountIn, amountIn, 1e18 * 0.01); } + + function test_getAmountOut_whenTokenOutIsTokenWith6TokenDecimals_shouldRoundDownInFavorOfReserve() public { + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange4); + + uint256 amountIn = 55e18; + uint256 amountOut6Decimals = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId6Decimals, + tokenIn: address(reserveToken), + tokenOut: address(tokenWith6Decimals), + amountIn: amountIn + }); + + bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); + bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); + + uint256 amountOut18Decimals = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId18Decimals, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); + + assertTrue(amountOut6Decimals * 1e12 < amountOut18Decimals); + assertEq(amountOut6Decimals, (amountOut18Decimals / 1e12)); + } + + function test_getAmountOut_whenTokenOutIsReserveAssetWith6TokenDecimals_shouldRoundDownInFavorOfReserve() public { + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange3); + + uint256 amountIn = 55e18; + uint256 amountOut6Decimals = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId6Decimals, + tokenIn: address(token), + tokenOut: address(reserveTokenWith6Decimals), + amountIn: amountIn + }); + + bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); + bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); + + uint256 amountOut18Decimals = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId18Decimals, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + + assertTrue(amountOut6Decimals * 1e12 < amountOut18Decimals); + assertEq(amountOut6Decimals, (amountOut18Decimals / 1e12)); + } } contract BancorExchangeProviderTest_currentPrice is BancorExchangeProviderTest { @@ -1490,6 +1612,15 @@ contract BancorExchangeProviderTest_currentPrice is BancorExchangeProviderTest { assertEq(price, expectedPrice); } + function test_currentPrice_whenReserveTokenHasLessThan18Decimals_shouldReturnCorrectPrice() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange3); + // formula: price = reserveBalance / tokenSupply * reserveRatio + // calculation: 60_000 / 300_000 * 0.2 = 1 + uint256 expectedPrice = 1e6; + uint256 price = bancorExchangeProvider.currentPrice(exchangeId); + assertEq(price, expectedPrice); + } + function test_currentPrice_fuzz(uint256 reserveBalance, uint256 tokenSupply, uint256 reserveRatio) public { // reserveBalance range between 1 token and 10_000_000 tokens reserveBalance = bound(reserveBalance, 1e18, 10_000_000 * 1e18); @@ -1554,6 +1685,14 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { bancorExchangeProvider.swapIn(exchangeId, address(token), address(token), 1e18); } + function test_swapIn_whenTokenInIsTokenAndAmountIsLargerOrEqualSupply_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + vm.expectRevert("amountIn is greater than tokenSupply"); + bancorExchangeProvider.swapIn(exchangeId, address(token), address(reserveToken), poolExchange1.tokenSupply); + } + function test_swapIn_whenTokenInIsReserveAsset_shouldSwapIn() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); uint256 amountIn = 1e18; @@ -1578,6 +1717,34 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { assertEq(tokenSupplyAfter, tokenSupplyBefore + amountOut); } + function test_swapIn_whenTokenInIsReserveAssetWith6Decimals_shouldSwapIn() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountIn = 1e6; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange3); + (, , uint256 tokenSupplyBefore, uint256 reserveBalanceBefore, , ) = bancorExchangeProvider.exchanges(exchangeId); + + uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveTokenWith6Decimals), + tokenOut: address(token), + amountIn: amountIn + }); + vm.prank(brokerAddress); + uint256 amountOut = bancorExchangeProvider.swapIn( + exchangeId, + address(reserveTokenWith6Decimals), + address(token), + amountIn + ); + assertEq(amountOut, expectedAmountOut); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore + amountIn * 1e12); + assertEq(tokenSupplyAfter, tokenSupplyBefore + amountOut); + } + function test_swapIn_whenTokenInIsToken_shouldSwapIn() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); uint256 amountIn = 1e18; @@ -1601,6 +1768,121 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); assertEq(tokenSupplyAfter, tokenSupplyBefore - amountIn); } + + function test_swapIn_whenTokenIsTokenWith6Decimals_shouldSwapIn() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountIn = 1e18; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange4); + (, , uint256 tokenSupplyBefore, uint256 reserveBalanceBefore, , ) = bancorExchangeProvider.exchanges(exchangeId); + + uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(tokenWith6Decimals), + amountIn: amountIn + }); + vm.prank(brokerAddress); + uint256 amountOut = bancorExchangeProvider.swapIn( + exchangeId, + address(reserveToken), + address(tokenWith6Decimals), + amountIn + ); + assertEq(amountOut, expectedAmountOut); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore + amountIn); + assertApproxEqRel(tokenSupplyAfter, tokenSupplyBefore + amountOut * 1e12, 1e18 * 0.0001); + } + + function test_swapIn_whenTokenInIsTokenAndExitContributionIsNonZero_shouldReturnSameAmountWhenSellIsDoneInMultipleSteps() + public + { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountToSell = 100_000 * 1e18; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + uint256 amountOutInOneSell = bancorExchangeProvider.swapIn( + exchangeId, + address(token), + address(reserveToken), + amountToSell + ); + + // destroy and recreate the exchange to reset everything + vm.prank(bancorExchangeProvider.owner()); + bancorExchangeProvider.destroyExchange(exchangeId, 0); + exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + uint256 amountOutInMultipleSells; + for (uint256 i = 0; i < 100_000; i++) { + vm.prank(brokerAddress); + amountOutInMultipleSells += bancorExchangeProvider.swapIn( + exchangeId, + address(token), + address(reserveToken), + 1e18 + ); + } + // we allow up to 0.1% difference due to precision loss on exitContribution accounting + assertApproxEqRel(amountOutInOneSell, amountOutInMultipleSells, 1e18 * 0.001); + } + + function test_swapIn_whenTokenOutIsTokenWith6Decimals_shouldRoundDownInFavorOfReserve() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountIn = 55e18; + + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange4); + vm.prank(brokerAddress); + uint256 amountOut6Decimals = bancorExchangeProvider.swapIn( + exchangeId6Decimals, + address(reserveToken), + address(tokenWith6Decimals), + amountIn + ); + + bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); + bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + uint256 amountOut18Decimals = bancorExchangeProvider.swapIn( + exchangeId18Decimals, + address(reserveToken), + address(token), + amountIn + ); + + assertTrue(amountOut6Decimals * 1e12 < amountOut18Decimals); + assertEq(amountOut6Decimals, (amountOut18Decimals / 1e12)); + } + + function test_swapIn_whenTokenOutIsReserveAssetWith6Decimals_shouldRoundDownInFavorOfReserve() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountIn = 55e18; + + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange3); + vm.prank(brokerAddress); + uint256 amountOut6Decimals = bancorExchangeProvider.swapIn( + exchangeId6Decimals, + address(token), + address(reserveTokenWith6Decimals), + amountIn + ); + + bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); + bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + uint256 amountOut18Decimals = bancorExchangeProvider.swapIn( + exchangeId18Decimals, + address(token), + address(reserveToken), + amountIn + ); + + assertTrue(amountOut6Decimals * 1e12 < amountOut18Decimals); + assertEq(amountOut6Decimals, (amountOut18Decimals / 1e12)); + } } contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { @@ -1619,7 +1901,7 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { bancorExchangeProvider.swapOut("0xexchangeId", address(reserveToken), address(token), 1e18); } - function test_swapOut_whenTokenInNotInexchange_shouldRevert() public { + function test_swapOut_whenTokenInNotInExchange_shouldRevert() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); vm.prank(brokerAddress); @@ -1627,7 +1909,7 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { bancorExchangeProvider.swapOut(exchangeId, address(token2), address(token), 1e18); } - function test_swapOut_whenTokenOutNotInexchange_shouldRevert() public { + function test_swapOut_whenTokenOutNotInExchange_shouldRevert() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); vm.prank(brokerAddress); @@ -1643,6 +1925,14 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { bancorExchangeProvider.swapOut(exchangeId, address(token), address(token), 1e18); } + function test_swapOut_whenTokenInIsTokenAndAmountInIsLargerOrEqualSupply_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + vm.expectRevert("amountIn required is greater than tokenSupply"); + bancorExchangeProvider.swapOut(exchangeId, address(token), address(reserveToken), poolExchange1.reserveBalance); + } + function test_swapOut_whenTokenInIsReserveAsset_shouldSwapOut() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); uint256 amountOut = 1e18; @@ -1667,6 +1957,34 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { assertEq(tokenSupplyAfter, tokenSupplyBefore + amountOut); } + function test_swapOut_whenTokenInIsReserveAssetWith6Decimals_shouldSwapIn() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountOut = 1e18; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange3); + (, , uint256 tokenSupplyBefore, uint256 reserveBalanceBefore, , ) = bancorExchangeProvider.exchanges(exchangeId); + + uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveTokenWith6Decimals), + tokenOut: address(token), + amountOut: amountOut + }); + vm.prank(brokerAddress); + uint256 amountIn = bancorExchangeProvider.swapOut( + exchangeId, + address(reserveTokenWith6Decimals), + address(token), + amountOut + ); + assertEq(amountIn, expectedAmountIn); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertApproxEqRel(reserveBalanceAfter, reserveBalanceBefore + amountIn * 1e12, 1e18 * 0.0001); + assertEq(tokenSupplyAfter, tokenSupplyBefore + amountOut); + } + function test_swapOut_whenTokenInIsToken_shouldSwapOut() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); uint256 amountOut = 1e18; @@ -1690,4 +2008,146 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); assertEq(tokenSupplyAfter, tokenSupplyBefore - amountIn); } + + function test_swapOut_whenTokenInIsTokenWith6Decimals_shouldSwapIn() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountOut = 1e18; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange4); + (, , uint256 tokenSupplyBefore, uint256 reserveBalanceBefore, , ) = bancorExchangeProvider.exchanges(exchangeId); + + uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(tokenWith6Decimals), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + vm.prank(brokerAddress); + uint256 amountIn = bancorExchangeProvider.swapOut( + exchangeId, + address(tokenWith6Decimals), + address(reserveToken), + amountOut + ); + assertEq(amountIn, expectedAmountIn); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); + assertApproxEqRel(tokenSupplyAfter, tokenSupplyBefore - amountIn * 1e12, 1e18 * 0.0001); + } + function test_swapOut_whenTokenInIsTokenAndExitContributionIsNonZero_shouldReturnSameAmountWhenSellIsDoneInMultipleSteps() + public + { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountToBuy = 50_000 * 1e18; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + uint256 amountInInOneBuy = bancorExchangeProvider.swapOut( + exchangeId, + address(token), + address(reserveToken), + amountToBuy + ); + + bancorExchangeProvider.destroyExchange(exchangeId, 0); + exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + uint256 amountInInMultipleBuys; + for (uint256 i = 0; i < 50_000; i++) { + vm.prank(brokerAddress); + amountInInMultipleBuys += bancorExchangeProvider.swapOut(exchangeId, address(token), address(reserveToken), 1e18); + } + // we allow up to 0.1% difference due to precision loss on exitContribution accounting + assertApproxEqRel(amountInInOneBuy, amountInInMultipleBuys, 1e18 * 0.001); + } + + function test_swapOut_whenTokenInIsTokenWith6Decimals_shouldRoundUpInFavorOfReserve() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountOut = 55e18; + + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange4); + vm.prank(brokerAddress); + uint256 amountIn6Decimals = bancorExchangeProvider.swapOut( + exchangeId6Decimals, + address(tokenWith6Decimals), + address(reserveToken), + amountOut + ); + + bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); + bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + uint256 amountIn18Decimals = bancorExchangeProvider.swapOut( + exchangeId18Decimals, + address(token), + address(reserveToken), + amountOut + ); + + assertTrue(amountIn18Decimals < amountIn6Decimals * 1e12); + assertEq(amountIn6Decimals, (amountIn18Decimals / 1e12) + 1); + } + + function test_swapOut_whenTokenInIsReserveAssetWith6Decimals_shouldRoundUpInFavorOfReserve() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountOut = 55e18; + + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange3); + vm.prank(brokerAddress); + uint256 amountIn6Decimals = bancorExchangeProvider.swapOut( + exchangeId6Decimals, + address(reserveTokenWith6Decimals), + address(token), + amountOut + ); + + bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); + bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + uint256 amountIn18Decimals = bancorExchangeProvider.swapOut( + exchangeId18Decimals, + address(reserveToken), + address(token), + amountOut + ); + + assertTrue(amountIn18Decimals < amountIn6Decimals * 1e12); + assertEq(amountIn6Decimals, (amountIn18Decimals / 1e12) + 1); + } + + function test_swapOut_whenTokenInHas6DecimalsButNoRoundingNeeded_shouldNotRoundUp() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: 15e17 + }); + + vm.prank(brokerAddress); + uint256 amountIn18Decimals = bancorExchangeProvider.swapOut( + exchangeId, + address(token), + address(reserveToken), + amountOut + ); + + assertEq(amountIn18Decimals, 15e17); + + bancorExchangeProvider.destroyExchange(exchangeId, 0); + exchangeId = bancorExchangeProvider.createExchange(poolExchange4); + + vm.prank(brokerAddress); + uint256 amountIn6Decimals = bancorExchangeProvider.swapOut( + exchangeId, + address(tokenWith6Decimals), + address(reserveToken), + amountOut + ); + + assertEq(amountIn6Decimals, amountIn18Decimals / 1e12); + } } diff --git a/mento-core/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol b/mento-core/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol index c87f541..1a30aa2 100644 --- a/mento-core/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol +++ b/mento-core/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol @@ -349,11 +349,12 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP function test_mintFromExpansion_whenValidReserveRatioScalar_shouldReturnCorrectAmountAndEmit() public { // reserveRatioScalar is (1-0.000288617289022312) based of 10% yearly expansion rate // Formula: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio - // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * (1-0.000288617289022312) = 0.285631817919071438 - // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.285631817919071438) / 0.285631817919071438 - // ≈ 2_020_904,291074052815139287 + // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * (1-0.000288617289022312) + // newRatio = 0.28563181 (only 8 decimals) + // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.28563181) / 0.28563181 + // ≈ 2_021_098,420375517698816528 uint32 expectedReserveRatio = 28563181; - uint256 expectedAmountToMint = 2020904291074052815139287; + uint256 expectedAmountToMint = 2021098420375517698816528; uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); vm.expectEmit(true, true, true, true); @@ -371,18 +372,17 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP "Token supply should increase by minted amount" ); assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); - // 0.01% relative error tolerance because of precision loss when new reserve ratio is calculated - assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); } function test_mintFromExpansion_withSmallReserveRatioScalar_shouldReturnCorrectAmount() public { uint256 smallReserveRatioScalar = 1e18 * 0.00001; // 0.001% // Formula: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio - // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * 1e13/1e18 = 0.0000028571428 - // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.0000028571428) /0.0000028571428 - // amountToMint ≈ 699993000000000 + // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * 1e13/1e18 = 0.00000285 (only 8 decimals) + // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.00000285) /0.00000285 + // amountToMint ≈ 701.747.371.929.824,561403508771929824 uint32 expectedReserveRatio = 285; - uint256 expectedAmountToMint = 699993000000000 * 1e18; + uint256 expectedAmountToMint = 701747371929824561403508771929824; uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); vm.expectEmit(true, true, true, true); @@ -400,18 +400,17 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP "Token supply should increase by minted amount" ); assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); - // 1% relative error tolerance because of precision loss when new reserve ratio is calculated - assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.01, "Price should remain within 1% of initial price"); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); } function test_mintFromExpansion_withLargeReserveRatioScalar_shouldReturnCorrectAmount() public { uint256 largeReserveRatioScalar = 1e18 - 1; // Just below 100% // Formula: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio - // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * (1e18 -1)/1e18 ≈ 0.285714279999999999 - // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.285714279999999999) /0.285714279999999999 - // amountToMint ≈ 0.00000002450000049000 + // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * (1e18 -1)/1e18 ≈ 0.28571427 (only 8 decimals) + // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.28571427) /0.28571427 + // amountToMint ≈ 245.00001347500074112504 uint32 expectedReserveRatio = 28571427; - uint256 expectedAmountToMint = 24500000490; + uint256 expectedAmountToMint = 245000013475000741125; uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); vm.expectEmit(true, true, true, true); @@ -429,8 +428,7 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP "Token supply should increase by minted amount" ); assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); - // 0.01% relative error tolerance because of precision loss when new reserve ratio is calculated - assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); } function test_mintFromExpansion_withMultipleConsecutiveExpansions_shouldMintCorrectly() public { @@ -469,23 +467,23 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP 1e18 * 0.0001, // 0.01% relative error tolerance because of precision loss when new reserve ratio is calculated "Reserve ratio should be updated correctly within 0.01% tolerance" ); - assertApproxEqRel(initialPrice, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + assertEq(initialPrice, priceAfter, "Price should remain unchanged"); } - function testFuzz_mintFromExpansion(uint256 reserveRatioScalar) public { + function testFuzz_mintFromExpansion(uint256 _reserveRatioScalar) public { // 0.001% to 100% - reserveRatioScalar = bound(reserveRatioScalar, 1e18 * 0.00001, 1e18); + _reserveRatioScalar = bound(_reserveRatioScalar, 1e18 * 0.00001, 1e18); uint256 initialTokenSupply = poolExchange.tokenSupply; uint32 initialReserveRatio = poolExchange.reserveRatio; uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); - uint256 expectedReserveRatio = (uint256(initialReserveRatio) * reserveRatioScalar) / 1e18; + uint256 expectedReserveRatio = (uint256(initialReserveRatio) * _reserveRatioScalar) / 1e18; vm.expectEmit(true, true, true, true); emit ReserveRatioUpdated(exchangeId, uint32(expectedReserveRatio)); vm.prank(expansionControllerAddress); - uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar); + uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, _reserveRatioScalar); IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); @@ -497,8 +495,7 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP initialTokenSupply + amountToMint, "Token supply should increase by minted amount" ); - // 1% relative error tolerance because of precision loss when new reserve ratio is calculated - assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.01, "Price should remain within 1% of initial price"); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); } } @@ -683,28 +680,37 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan exchangeId = exchangeProvider.createExchange(poolExchange); } + function test_updateRatioForReward_whenNewRatioIsZero_shouldRevert() public { + // Use a very large reward that will make the denominator massive compared to numerator + uint256 veryLargeReward = type(uint256).max / 1e20; // Large but not large enough to overflow + + vm.expectRevert("New ratio must be greater than 0"); + vm.prank(expansionControllerAddress); + exchangeProvider.updateRatioForReward(exchangeId, veryLargeReward, 1e8); + } + function test_updateRatioForReward_whenCallerIsNotExpansionController_shouldRevert() public { vm.prank(makeAddr("NotExpansionController")); vm.expectRevert("Only ExpansionController can call this function"); - exchangeProvider.updateRatioForReward(exchangeId, reward); + exchangeProvider.updateRatioForReward(exchangeId, reward, 1e8); } function test_updateRatioForReward_whenExchangeIdIsInvalid_shouldRevert() public { vm.prank(expansionControllerAddress); vm.expectRevert("Exchange does not exist"); - exchangeProvider.updateRatioForReward(bytes32(0), reward); + exchangeProvider.updateRatioForReward(bytes32(0), reward, 1e8); } function test_updateRatioForReward_whenRewardLarger0_shouldReturnCorrectRatioAndEmit() public { - // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) - // reserveRatio = 200_000 / ((7_000_000_000 + 1_000) * 0.000100000002) ≈ 0.28571423... + // formula newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward) + // formula: newRatio = (7_000_000_000 * 0.28571428) / (7_000_000_000 + 1_000) = 0.28571423 uint32 expectedReserveRatio = 28571423; uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); vm.expectEmit(true, true, true, true); emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); vm.prank(expansionControllerAddress); - exchangeProvider.updateRatioForReward(exchangeId, reward); + exchangeProvider.updateRatioForReward(exchangeId, reward, 1e8); IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); @@ -720,16 +726,17 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan } function test_updateRatioForReward_whenRewardIsSmall_shouldReturnCorrectRatioAndEmit() public { - uint256 reward = 1e18; // 1 token - // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) - // reserveRatio = 200_000 / ((7_000_000_000 + 1) * 0.000100000002) ≈ 0.2857142799 + uint256 _reward = 1e18; // 1 token + // formula newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward) + // formula: newRatio = (7_000_000_000 * 0.28571428) / (7_000_000_000 + 1) = 0.28571427 + uint32 expectedReserveRatio = 28571427; uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); vm.expectEmit(true, true, true, true); emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); vm.prank(expansionControllerAddress); - exchangeProvider.updateRatioForReward(exchangeId, reward); + exchangeProvider.updateRatioForReward(exchangeId, _reward, 1e8); IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); @@ -737,16 +744,16 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); assertEq( poolExchangeAfter.tokenSupply, - poolExchange.tokenSupply + reward, + poolExchange.tokenSupply + _reward, "Token supply should increase by reward amount" ); assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); } function test_updateRatioForReward_whenRewardIsLarge_shouldReturnCorrectRatioAndEmit() public { - uint256 reward = 1_000_000_000 * 1e18; // 1 billion tokens - // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) - // reserveRatio = 200_000 / ((7_000_000_000 + 1_000_000_000) * 0.000100000002) ≈ 0.2499999950000... + uint256 _reward = 1_000_000_000 * 1e18; // 1 billion tokens + // formula newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward) + // formula: newRatio = (7_000_000_000 * 0.28571428) / (7_000_000_000 + 1_000_000_000) = 0.249999995 uint32 expectedReserveRatio = 24999999; uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); @@ -754,7 +761,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan vm.expectEmit(true, true, true, true); emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); vm.prank(expansionControllerAddress); - exchangeProvider.updateRatioForReward(exchangeId, reward); + exchangeProvider.updateRatioForReward(exchangeId, _reward, 1e8); IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); @@ -762,12 +769,30 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); assertEq( poolExchangeAfter.tokenSupply, - poolExchange.tokenSupply + reward, + poolExchange.tokenSupply + _reward, "Token supply should increase by reward amount" ); assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); } + function test_updateRatioForReward_whenSlippageIsHigherThanAccepted_shouldRevert() public { + uint256 _reward = 1_000_000_000 * 1e18; // 1 billion tokens + // formula newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward) + // formula: newRatio = (7_000_000_000 * 0.28571428) / (7_000_000_000 + 1_000_000_000) = 0.249999995 + // slippage = (newRatio - reserveRatio) / reserveRatio = (0.249999995 - 0.28571428) / 0.28571428 ~= -0.125 + + uint32 expectedReserveRatio = 24999999; + + vm.prank(expansionControllerAddress); + vm.expectRevert("Slippage exceeded"); + exchangeProvider.updateRatioForReward(exchangeId, _reward, 12 * 1e6); + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); + exchangeProvider.updateRatioForReward(exchangeId, _reward, 13 * 1e6); + } + function test_updateRatioForReward_withMultipleConsecutiveRewards() public { uint256 totalReward = 0; uint256 initialTokenSupply = poolExchange.tokenSupply; @@ -777,7 +802,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan vm.startPrank(expansionControllerAddress); for (uint256 i = 0; i < 5; i++) { - exchangeProvider.updateRatioForReward(exchangeId, reward); + exchangeProvider.updateRatioForReward(exchangeId, reward, 1e8); totalReward += reward; } vm.stopPrank(); @@ -805,7 +830,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); vm.prank(expansionControllerAddress); - exchangeProvider.updateRatioForReward(exchangeId, fuzzedReward); + exchangeProvider.updateRatioForReward(exchangeId, fuzzedReward, 1e8); IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); @@ -870,7 +895,7 @@ contract GoodDollarExchangeProviderTest_pausable is GoodDollarExchangeProviderTe exchangeProvider.mintFromInterest(exchangeId, 1e18); vm.expectRevert("Pausable: paused"); - exchangeProvider.updateRatioForReward(exchangeId, 1e18); + exchangeProvider.updateRatioForReward(exchangeId, 1e18, 100); } function test_unpause_whenCallerIsAvatar_shouldUnpauseAndEnableExchange() public { @@ -891,6 +916,6 @@ contract GoodDollarExchangeProviderTest_pausable is GoodDollarExchangeProviderTe exchangeProvider.mintFromExpansion(exchangeId, 1e18); exchangeProvider.mintFromInterest(exchangeId, 1e18); - exchangeProvider.updateRatioForReward(exchangeId, 1e18); + exchangeProvider.updateRatioForReward(exchangeId, 1e18, 1e8); } } diff --git a/mento-core/test/unit/goodDollar/GoodDollarExpansionController.t.sol b/mento-core/test/unit/goodDollar/GoodDollarExpansionController.t.sol index 4e46e62..23366d9 100644 --- a/mento-core/test/unit/goodDollar/GoodDollarExpansionController.t.sol +++ b/mento-core/test/unit/goodDollar/GoodDollarExpansionController.t.sol @@ -5,12 +5,15 @@ pragma solidity 0.8.18; import { Test } from "forge-std/Test.sol"; import { ERC20Mock } from "openzeppelin-contracts-next/contracts/mocks/ERC20Mock.sol"; +import { ERC20DecimalsMock } from "openzeppelin-contracts-next/contracts/mocks/ERC20DecimalsMock.sol"; import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarExpansionController.sol"; +import { GoodDollarExchangeProvider } from "contracts/goodDollar/GoodDollarExchangeProvider.sol"; import { IGoodDollarExpansionController } from "contracts/interfaces/IGoodDollarExpansionController.sol"; import { IGoodDollarExchangeProvider } from "contracts/interfaces/IGoodDollarExchangeProvider.sol"; import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; import { IDistributionHelper } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; +import { IReserve } from "contracts/interfaces/IReserve.sol"; import { GoodDollarExpansionControllerHarness } from "test/utils/harnesses/GoodDollarExpansionControllerHarness.sol"; @@ -326,6 +329,37 @@ contract GoodDollarExpansionControllerTest_mintUBIFromReserveBalance is GoodDoll assertEq(amountMinted, 0); } + function test_mintUBIFromReserveBalance_whenReserveAssetDecimalsIsLessThan18_shouldScaleCorrectly() public { + ERC20DecimalsMock reserveToken6DecimalsMock = new ERC20DecimalsMock("Reserve Token", "RES", 6); + IBancorExchangeProvider.PoolExchange memory pool2 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken6DecimalsMock), + tokenAddress: address(token), + tokenSupply: 7 * 1e9 * 1e18, + reserveBalance: 200_000 * 1e18, // internally scaled to 18 decimals + reserveRatio: 0.2 * 1e8, // 20% + exitContribution: 0.1 * 1e8 // 10% + }); + + uint256 reserveInterest = 1000e6; + deal(address(reserveToken6DecimalsMock), reserveAddress, 200_000 * 1e6 + reserveInterest); + + vm.mockCall( + address(exchangeProvider), + abi.encodeWithSelector(IBancorExchangeProvider(exchangeProvider).getPoolExchange.selector, exchangeId), + abi.encode(pool2) + ); + + vm.expectCall( + address(exchangeProvider), + abi.encodeWithSelector( + IGoodDollarExchangeProvider(exchangeProvider).mintFromInterest.selector, + exchangeId, + reserveInterest * 1e12 + ) + ); + expansionController.mintUBIFromReserveBalance(exchangeId); + } + function test_mintUBIFromReserveBalance_whenAdditionalReserveBalanceIsLargerThan0_shouldMintAndEmit() public { uint256 amountToMint = 1000e18; uint256 additionalReserveBalance = 1000e18; @@ -370,7 +404,7 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp expansionController.mintUBIFromExpansion("NotSetExchangeId"); } - function test_mintUBIFromExpansion_whenShouldNotExpand_shouldNotExpand() public { + function test_mintUBIFromExpansion_whenLessThanExpansionFrequencyPassed_shouldNotExpand() public { // doing one initial expansion to not be first expansion // since on first expansion the expansion is always applied once. expansionController.mintUBIFromExpansion(exchangeId); @@ -378,8 +412,7 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( exchangeId ); - uint256 lastExpansion = config.lastExpansion; - skip(lastExpansion + config.expansionFrequency - 1); + skip(config.expansionFrequency - 1); assertEq(expansionController.mintUBIFromExpansion(exchangeId), 0); } @@ -465,14 +498,11 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp // 1 day has passed since last expansion and expansion rate is 1% so the rate passed to the exchangeProvider // should be 0.99^1 = 0.99 uint256 reserveRatioScalar = 1e18 * 0.99; - skip(expansionFrequency + 1); + skip(expansionFrequency); uint256 amountToMint = 1000e18; uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); - vm.expectEmit(true, true, true, true); - emit ExpansionUBIMinted(exchangeId, amountToMint); - vm.expectCall( exchangeProvider, abi.encodeWithSelector( @@ -486,6 +516,9 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp abi.encodeWithSelector(IDistributionHelper(distributionHelper).onDistribution.selector, amountToMint) ); + vm.expectEmit(true, true, true, true); + emit ExpansionUBIMinted(exchangeId, amountToMint); + uint256 amountMinted = expansionController.mintUBIFromExpansion(exchangeId); IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( exchangeId @@ -496,7 +529,7 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp assertEq(config.lastExpansion, block.timestamp); } - function test_mintUBIFromExpansion_whenMultipleDaysPassed_shouldCalculateCorrectRateAndExpand() public { + function test_mintUBIFromExpansion_whenThreeAndAHalfDaysPassed_shouldMintCorrectAmountAndSetLastExpansion() public { // doing one initial expansion to not be first expansion // since on first expansion the expansion is always applied once. expansionController.mintUBIFromExpansion(exchangeId); @@ -505,7 +538,12 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp // should be 0.99^3 = 0.970299 uint256 reserveRatioScalar = 1e18 * 0.970299; - skip(3 * expansionFrequency + 1); + IGoodDollarExpansionController.ExchangeExpansionConfig memory stateBefore = expansionController.getExpansionConfig( + exchangeId + ); + + // 3.5 days have passed since last expansion + skip((7 * expansionFrequency) / 2); uint256 amountToMint = 1000e18; uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); @@ -533,7 +571,7 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp assertEq(amountMinted, amountToMint); assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); - assertEq(config.lastExpansion, block.timestamp); + assertEq(config.lastExpansion, stateBefore.lastExpansion + expansionFrequency * 3); } } @@ -543,18 +581,14 @@ contract GoodDollarExpansionControllerTest_getExpansionScalar is GoodDollarExpan function setUp() public override { super.setUp(); expansionController = new GoodDollarExpansionControllerHarness(false); + expansionController.initialize(exchangeProvider, distributionHelper, reserveAddress, avatarAddress); } - function test_getExpansionScaler_whenExpansionRateIs0_shouldReturn1e18() public { - IGoodDollarExpansionController.ExchangeExpansionConfig memory config = IGoodDollarExpansionController - .ExchangeExpansionConfig(0, 1, 0); - assertEq(expansionController.exposed_getReserveRatioScalar(config), 1e18); - } - - function test_getExpansionScaler_whenExpansionRateIs1_shouldReturn1() public { - IGoodDollarExpansionController.ExchangeExpansionConfig memory config = IGoodDollarExpansionController - .ExchangeExpansionConfig(1e18 - 1, 1, 0); - assertEq(expansionController.exposed_getReserveRatioScalar(config), 1); + function test_getExpansionScaler_whenStepReserveRatioScalerIs1_shouldReturn1() public { + vm.prank(avatarAddress); + expansionController.setExpansionConfig(exchangeId, 1e18 - 1, 1); + // stepReserveRatioScalar is 1e18 - expansionRate = 1e18 - (1e18 - 1) = 1 + assertEq(expansionController.exposed_getReserveRatioScalar(exchangeId), 1); } function testFuzz_getExpansionScaler( @@ -570,9 +604,11 @@ contract GoodDollarExpansionControllerTest_getExpansionScalar is GoodDollarExpan skip(lastExpansion + timeDelta); - IGoodDollarExpansionController.ExchangeExpansionConfig memory config = IGoodDollarExpansionController - .ExchangeExpansionConfig(expansionRate, expansionFrequency, lastExpansion); - uint256 scaler = expansionController.exposed_getReserveRatioScalar(config); + vm.prank(avatarAddress); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + expansionController.setLastExpansion(exchangeId, lastExpansion); + uint256 scaler = expansionController.exposed_getReserveRatioScalar(exchangeId); + assert(scaler >= 0 && scaler <= 1e18); } } @@ -611,6 +647,12 @@ contract GoodDollarExpansionControllerTest_mintRewardFromReserveRatio is GoodDol expansionController.mintRewardFromReserveRatio(exchangeId, makeAddr("To"), 0); } + function test_mintRewardFromReserveRatio_whenSlippageIsGreaterThan100_shouldRevert() public { + vm.prank(avatarAddress); + vm.expectRevert("Max slippage percentage cannot be greater than 100%"); + expansionController.mintRewardFromReserveRatio(exchangeId, makeAddr("To"), 1000e18, 1e8 + 1); + } + function test_mintRewardFromReserveRatio_whenCallerIsAvatar_shouldMintAndEmit() public { uint256 amountToMint = 1000e18; address to = makeAddr("To"); @@ -624,4 +666,99 @@ contract GoodDollarExpansionControllerTest_mintRewardFromReserveRatio is GoodDol assertEq(token.balanceOf(to), toBalanceBefore + amountToMint); } + + function test_mintRewardFromReserveRatio_whenCustomSlippage_shouldMintAndEmit() public { + uint256 amountToMint = 1000e18; + address to = makeAddr("To"); + uint256 toBalanceBefore = token.balanceOf(to); + + vm.expectEmit(true, true, true, true); + emit RewardMinted(exchangeId, to, amountToMint); + + vm.prank(avatarAddress); + expansionController.mintRewardFromReserveRatio(exchangeId, to, amountToMint, 1); + + assertEq(token.balanceOf(to), toBalanceBefore + amountToMint); + } +} + +contract GoodDollarExpansionControllerIntegrationTest is GoodDollarExpansionControllerTest { + address brokerAddress = makeAddr("Broker"); + GoodDollarExpansionController _expansionController; + GoodDollarExchangeProvider _exchangeProvider; + ERC20DecimalsMock reserveToken6DecimalsMock; + + function setUp() public override { + super.setUp(); + _exchangeProvider = new GoodDollarExchangeProvider(false); + _expansionController = new GoodDollarExpansionController(false); + + _expansionController.initialize(address(_exchangeProvider), distributionHelper, reserveAddress, avatarAddress); + _exchangeProvider.initialize(brokerAddress, reserveAddress, address(_expansionController), avatarAddress); + + reserveToken6DecimalsMock = new ERC20DecimalsMock("Reserve Token", "RES", 6); + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken6DecimalsMock), + tokenAddress: address(token), + tokenSupply: 7 * 1e9 * 1e18, + reserveBalance: 200_000 * 1e6, + reserveRatio: 0.2 * 1e8, // 20% + exitContribution: 0.1 * 1e8 // 10% + }); + + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token)), + abi.encode(true) + ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken6DecimalsMock)), + abi.encode(true) + ); + vm.prank(avatarAddress); + exchangeId = _exchangeProvider.createExchange(poolExchange); + } + + function test_mintUBIFromReserveBalance_whenReserveTokenHas6Decimals_shouldMintAndEmit() public { + uint256 reserveInterest = 1000e6; + // amountToMint = reserveInterest * tokenSupply / reserveBalance + uint256 amountToMint = 35_000_000e18; + + deal(address(reserveToken6DecimalsMock), reserveAddress, 200_000 * 1e6 + reserveInterest); + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.expectEmit(true, true, true, true); + emit InterestUBIMinted(exchangeId, amountToMint); + uint256 amountMinted = _expansionController.mintUBIFromReserveBalance(exchangeId); + + assertEq(amountMinted, amountToMint); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + } + + function test_mintUBIFromInterest_whenReserveTokenHas6Decimals_shouldMintAndEmit() public { + uint256 reserveInterest = 1000e6; + // amountToMint = reserveInterest * tokenSupply / reserveBalance + uint256 amountToMint = 35_000_000e18; + address interestCollector = makeAddr("InterestCollector"); + + deal(address(reserveToken6DecimalsMock), interestCollector, reserveInterest); + + vm.startPrank(interestCollector); + reserveToken6DecimalsMock.approve(address(_expansionController), reserveInterest); + + uint256 interestCollectorBalanceBefore = reserveToken6DecimalsMock.balanceOf(interestCollector); + uint256 reserveBalanceBefore = reserveToken6DecimalsMock.balanceOf(reserveAddress); + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.expectEmit(true, true, true, true); + emit InterestUBIMinted(exchangeId, amountToMint); + uint256 amountMinted = _expansionController.mintUBIFromInterest(exchangeId, reserveInterest); + + assertEq(amountMinted, amountToMint); + + assertEq(reserveToken6DecimalsMock.balanceOf(reserveAddress), reserveBalanceBefore + reserveInterest); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + assertEq(reserveToken6DecimalsMock.balanceOf(interestCollector), interestCollectorBalanceBefore - reserveInterest); + } } diff --git a/mento-core/test/unit/libraries/TradingLimits.t.sol b/mento-core/test/unit/libraries/TradingLimits.t.sol index 878fdf2..e9681f7 100644 --- a/mento-core/test/unit/libraries/TradingLimits.t.sol +++ b/mento-core/test/unit/libraries/TradingLimits.t.sol @@ -259,6 +259,13 @@ contract TradingLimitsTest is Test { assertEq(state.netflowGlobal, 0); } + function test_update_withZeroDeltaFlow_doesNotUpdate() public { + state = harness.update(state, configL0L1LG(300, 1000, 1 days, 10000, 1000000), 0, 18); + assertEq(state.netflow0, 0); + assertEq(state.netflow1, 0); + assertEq(state.netflowGlobal, 0); + } + function test_update_withL0_updatesActive() public { state = harness.update(state, configL0(500, 1000), 100 * 1e18, 18); assertEq(state.netflow0, 100); @@ -301,12 +308,31 @@ contract TradingLimitsTest is Test { state = harness.update(state, configLG(500000), 3 * 10e32, 18); } + function test_update_withTooSmallAmount_reverts() public { + int256 tooSmall = (type(int48).min - int256(1)) * 1e18; + vm.expectRevert(bytes("dFlow too small")); + state = harness.update(state, configLG(500000), tooSmall, 18); + } + function test_update_withOverflowOnAdd_reverts() public { ITradingLimits.Config memory config = configLG(int48(uint48(2 ** 47))); - int256 maxFlow = int256(uint256(type(uint48).max / 2)); + int256 maxFlow = int256(type(int48).max); state = harness.update(state, config, (maxFlow - 1000) * 1e18, 18); + state = harness.update(state, config, 1000 * 1e18, 18); + + vm.expectRevert(bytes("int48 addition overflow")); + state = harness.update(state, config, 1 * 1e18, 18); + } + + function test_update_withUnderflowOnAdd_reverts() public { + ITradingLimits.Config memory config = configLG(int48(uint48(2 ** 47))); + int256 minFlow = int256(type(int48).min); + + state = harness.update(state, config, (minFlow + 1000) * 1e18, 18); + state = harness.update(state, config, -1000 * 1e18, 18); + vm.expectRevert(bytes("int48 addition overflow")); - state = harness.update(state, config, 1002 * 10e18, 18); + state = harness.update(state, config, -1 * 1e18, 18); } } diff --git a/mento-core/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol b/mento-core/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol index ad6f080..aef0837 100644 --- a/mento-core/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol +++ b/mento-core/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol @@ -7,7 +7,11 @@ import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarEx contract GoodDollarExpansionControllerHarness is GoodDollarExpansionController { constructor(bool disabled) GoodDollarExpansionController(disabled) {} - function exposed_getReserveRatioScalar(ExchangeExpansionConfig calldata config) external returns (uint256) { - return _getReserveRatioScalar(config); + function exposed_getReserveRatioScalar(bytes32 exchangeId) external returns (uint256) { + return _getReserveRatioScalar(exchangeId); + } + + function setLastExpansion(bytes32 exchangeId, uint32 lastExpansion) external { + exchangeExpansionConfigs[exchangeId].lastExpansion = lastExpansion; } } From 33d621f6e467f7f70b17d3f9ea4b6b71286b2b9a Mon Sep 17 00:00:00 2001 From: sherlock-admin Date: Wed, 4 Dec 2024 15:56:54 +0000 Subject: [PATCH 2/2] Fix Review --- .../goodDollar/BancorExchangeProvider.sol.rej | 192 ++++ .../GoodDollarExchangeProvider.sol.rej | 73 ++ .../GoodDollarExpansionController.sol.rej | 158 +++ .../IGoodDollarExchangeProvider.sol.rej | 12 + .../IGoodDollarExpansionController.sol.rej | 38 + .../contracts/interfaces/IReserve.sol.rej | 8 + .../contracts/libraries/TradingLimits.sol.rej | 32 + mento-core/test/fork/BaseForkTest.sol.rej | 31 + mento-core/test/fork/ForkTests.t.sol.rej | 30 + .../goodDollar/BancorExchangeProvider.t.sol | 99 ++ .../BancorExchangeProvider.t.sol.rej | 902 ++++++++++++++++++ .../GoodDollarExchangeProvider.t.sol.rej | 277 ++++++ .../GoodDollarExpansionController.t.sol | 98 ++ .../GoodDollarExpansionController.t.sol.rej | 175 ++++ .../test/unit/libraries/TradingLimits.t.sol | 7 + .../unit/libraries/TradingLimits.t.sol.rej | 35 + ...odDollarExpansionControllerHarness.sol.rej | 15 + 17 files changed, 2182 insertions(+) create mode 100644 mento-core/contracts/goodDollar/BancorExchangeProvider.sol.rej create mode 100644 mento-core/contracts/goodDollar/GoodDollarExchangeProvider.sol.rej create mode 100644 mento-core/contracts/goodDollar/GoodDollarExpansionController.sol.rej create mode 100644 mento-core/contracts/interfaces/IGoodDollarExchangeProvider.sol.rej create mode 100644 mento-core/contracts/interfaces/IGoodDollarExpansionController.sol.rej create mode 100644 mento-core/contracts/interfaces/IReserve.sol.rej create mode 100644 mento-core/contracts/libraries/TradingLimits.sol.rej create mode 100644 mento-core/test/fork/BaseForkTest.sol.rej create mode 100644 mento-core/test/fork/ForkTests.t.sol.rej create mode 100644 mento-core/test/unit/goodDollar/BancorExchangeProvider.t.sol.rej create mode 100644 mento-core/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol.rej create mode 100644 mento-core/test/unit/goodDollar/GoodDollarExpansionController.t.sol.rej create mode 100644 mento-core/test/unit/libraries/TradingLimits.t.sol.rej create mode 100644 mento-core/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol.rej diff --git a/mento-core/contracts/goodDollar/BancorExchangeProvider.sol.rej b/mento-core/contracts/goodDollar/BancorExchangeProvider.sol.rej new file mode 100644 index 0000000..88e9ada --- /dev/null +++ b/mento-core/contracts/goodDollar/BancorExchangeProvider.sol.rej @@ -0,0 +1,192 @@ +diff a/mento-core/contracts/goodDollar/BancorExchangeProvider.sol b/mento-core/contracts/goodDollar/BancorExchangeProvider.sol (rejected hunks) +@@ -122,6 +122,13 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B + ) external view virtual returns (uint256 amountOut) { + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledAmountIn = amountIn * tokenPrecisionMultipliers[tokenIn]; ++ ++ if (tokenIn == exchange.tokenAddress) { ++ require(scaledAmountIn < exchange.tokenSupply, "amountIn is greater than tokenSupply"); ++ // apply exit contribution ++ scaledAmountIn = (scaledAmountIn * (MAX_WEIGHT - exchange.exitContribution)) / MAX_WEIGHT; ++ } ++ + uint256 scaledAmountOut = _getScaledAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); + amountOut = scaledAmountOut / tokenPrecisionMultipliers[tokenOut]; + return amountOut; +@@ -137,19 +144,27 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledAmountOut = amountOut * tokenPrecisionMultipliers[tokenOut]; + uint256 scaledAmountIn = _getScaledAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); +- amountIn = scaledAmountIn / tokenPrecisionMultipliers[tokenIn]; ++ ++ if (tokenIn == exchange.tokenAddress) { ++ // apply exit contribution ++ scaledAmountIn = (scaledAmountIn * MAX_WEIGHT) / (MAX_WEIGHT - exchange.exitContribution); ++ require(scaledAmountIn < exchange.tokenSupply, "amountIn is greater than tokenSupply"); ++ } ++ ++ amountIn = divAndRoundUp(scaledAmountIn, tokenPrecisionMultipliers[tokenIn]); + return amountIn; + } + + /// @inheritdoc IBancorExchangeProvider + function currentPrice(bytes32 exchangeId) public view returns (uint256 price) { + // calculates: reserveBalance / (tokenSupply * reserveRatio) +- require(exchanges[exchangeId].reserveAsset != address(0), "Exchange does not exist"); + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledReserveRatio = uint256(exchange.reserveRatio) * 1e10; ++ + UD60x18 denominator = wrap(exchange.tokenSupply).mul(wrap(scaledReserveRatio)); +- price = unwrap(wrap(exchange.reserveBalance).div(denominator)); +- return price; ++ uint256 priceScaled = unwrap(wrap(exchange.reserveBalance).div(denominator)); ++ ++ price = priceScaled / tokenPrecisionMultipliers[exchange.reserveAsset]; + } + + /* ============================================================ */ +@@ -165,9 +180,9 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B + + /// @inheritdoc IBancorExchangeProvider + function setReserve(address _reserve) public onlyOwner { +- require(address(_reserve) != address(0), "Reserve address must be set"); ++ require(_reserve != address(0), "Reserve address must be set"); + reserve = IReserve(_reserve); +- emit ReserveUpdated(address(_reserve)); ++ emit ReserveUpdated(_reserve); + } + + /// @inheritdoc IBancorExchangeProvider +@@ -197,8 +212,21 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B + ) public virtual onlyBroker returns (uint256 amountOut) { + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledAmountIn = amountIn * tokenPrecisionMultipliers[tokenIn]; ++ uint256 exitContribution = 0; ++ ++ if (tokenIn == exchange.tokenAddress) { ++ require(scaledAmountIn < exchange.tokenSupply, "amountIn is greater than tokenSupply"); ++ // apply exit contribution ++ exitContribution = (scaledAmountIn * exchange.exitContribution) / MAX_WEIGHT; ++ scaledAmountIn -= exitContribution; ++ } ++ + uint256 scaledAmountOut = _getScaledAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); ++ + executeSwap(exchangeId, tokenIn, scaledAmountIn, scaledAmountOut); ++ if (exitContribution > 0) { ++ _accountExitContribution(exchangeId, exitContribution); ++ } + + amountOut = scaledAmountOut / tokenPrecisionMultipliers[tokenOut]; + return amountOut; +@@ -214,9 +242,26 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledAmountOut = amountOut * tokenPrecisionMultipliers[tokenOut]; + uint256 scaledAmountIn = _getScaledAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); ++ ++ uint256 exitContribution = 0; ++ uint256 scaledAmountInWithExitContribution = scaledAmountIn; ++ ++ if (tokenIn == exchange.tokenAddress) { ++ // apply exit contribution ++ scaledAmountInWithExitContribution = (scaledAmountIn * MAX_WEIGHT) / (MAX_WEIGHT - exchange.exitContribution); ++ require( ++ scaledAmountInWithExitContribution < exchange.tokenSupply, ++ "amountIn required is greater than tokenSupply" ++ ); ++ exitContribution = scaledAmountInWithExitContribution - scaledAmountIn; ++ } ++ + executeSwap(exchangeId, tokenIn, scaledAmountIn, scaledAmountOut); ++ if (exitContribution > 0) { ++ _accountExitContribution(exchangeId, exitContribution); ++ } + +- amountIn = scaledAmountIn / tokenPrecisionMultipliers[tokenIn]; ++ amountIn = divAndRoundUp(scaledAmountInWithExitContribution, tokenPrecisionMultipliers[tokenIn]); + return amountIn; + } + +@@ -242,6 +287,9 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B + tokenPrecisionMultipliers[exchange.reserveAsset] = 10 ** (18 - uint256(reserveAssetDecimals)); + tokenPrecisionMultipliers[exchange.tokenAddress] = 10 ** (18 - uint256(tokenDecimals)); + ++ exchange.reserveBalance = exchange.reserveBalance * tokenPrecisionMultipliers[exchange.reserveAsset]; ++ exchange.tokenSupply = exchange.tokenSupply * tokenPrecisionMultipliers[exchange.tokenAddress]; ++ + exchanges[exchangeId] = exchange; + exchangeIds.push(exchangeId); + emit ExchangeCreated(exchangeId, exchange.reserveAsset, exchange.tokenAddress); +@@ -262,7 +310,7 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B + + function _setExitContribution(bytes32 exchangeId, uint32 exitContribution) internal { + require(exchanges[exchangeId].reserveAsset != address(0), "Exchange does not exist"); +- require(exitContribution <= MAX_WEIGHT, "Exit contribution is too high"); ++ require(exitContribution < MAX_WEIGHT, "Exit contribution is too high"); + + PoolExchange storage exchange = exchanges[exchangeId]; + exchange.exitContribution = exitContribution; +@@ -290,6 +338,37 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B + exchanges[exchangeId].tokenSupply = exchange.tokenSupply; + } + ++ /** ++ * @notice Accounting of exit contribution on a swap. ++ * @dev Accounting of exit contribution without changing the current price of an exchange. ++ * this is done by updating the reserve ratio and subtracting the exit contribution from the token supply. ++ * Formula: newRatio = (Supply * oldRatio) / (Supply - exitContribution) ++ * @param exchangeId The ID of the pool ++ * @param exitContribution The amount of the token to be removed from the pool, scaled to 18 decimals ++ */ ++ function _accountExitContribution(bytes32 exchangeId, uint256 exitContribution) internal { ++ PoolExchange memory exchange = getPoolExchange(exchangeId); ++ uint256 scaledReserveRatio = uint256(exchange.reserveRatio) * 1e10; ++ UD60x18 nominator = wrap(exchange.tokenSupply).mul(wrap(scaledReserveRatio)); ++ UD60x18 denominator = wrap(exchange.tokenSupply - exitContribution); ++ UD60x18 newRatioScaled = nominator.div(denominator); ++ ++ uint256 newRatio = unwrap(newRatioScaled) / 1e10; ++ ++ exchanges[exchangeId].reserveRatio = uint32(newRatio); ++ exchanges[exchangeId].tokenSupply -= exitContribution; ++ } ++ ++ /** ++ * @notice Division and rounding up if there is a remainder ++ * @param a The dividend ++ * @param b The divisor ++ * @return The result of the division rounded up ++ */ ++ function divAndRoundUp(uint256 a, uint256 b) internal pure returns (uint256) { ++ return (a / b) + (a % b > 0 ? 1 : 0); ++ } ++ + /** + * @notice Calculate the scaledAmountIn of tokenIn for a given scaledAmountOut of tokenOut + * @param exchange The pool exchange to operate on +@@ -307,8 +386,6 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B + if (tokenIn == exchange.reserveAsset) { + scaledAmountIn = fundCost(exchange.tokenSupply, exchange.reserveBalance, exchange.reserveRatio, scaledAmountOut); + } else { +- // apply exit contribution +- scaledAmountOut = (scaledAmountOut * MAX_WEIGHT) / (MAX_WEIGHT - exchange.exitContribution); + scaledAmountIn = saleCost(exchange.tokenSupply, exchange.reserveBalance, exchange.reserveRatio, scaledAmountOut); + } + } +@@ -341,8 +418,6 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B + exchange.reserveRatio, + scaledAmountIn + ); +- // apply exit contribution +- scaledAmountOut = (scaledAmountOut * (MAX_WEIGHT - exchange.exitContribution)) / MAX_WEIGHT; + } + } + +@@ -362,6 +437,7 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B + require(exchange.reserveRatio > 1, "Reserve ratio is too low"); + require(exchange.reserveRatio <= MAX_WEIGHT, "Reserve ratio is too high"); + require(exchange.exitContribution <= MAX_WEIGHT, "Exit contribution is too high"); ++ require(exchange.reserveBalance > 0, "Reserve balance must be greater than 0"); + } + + /** diff --git a/mento-core/contracts/goodDollar/GoodDollarExchangeProvider.sol.rej b/mento-core/contracts/goodDollar/GoodDollarExchangeProvider.sol.rej new file mode 100644 index 0000000..3972308 --- /dev/null +++ b/mento-core/contracts/goodDollar/GoodDollarExchangeProvider.sol.rej @@ -0,0 +1,73 @@ +diff a/mento-core/contracts/goodDollar/GoodDollarExchangeProvider.sol b/mento-core/contracts/goodDollar/GoodDollarExchangeProvider.sol (rejected hunks) +@@ -144,7 +144,12 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan + PoolExchange memory exchange = getPoolExchange(exchangeId); + + UD60x18 scaledRatio = wrap(uint256(exchange.reserveRatio) * 1e10); +- UD60x18 newRatio = scaledRatio.mul(wrap(reserveRatioScalar)); ++ ++ // The division and multiplication by 1e10 here ensures that the new ratio used for calculating the amount to mint ++ // is the same as the one set in the exchange but only scaled to 18 decimals. ++ // Ignored, because the division and multiplication by 1e10 is needed see comment above. ++ // slither-disable-next-line divide-before-multiply ++ UD60x18 newRatio = wrap((unwrap(scaledRatio.mul(wrap(reserveRatioScalar))) / 1e10) * 1e10); + + uint32 newRatioUint = uint32(unwrap(newRatio) / 1e10); + require(newRatioUint > 0, "New ratio must be greater than 0"); +@@ -175,14 +180,13 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan + ) external onlyExpansionController whenNotPaused returns (uint256 amountToMint) { + PoolExchange memory exchange = getPoolExchange(exchangeId); + +- uint256 reserveinterestScaled = reserveInterest * tokenPrecisionMultipliers[exchange.reserveAsset]; + uint256 amountToMintScaled = unwrap( +- wrap(reserveinterestScaled).mul(wrap(exchange.tokenSupply)).div(wrap(exchange.reserveBalance)) ++ wrap(reserveInterest).mul(wrap(exchange.tokenSupply)).div(wrap(exchange.reserveBalance)) + ); + amountToMint = amountToMintScaled / tokenPrecisionMultipliers[exchange.tokenAddress]; + + exchanges[exchangeId].tokenSupply += amountToMintScaled; +- exchanges[exchangeId].reserveBalance += reserveinterestScaled; ++ exchanges[exchangeId].reserveBalance += reserveInterest; + + return amountToMint; + } +@@ -190,21 +194,31 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan + /** + * @inheritdoc IGoodDollarExchangeProvider + * @dev Calculates the new reserve ratio needed to mint the G$ reward while keeping the current price the same. +- * calculation: newRatio = reserveBalance / (tokenSupply + reward) * currentPrice ++ * calculation: newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward) + */ +- function updateRatioForReward(bytes32 exchangeId, uint256 reward) external onlyExpansionController whenNotPaused { ++ function updateRatioForReward( ++ bytes32 exchangeId, ++ uint256 reward, ++ uint256 maxSlippagePercentage ++ ) external onlyExpansionController whenNotPaused { + PoolExchange memory exchange = getPoolExchange(exchangeId); + +- uint256 currentPriceScaled = currentPrice(exchangeId) * tokenPrecisionMultipliers[exchange.reserveAsset]; +- uint256 rewardScaled = reward * tokenPrecisionMultipliers[exchange.tokenAddress]; ++ uint256 scaledRatio = uint256(exchange.reserveRatio) * 1e10; ++ uint256 scaledReward = reward * tokenPrecisionMultipliers[exchange.tokenAddress]; ++ ++ UD60x18 numerator = wrap(exchange.tokenSupply).mul(wrap(scaledRatio)); ++ UD60x18 denominator = wrap(exchange.tokenSupply).add(wrap(scaledReward)); ++ uint256 newScaledRatio = unwrap(numerator.div(denominator)); ++ ++ uint32 newRatioUint = uint32(newScaledRatio / 1e10); ++ ++ require(newRatioUint > 0, "New ratio must be greater than 0"); + +- UD60x18 numerator = wrap(exchange.reserveBalance); +- UD60x18 denominator = wrap(exchange.tokenSupply + rewardScaled).mul(wrap(currentPriceScaled)); +- uint256 newRatioScaled = unwrap(numerator.div(denominator)); ++ uint256 allowedSlippage = (exchange.reserveRatio * maxSlippagePercentage) / MAX_WEIGHT; ++ require(exchange.reserveRatio - newRatioUint <= allowedSlippage, "Slippage exceeded"); + +- uint32 newRatioUint = uint32(newRatioScaled / 1e10); + exchanges[exchangeId].reserveRatio = newRatioUint; +- exchanges[exchangeId].tokenSupply += rewardScaled; ++ exchanges[exchangeId].tokenSupply += scaledReward; + + emit ReserveRatioUpdated(exchangeId, newRatioUint); + } diff --git a/mento-core/contracts/goodDollar/GoodDollarExpansionController.sol.rej b/mento-core/contracts/goodDollar/GoodDollarExpansionController.sol.rej new file mode 100644 index 0000000..4faa46e --- /dev/null +++ b/mento-core/contracts/goodDollar/GoodDollarExpansionController.sol.rej @@ -0,0 +1,158 @@ +diff a/mento-core/contracts/goodDollar/GoodDollarExpansionController.sol b/mento-core/contracts/goodDollar/GoodDollarExpansionController.sol (rejected hunks) +@@ -5,10 +5,10 @@ import { IGoodDollarExpansionController } from "contracts/interfaces/IGoodDollar + import { IGoodDollarExchangeProvider } from "contracts/interfaces/IGoodDollarExchangeProvider.sol"; + import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; + import { IERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/IERC20.sol"; ++import { IERC20Metadata } from "openzeppelin-contracts-next/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + import { IGoodDollar } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; + import { IDistributionHelper } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; + +-import { PausableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/security/PausableUpgradeable.sol"; + import { OwnableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; + import { unwrap, wrap, powu } from "prb/math/UD60x18.sol"; + +@@ -16,13 +16,16 @@ import { unwrap, wrap, powu } from "prb/math/UD60x18.sol"; + * @title GoodDollarExpansionController + * @notice Provides functionality to expand the supply of GoodDollars. + */ +-contract GoodDollarExpansionController is IGoodDollarExpansionController, PausableUpgradeable, OwnableUpgradeable { ++contract GoodDollarExpansionController is IGoodDollarExpansionController, OwnableUpgradeable { + /* ========================================================= */ + /* ==================== State Variables ==================== */ + /* ========================================================= */ + +- // MAX_WEIGHT is the max rate that can be assigned to an exchange +- uint256 public constant MAX_WEIGHT = 1e18; ++ // EXPANSION_MAX_WEIGHT is the max rate that can be assigned to an exchange ++ uint256 public constant EXPANSION_MAX_WEIGHT = 1e18; ++ ++ // BANCOR_MAX_WEIGHT is used for BPS calculations in GoodDollarExchangeProvider ++ uint32 public constant BANCOR_MAX_WEIGHT = 1e8; + + // Address of the distribution helper contract + IDistributionHelper public distributionHelper; +@@ -63,7 +66,6 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab + address _reserve, + address _avatar + ) public initializer { +- __Pausable_init(); + __Ownable_init(); + + setGoodDollarExchangeProvider(_goodDollarExchangeProvider); +@@ -123,7 +125,7 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab + + /// @inheritdoc IGoodDollarExpansionController + function setExpansionConfig(bytes32 exchangeId, uint64 expansionRate, uint32 expansionFrequency) external onlyAvatar { +- require(expansionRate < MAX_WEIGHT, "Expansion rate must be less than 100%"); ++ require(expansionRate < EXPANSION_MAX_WEIGHT, "Expansion rate must be less than 100%"); + require(expansionRate > 0, "Expansion rate must be greater than 0"); + require(expansionFrequency > 0, "Expansion frequency must be greater than 0"); + +@@ -134,19 +136,21 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab + } + + /// @inheritdoc IGoodDollarExpansionController +- function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external { ++ function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external returns (uint256 amountMinted) { + require(reserveInterest > 0, "Reserve interest must be greater than 0"); + IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) + .getPoolExchange(exchangeId); + +- uint256 amountToMint = goodDollarExchangeProvider.mintFromInterest(exchangeId, reserveInterest); +- + require(IERC20(exchange.reserveAsset).transferFrom(msg.sender, reserve, reserveInterest), "Transfer failed"); +- IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountToMint); ++ ++ uint256 reserveInterestScaled = reserveInterest * (10 ** (18 - IERC20Metadata(exchange.reserveAsset).decimals())); ++ amountMinted = goodDollarExchangeProvider.mintFromInterest(exchangeId, reserveInterestScaled); ++ ++ IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountMinted); + + // Ignored, because contracts only interacts with trusted contracts and tokens + // slither-disable-next-line reentrancy-events +- emit InterestUBIMinted(exchangeId, amountToMint); ++ emit InterestUBIMinted(exchangeId, amountMinted); + } + + /// @inheritdoc IGoodDollarExpansionController +@@ -154,7 +158,9 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab + IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) + .getPoolExchange(exchangeId); + +- uint256 contractReserveBalance = IERC20(exchange.reserveAsset).balanceOf(reserve); ++ uint256 contractReserveBalance = IERC20(exchange.reserveAsset).balanceOf(reserve) * ++ (10 ** (18 - IERC20Metadata(exchange.reserveAsset).decimals())); ++ + uint256 additionalReserveBalance = contractReserveBalance - exchange.reserveBalance; + if (additionalReserveBalance > 0) { + amountMinted = goodDollarExchangeProvider.mintFromInterest(exchangeId, additionalReserveBalance); +@@ -172,11 +178,10 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab + .getPoolExchange(exchangeId); + ExchangeExpansionConfig memory config = getExpansionConfig(exchangeId); + +- bool shouldExpand = block.timestamp > config.lastExpansion + config.expansionFrequency; ++ bool shouldExpand = block.timestamp >= config.lastExpansion + config.expansionFrequency; + if (shouldExpand || config.lastExpansion == 0) { +- uint256 reserveRatioScalar = _getReserveRatioScalar(config); ++ uint256 reserveRatioScalar = _getReserveRatioScalar(exchangeId); + +- exchangeExpansionConfigs[exchangeId].lastExpansion = uint32(block.timestamp); + amountMinted = goodDollarExchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar); + + IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountMinted); +@@ -190,12 +195,24 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab + + /// @inheritdoc IGoodDollarExpansionController + function mintRewardFromReserveRatio(bytes32 exchangeId, address to, uint256 amount) external onlyAvatar { ++ // Defaults to no slippage protection ++ mintRewardFromReserveRatio(exchangeId, to, amount, BANCOR_MAX_WEIGHT); ++ } ++ ++ /// @inheritdoc IGoodDollarExpansionController ++ function mintRewardFromReserveRatio( ++ bytes32 exchangeId, ++ address to, ++ uint256 amount, ++ uint256 maxSlippagePercentage ++ ) public onlyAvatar { + require(to != address(0), "Recipient address must be set"); + require(amount > 0, "Amount must be greater than 0"); ++ require(maxSlippagePercentage <= BANCOR_MAX_WEIGHT, "Max slippage percentage cannot be greater than 100%"); + IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) + .getPoolExchange(exchangeId); + +- goodDollarExchangeProvider.updateRatioForReward(exchangeId, amount); ++ goodDollarExchangeProvider.updateRatioForReward(exchangeId, amount, maxSlippagePercentage); + IGoodDollar(exchange.tokenAddress).mint(to, amount); + + // Ignored, because contracts only interacts with trusted contracts and tokens +@@ -219,20 +236,26 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab + + /** + * @notice Calculates the reserve ratio scalar for the given expansion config. +- * @param config The expansion config. ++ * @param exchangeId The ID of the exchange. + * @return reserveRatioScalar The reserve ratio scalar. + */ +- function _getReserveRatioScalar(ExchangeExpansionConfig memory config) internal view returns (uint256) { ++ function _getReserveRatioScalar(bytes32 exchangeId) internal returns (uint256) { ++ ExchangeExpansionConfig memory config = getExpansionConfig(exchangeId); + uint256 numberOfExpansions; + + // If there was no previous expansion, we expand once. + if (config.lastExpansion == 0) { + numberOfExpansions = 1; ++ exchangeExpansionConfigs[exchangeId].lastExpansion = uint32(block.timestamp); + } else { + numberOfExpansions = (block.timestamp - config.lastExpansion) / config.expansionFrequency; ++ // slither-disable-next-line divide-before-multiply ++ exchangeExpansionConfigs[exchangeId].lastExpansion = uint32( ++ config.lastExpansion + numberOfExpansions * config.expansionFrequency ++ ); + } + +- uint256 stepReserveRatioScalar = MAX_WEIGHT - config.expansionRate; ++ uint256 stepReserveRatioScalar = EXPANSION_MAX_WEIGHT - config.expansionRate; + return unwrap(powu(wrap(stepReserveRatioScalar), numberOfExpansions)); + } + diff --git a/mento-core/contracts/interfaces/IGoodDollarExchangeProvider.sol.rej b/mento-core/contracts/interfaces/IGoodDollarExchangeProvider.sol.rej new file mode 100644 index 0000000..5038f6f --- /dev/null +++ b/mento-core/contracts/interfaces/IGoodDollarExchangeProvider.sol.rej @@ -0,0 +1,12 @@ +diff a/mento-core/contracts/interfaces/IGoodDollarExchangeProvider.sol b/mento-core/contracts/interfaces/IGoodDollarExchangeProvider.sol (rejected hunks) +@@ -72,8 +72,9 @@ interface IGoodDollarExchangeProvider { + * @notice Calculates the reserve ratio needed to mint the given G$ reward. + * @param exchangeId The ID of the pool the G$ reward is minted from. + * @param reward The amount of G$ tokens to be minted as a reward. ++ * @param maxSlippagePercentage Maximum allowed percentage difference between new and old reserve ratio (0-1e8). + */ +- function updateRatioForReward(bytes32 exchangeId, uint256 reward) external; ++ function updateRatioForReward(bytes32 exchangeId, uint256 reward, uint256 maxSlippagePercentage) external; + + /** + * @notice Pauses the Exchange, disabling minting. diff --git a/mento-core/contracts/interfaces/IGoodDollarExpansionController.sol.rej b/mento-core/contracts/interfaces/IGoodDollarExpansionController.sol.rej new file mode 100644 index 0000000..5955627 --- /dev/null +++ b/mento-core/contracts/interfaces/IGoodDollarExpansionController.sol.rej @@ -0,0 +1,38 @@ +diff a/mento-core/contracts/interfaces/IGoodDollarExpansionController.sol b/mento-core/contracts/interfaces/IGoodDollarExpansionController.sol (rejected hunks) +@@ -130,8 +130,9 @@ interface IGoodDollarExpansionController { + * @notice Mints UBI as G$ tokens for a given pool from collected reserve interest. + * @param exchangeId The ID of the pool to mint UBI for. + * @param reserveInterest The amount of reserve tokens collected from interest. ++ * @return amountMinted The amount of G$ tokens minted. + */ +- function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external; ++ function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external returns (uint256 amountMinted); + + /** + * @notice Mints UBI as G$ tokens for a given pool by comparing the contract's reserve balance to the virtual balance. +@@ -148,10 +149,24 @@ interface IGoodDollarExpansionController { + function mintUBIFromExpansion(bytes32 exchangeId) external returns (uint256 amountMinted); + + /** +- * @notice Mints a reward of G$ tokens for a given pool. ++ * @notice Mints a reward of G$ tokens for a given pool. Defaults to no slippage protection. + * @param exchangeId The ID of the pool to mint a G$ reward for. + * @param to The address of the recipient. + * @param amount The amount of G$ tokens to mint. + */ + function mintRewardFromReserveRatio(bytes32 exchangeId, address to, uint256 amount) external; ++ ++ /** ++ * @notice Mints a reward of G$ tokens for a given pool. ++ * @param exchangeId The ID of the pool to mint a G$ reward for. ++ * @param to The address of the recipient. ++ * @param amount The amount of G$ tokens to mint. ++ * @param maxSlippagePercentage Maximum allowed percentage difference between new and old reserve ratio (0-100). ++ */ ++ function mintRewardFromReserveRatio( ++ bytes32 exchangeId, ++ address to, ++ uint256 amount, ++ uint256 maxSlippagePercentage ++ ) external; + } diff --git a/mento-core/contracts/interfaces/IReserve.sol.rej b/mento-core/contracts/interfaces/IReserve.sol.rej new file mode 100644 index 0000000..f51922b --- /dev/null +++ b/mento-core/contracts/interfaces/IReserve.sol.rej @@ -0,0 +1,8 @@ +diff a/mento-core/contracts/interfaces/IReserve.sol b/mento-core/contracts/interfaces/IReserve.sol (rejected hunks) +@@ -125,4 +125,6 @@ interface IReserve { + function removeOtherReserveAddress(address otherReserveAddress, uint256 index) external returns (bool); + + function collateralAssets(uint256 index) external view returns (address); ++ ++ function collateralAssetLastSpendingDay(address collateralAsset) external view returns (uint256); + } diff --git a/mento-core/contracts/libraries/TradingLimits.sol.rej b/mento-core/contracts/libraries/TradingLimits.sol.rej new file mode 100644 index 0000000..5c5b8b8 --- /dev/null +++ b/mento-core/contracts/libraries/TradingLimits.sol.rej @@ -0,0 +1,32 @@ +diff a/mento-core/contracts/libraries/TradingLimits.sol b/mento-core/contracts/libraries/TradingLimits.sol (rejected hunks) +@@ -45,6 +45,7 @@ library TradingLimits { + uint8 private constant L1 = 2; // 0b010 Limit1 + uint8 private constant LG = 4; // 0b100 LimitGlobal + int48 private constant MAX_INT48 = type(int48).max; ++ int48 private constant MIN_INT48 = type(int48).min; + + /** + * @notice Validate a trading limit configuration. +@@ -127,8 +128,13 @@ library TradingLimits { + int256 _deltaFlow, + uint8 decimals + ) internal view returns (ITradingLimits.State memory) { ++ if (_deltaFlow == 0) { ++ return self; ++ } ++ + int256 _deltaFlowUnits = _deltaFlow / int256((10 ** uint256(decimals))); + require(_deltaFlowUnits <= MAX_INT48, "dFlow too large"); ++ require(_deltaFlowUnits >= MIN_INT48, "dFlow too small"); + + int48 deltaFlowUnits = int48(_deltaFlowUnits); + if (deltaFlowUnits == 0) { +@@ -166,7 +172,7 @@ library TradingLimits { + */ + function safeINT48Add(int48 a, int48 b) internal pure returns (int48) { + int256 c = int256(a) + int256(b); +- require(c >= -1 * MAX_INT48 && c <= MAX_INT48, "int48 addition overflow"); ++ require(c >= MIN_INT48 && c <= MAX_INT48, "int48 addition overflow"); + return int48(c); + } + } diff --git a/mento-core/test/fork/BaseForkTest.sol.rej b/mento-core/test/fork/BaseForkTest.sol.rej new file mode 100644 index 0000000..01e0bb3 --- /dev/null +++ b/mento-core/test/fork/BaseForkTest.sol.rej @@ -0,0 +1,31 @@ +diff a/mento-core/test/fork/BaseForkTest.sol b/mento-core/test/fork/BaseForkTest.sol (rejected hunks) +@@ -9,6 +9,7 @@ import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; + // Interfaces + import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; + import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; ++import { IERC20 } from "contracts/interfaces/IERC20.sol"; + import { IBroker } from "contracts/interfaces/IBroker.sol"; + import { ICeloProxy } from "contracts/interfaces/ICeloProxy.sol"; + import { IOwnable } from "contracts/interfaces/IOwnable.sol"; +@@ -119,14 +120,16 @@ abstract contract BaseForkTest is Test { + + function mint(address asset, address to, uint256 amount, bool updateSupply) public { + if (asset == lookup("GoldToken")) { +- if (!updateSupply) { +- revert("BaseForkTest: can't mint GoldToken without updating supply"); +- } +- vm.prank(address(0)); +- IMint(asset).mint(to, amount); ++ // with L2 Celo, we need to transfer GoldToken to the user manually from the reserve ++ transferCeloFromReserve(to, amount); + return; + } + + deal(asset, to, amount, updateSupply); + } ++ ++ function transferCeloFromReserve(address to, uint256 amount) internal { ++ vm.prank(address(mentoReserve)); ++ IERC20(lookup("GoldToken")).transfer(to, amount); ++ } + } diff --git a/mento-core/test/fork/ForkTests.t.sol.rej b/mento-core/test/fork/ForkTests.t.sol.rej new file mode 100644 index 0000000..e07ec4f --- /dev/null +++ b/mento-core/test/fork/ForkTests.t.sol.rej @@ -0,0 +1,30 @@ +diff a/mento-core/test/fork/ForkTests.t.sol b/mento-core/test/fork/ForkTests.t.sol (rejected hunks) +@@ -45,7 +45,7 @@ import { GoodDollarTradingLimitsForkTest } from "./GoodDollar/TradingLimitsForkT + import { GoodDollarSwapForkTest } from "./GoodDollar/SwapForkTest.sol"; + import { GoodDollarExpansionForkTest } from "./GoodDollar/ExpansionForkTest.sol"; + +-contract Alfajores_ChainForkTest is ChainForkTest(ALFAJORES_ID, 1, uints(15)) {} ++contract Alfajores_ChainForkTest is ChainForkTest(ALFAJORES_ID, 1, uints(16)) {} + + contract Alfajores_P0E00_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 0) {} + +@@ -77,7 +77,9 @@ contract Alfajores_P0E13_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 1 + + contract Alfajores_P0E14_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 14) {} + +-contract Celo_ChainForkTest is ChainForkTest(CELO_ID, 1, uints(15)) {} ++contract Alfajores_P0E15_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 15) {} ++ ++contract Celo_ChainForkTest is ChainForkTest(CELO_ID, 1, uints(16)) {} + + contract Celo_P0E00_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 0) {} + +@@ -109,6 +111,8 @@ contract Celo_P0E13_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 13) {} + + contract Celo_P0E14_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 14) {} + ++contract Celo_P0E15_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 15) {} ++ + contract Celo_BancorExchangeProviderForkTest is BancorExchangeProviderForkTest(CELO_ID) {} + + contract Celo_GoodDollarTradingLimitsForkTest is GoodDollarTradingLimitsForkTest(CELO_ID) {} diff --git a/mento-core/test/unit/goodDollar/BancorExchangeProvider.t.sol b/mento-core/test/unit/goodDollar/BancorExchangeProvider.t.sol index d39a90d..0beea0a 100644 --- a/mento-core/test/unit/goodDollar/BancorExchangeProvider.t.sol +++ b/mento-core/test/unit/goodDollar/BancorExchangeProvider.t.sol @@ -98,6 +98,33 @@ contract BancorExchangeProviderTest is Test { exitContribution: 1e8 * 0.01 }); + poolExchange3 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveTokenWith6Decimals), + tokenAddress: address(token), + tokenSupply: 300_000 * 1e18, + reserveBalance: 60_000 * 1e6, + reserveRatio: 1e8 * 0.2, + exitContribution: 1e8 * 0.01 + }); + + poolExchange4 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(tokenWith6Decimals), + tokenSupply: 300_000 * 1e6, + reserveBalance: 60_000 * 1e18, + reserveRatio: 1e8 * 0.2, + exitContribution: 1e8 * 0.01 + }); + + poolExchange5 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveTokenWith6Decimals), + tokenAddress: address(tokenWith6Decimals), + tokenSupply: 300_000 * 1e6, + reserveBalance: 60_000 * 1e6, + reserveRatio: 1e8 * 0.2, + exitContribution: 1e8 * 0.01 + }); + vm.mockCall( reserveAddress, abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token)), @@ -306,6 +333,12 @@ contract BancorExchangeProviderTest_createExchange is BancorExchangeProviderTest bancorExchangeProvider.createExchange(poolExchange1); } + function test_createExchange_whenReserveBalanceIsZero_shouldRevert() public { + poolExchange1.reserveBalance = 0; + vm.expectRevert("Reserve balance must be greater than 0"); + bancorExchangeProvider.createExchange(poolExchange1); + } + function test_createExchange_whenReserveAssetIsNotCollateral_shouldRevert() public { vm.mockCall( reserveAddress, @@ -1244,6 +1277,7 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { tokenOut: address(reserveToken), amountIn: amountIn }); + assertEq(amountOut, expectedAmountOut); } @@ -1621,6 +1655,15 @@ contract BancorExchangeProviderTest_currentPrice is BancorExchangeProviderTest { assertEq(price, expectedPrice); } + function test_currentPrice_whenReserveTokenHasLessThan18Decimals_shouldReturnCorrectPrice() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange3); + // formula: price = reserveBalance / tokenSupply * reserveRatio + // calculation: 60_000 / 300_000 * 0.2 = 1 + uint256 expectedPrice = 1e6; + uint256 price = bancorExchangeProvider.currentPrice(exchangeId); + assertEq(price, expectedPrice); + } + function test_currentPrice_fuzz(uint256 reserveBalance, uint256 tokenSupply, uint256 reserveRatio) public { // reserveBalance range between 1 token and 10_000_000 tokens reserveBalance = bound(reserveBalance, 1e18, 10_000_000 * 1e18); @@ -1745,6 +1788,34 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { assertEq(tokenSupplyAfter, tokenSupplyBefore + amountOut); } + function test_swapIn_whenTokenInIsReserveAssetWith6Decimals_shouldSwapIn() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountIn = 1e6; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange3); + (, , uint256 tokenSupplyBefore, uint256 reserveBalanceBefore, , ) = bancorExchangeProvider.exchanges(exchangeId); + + uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveTokenWith6Decimals), + tokenOut: address(token), + amountIn: amountIn + }); + vm.prank(brokerAddress); + uint256 amountOut = bancorExchangeProvider.swapIn( + exchangeId, + address(reserveTokenWith6Decimals), + address(token), + amountIn + ); + assertEq(amountOut, expectedAmountOut); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore + amountIn * 1e12); + assertEq(tokenSupplyAfter, tokenSupplyBefore + amountOut); + } + function test_swapIn_whenTokenInIsToken_shouldSwapIn() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); uint256 amountIn = 1e18; @@ -1985,6 +2056,34 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { assertEq(tokenSupplyAfter, tokenSupplyBefore + amountOut); } + function test_swapOut_whenTokenInIsReserveAssetWith6Decimals_shouldSwapIn() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountOut = 1e18; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange3); + (, , uint256 tokenSupplyBefore, uint256 reserveBalanceBefore, , ) = bancorExchangeProvider.exchanges(exchangeId); + + uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveTokenWith6Decimals), + tokenOut: address(token), + amountOut: amountOut + }); + vm.prank(brokerAddress); + uint256 amountIn = bancorExchangeProvider.swapOut( + exchangeId, + address(reserveTokenWith6Decimals), + address(token), + amountOut + ); + assertEq(amountIn, expectedAmountIn); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertApproxEqRel(reserveBalanceAfter, reserveBalanceBefore + amountIn * 1e12, 1e18 * 0.0001); + assertEq(tokenSupplyAfter, tokenSupplyBefore + amountOut); + } + function test_swapOut_whenTokenInIsToken_shouldSwapOut() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); uint256 amountOut = 1e18; diff --git a/mento-core/test/unit/goodDollar/BancorExchangeProvider.t.sol.rej b/mento-core/test/unit/goodDollar/BancorExchangeProvider.t.sol.rej new file mode 100644 index 0000000..251bf00 --- /dev/null +++ b/mento-core/test/unit/goodDollar/BancorExchangeProvider.t.sol.rej @@ -0,0 +1,902 @@ +diff a/mento-core/test/unit/goodDollar/BancorExchangeProvider.t.sol b/mento-core/test/unit/goodDollar/BancorExchangeProvider.t.sol (rejected hunks) +@@ -5,6 +5,8 @@ pragma solidity 0.8.18; + + import { Test } from "forge-std/Test.sol"; + import { ERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/ERC20.sol"; ++import { ERC20DecimalsMock } from "openzeppelin-contracts-next/contracts/mocks/ERC20DecimalsMock.sol"; ++ + import { BancorExchangeProvider } from "contracts/goodDollar/BancorExchangeProvider.sol"; + import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; + import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; +@@ -30,16 +32,23 @@ contract BancorExchangeProviderTest is Test { + ERC20 public reserveToken; + ERC20 public token; + ERC20 public token2; ++ ERC20DecimalsMock public reserveTokenWith6Decimals; ++ ERC20DecimalsMock public tokenWith6Decimals; + + address public reserveAddress; + address public brokerAddress; + IBancorExchangeProvider.PoolExchange public poolExchange1; + IBancorExchangeProvider.PoolExchange public poolExchange2; ++ IBancorExchangeProvider.PoolExchange public poolExchange3; ++ IBancorExchangeProvider.PoolExchange public poolExchange4; ++ IBancorExchangeProvider.PoolExchange public poolExchange5; + + function setUp() public virtual { + reserveToken = new ERC20("cUSD", "cUSD"); + token = new ERC20("Good$", "G$"); + token2 = new ERC20("Good2$", "G2$"); ++ reserveTokenWith6Decimals = new ERC20DecimalsMock("Reserve Token", "RES", 6); ++ tokenWith6Decimals = new ERC20DecimalsMock("Token", "TKN", 6); + + brokerAddress = makeAddr("Broker"); + reserveAddress = makeAddr("Reserve"); +@@ -72,11 +108,22 @@ contract BancorExchangeProviderTest is Test { + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token2)), + abi.encode(true) + ); ++ vm.mockCall( ++ reserveAddress, ++ abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(tokenWith6Decimals)), ++ abi.encode(true) ++ ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken)), + abi.encode(true) + ); ++ ++ vm.mockCall( ++ reserveAddress, ++ abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveTokenWith6Decimals)), ++ abi.encode(true) ++ ); + } + + function initializeBancorExchangeProvider() internal returns (BancorExchangeProvider) { +@@ -164,12 +211,12 @@ contract BancorExchangeProviderTest_initilizerSettersGetters is BancorExchangePr + bancorExchangeProvider.setExitContribution(exchangeId, 1e5); + } + +- function test_setExitContribution_whenExitContributionAbove100Percent_shouldRevert() public { ++ function test_setExitContribution_whenExitContributionIsNotLessThan100Percent_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + uint32 maxWeight = bancorExchangeProvider.MAX_WEIGHT(); + vm.expectRevert("Exit contribution is too high"); +- bancorExchangeProvider.setExitContribution(exchangeId, maxWeight + 1); ++ bancorExchangeProvider.setExitContribution(exchangeId, maxWeight); + } + + function test_setExitContribution_whenSenderIsOwner_shouldUpdateAndEmit() public { +@@ -340,6 +393,23 @@ contract BancorExchangeProviderTest_createExchange is BancorExchangeProviderTest + assertEq(bancorExchangeProvider.tokenPrecisionMultipliers(address(reserveToken)), 1); + assertEq(bancorExchangeProvider.tokenPrecisionMultipliers(address(token)), 1); + } ++ ++ function test_createExchange_whenTokensHasLessThan18Decimals_shouldCreateExchangeWithCorrectSupplyAndBalance() ++ public ++ { ++ bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange5); ++ ++ IBancorExchangeProvider.PoolExchange memory poolExchange = bancorExchangeProvider.getPoolExchange(exchangeId); ++ assertEq(poolExchange.reserveAsset, poolExchange5.reserveAsset); ++ assertEq(poolExchange.tokenAddress, poolExchange5.tokenAddress); ++ assertEq(poolExchange.tokenSupply, poolExchange5.tokenSupply * 1e12); ++ assertEq(poolExchange.reserveBalance, poolExchange5.reserveBalance * 1e12); ++ assertEq(poolExchange.reserveRatio, poolExchange5.reserveRatio); ++ assertEq(poolExchange.exitContribution, poolExchange5.exitContribution); ++ ++ assertEq(bancorExchangeProvider.tokenPrecisionMultipliers(address(reserveTokenWith6Decimals)), 1e12); ++ assertEq(bancorExchangeProvider.tokenPrecisionMultipliers(address(tokenWith6Decimals)), 1e12); ++ } + } + + contract BancorExchangeProviderTest_destroyExchange is BancorExchangeProviderTest { +@@ -452,19 +522,6 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { + }); + } + +- function test_getAmountIn_whenTokenInIsTokenAndReserveBalanceIsZero_shouldRevert() public { +- poolExchange1.reserveBalance = 0; +- bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); +- +- vm.expectRevert("ERR_INVALID_RESERVE_BALANCE"); +- bancorExchangeProvider.getAmountIn({ +- exchangeId: exchangeId, +- tokenIn: address(token), +- tokenOut: address(reserveToken), +- amountOut: 1e18 +- }); +- } +- + function test_getAmountIn_whenTokenInIsTokenAndAmountOutLargerThanReserveBalance_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("ERR_INVALID_AMOUNT"); +@@ -487,19 +544,15 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { + assertEq(amountIn, 0); + } + +- function test_getAmountIn_whenTokenInIsTokenAndAmountOutEqualReserveBalance_shouldReturnSupply() public { +- // need to set exit contribution to 0 to make the formula work otherwise amountOut would need to be adjusted +- // to be equal to reserveBalance after exit contribution is applied +- poolExchange1.exitContribution = 0; ++ function test_getAmountIn_whenTokenInIsTokenAndAmountInIsLargerOrEqualSupply_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); +- uint256 expectedAmountIn = poolExchange1.tokenSupply; +- uint256 amountIn = bancorExchangeProvider.getAmountIn({ ++ vm.expectRevert("amountIn is greater than tokenSupply"); ++ bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: poolExchange1.reserveBalance + }); +- assertEq(amountIn, expectedAmountIn); + } + + function test_getAmountIn_whenTokenInIsTokenAndReserveRatioIs100Percent_shouldReturnCorrectAmount() public { +@@ -533,19 +586,6 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { + }); + } + +- function test_getAmountIn_whenTokenInIsReserveAssetAndReserveBalanceIsZero_shouldRevert() public { +- poolExchange1.reserveBalance = 0; +- bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); +- +- vm.expectRevert("ERR_INVALID_RESERVE_BALANCE"); +- bancorExchangeProvider.getAmountIn({ +- exchangeId: exchangeId, +- tokenIn: address(reserveToken), +- tokenOut: address(token), +- amountOut: 1e18 +- }); +- } +- + function test_getAmountIn_whenTokenInIsReserveAssetAndAmountOutIsZero_shouldReturnZero() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountIn = bancorExchangeProvider.getAmountIn({ +@@ -591,12 +631,12 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { + + function test_getAmountIn_whenTokenInIsToken_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); +- // formula: = tokenSupply * (-1 + (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ) +- // formula: amountIn = ------------------------------------------------------------------------------------------------ this is a fractional line +- // formula: = (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ++ // formula: = tokenSupply * (-1 + (reserveBalance/(reserveBalance - amountOut))^reserveRatio ) ++ // formula: amountIn = -------------------------------------------------------------------------------- ÷ (1-e) ++ // formula: = (reserveBalance / (reserveBalance - amountOut) )^reserveRatio + +- // calculation: (300000 * ( -1 + (60000 / (60000-(1/0.99)))^0.2))/(60000 / (60000-(1/0.99)))^0.2 = 1.010107812196722301 +- uint256 expectedAmountIn = 1010107812196722302; ++ // calculation: (300000 * ( -1 + (60000 / (60000-1))^0.2))/(60000 / (60000-1))^0.2 ÷ 0.99 = 1.010107744175084961 ++ uint256 expectedAmountIn = 1010107744175084961; + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), +@@ -624,11 +664,12 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { + function test_getAmountIn_whenTokenInIsTokenAndAmountOutIsSmall_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = 1e12; // 0.000001 token +- // formula: = tokenSupply * (-1 + (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ) +- // formula: amountIn = ------------------------------------------------------------------------------------------------ this is a fractional line +- // formula: = (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ++ // formula: = tokenSupply * (-1 + (reserveBalance/(reserveBalance - amountOut))^reserveRatio ) ++ // formula: amountIn = -------------------------------------------------------------------------------- ÷ (1-e) ++ // formula: = (reserveBalance / (reserveBalance - amountOut) )^reserveRatio + +- // calculation: (300000 * ( -1 + (60000 / (60000-(0.000001/0.99)))^0.2))/(60000 / (60000-(0.000001/0.99)))^0.2 ≈ 0.000001010101010107 ++ // calculation: (300000 * ( -1 + (60000 / (60000-0.000001))^0.2))/(60000 / (60000-0.000001))^0.2 ÷ 0.99 ≈ 0.000001010101010107 ++ // 1 wei difference due to precision loss + uint256 expectedAmountIn = 1010101010108; + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, +@@ -658,12 +699,12 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { + function test_getAmountIn_whenTokenInIsTokenAndAmountOutIsLarge_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = 59000e18; // 59_000 since total reserve is 60k +- // formula: = tokenSupply * (-1 + (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ) +- // formula: amountIn = ------------------------------------------------------------------------------------------------ this is a fractional line +- // formula: = (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ++ // formula: = tokenSupply * (-1 + (reserveBalance/(reserveBalance - amountOut))^reserveRatio ) ++ // formula: amountIn = -------------------------------------------------------------------------------- ÷ (1-e) ++ // formula: = (reserveBalance / (reserveBalance - amountOut) )^reserveRatio + +- // calculation: (300000 * ( -1 + (60000 / (60000-(59000/0.99)))^0.2))/(60000 / (60000-(59000/0.99)))^0.2 = 189649.078540006525698460 +- uint256 expectedAmountIn = 189649078540006525698460; ++ // calculation: (300000 * ( -1 + (60000 / (60000-59000))^0.2))/(60000 / (60000-59000))^0.2 ÷ 0.99 ≈ 169415.120269436288420151 ++ uint256 expectedAmountIn = 169415120269436288420151; + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), +@@ -681,9 +722,8 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { + bancorExchangeProvider.setExitContribution(exchangeId, 1e6); + bytes32 exchangeId2 = bancorExchangeProvider.createExchange(poolExchange2); + bancorExchangeProvider.setExitContribution(exchangeId2, 0); +- + uint256 amountOut = 116e18; +- // formula: amountIn = (tokenSupply * (( (amountOut + reserveBalance) / reserveBalance) ^ (reserveRatio) - 1)) / exitContribution ++ + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), +@@ -691,17 +731,13 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { + amountOut: amountOut + }); + +- // exit contribution is 1% +- uint256 amountOut2 = (amountOut * 100) / 99; +- assertTrue(amountOut < amountOut2); +- + uint256 amountIn2 = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId2, + tokenIn: address(token2), + tokenOut: address(reserveToken), +- amountOut: amountOut2 ++ amountOut: amountOut + }); +- assertEq(amountIn, amountIn2); ++ assertEq(amountIn, (amountIn2 * 100) / 99); + } + + function test_getAmountIn_whenDifferentTokenDecimals_shouldReturnCorrectAmount() public { +@@ -728,7 +764,7 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { + reserveAsset: address(reserveToken6), + tokenAddress: address(stableToken18), + tokenSupply: 100_000 * 1e18, // 100,000 +- reserveBalance: 50_000 * 1e18, // 50,000 ++ reserveBalance: 50_000 * 1e6, // 50,000 + reserveRatio: 1e8 * 0.5, // 50% + exitContribution: 0 + }); +@@ -932,6 +968,88 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { + // we allow up to 1% difference due to precision loss + assertApproxEqRel(reversedAmountOut, amountOut, 1e18 * 0.01); + } ++ ++ function test_getAmountIn_whenTokenInIsTokenWith6TokenDecimals_shouldRoundUpInFavorOfReserve() public { ++ bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange4); ++ ++ uint256 amountOut = 55e18; ++ uint256 amountIn6Decimals = bancorExchangeProvider.getAmountIn({ ++ exchangeId: exchangeId6Decimals, ++ tokenIn: address(tokenWith6Decimals), ++ tokenOut: address(reserveToken), ++ amountOut: amountOut ++ }); ++ ++ bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); ++ bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); ++ ++ uint256 amountIn18Decimals = bancorExchangeProvider.getAmountIn({ ++ exchangeId: exchangeId18Decimals, ++ tokenIn: address(token), ++ tokenOut: address(reserveToken), ++ amountOut: amountOut ++ }); ++ ++ assertTrue(amountIn18Decimals <= amountIn6Decimals * 1e12); ++ assertEq(amountIn6Decimals, (amountIn18Decimals / 1e12) + 1); ++ } ++ ++ function test_getAmountIn_whenTokenInIsReserveAssetWith6TokenDecimals_shouldRoundUpInFavorOfReserve() public { ++ bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange3); ++ ++ uint256 amountOut = 55e18; ++ uint256 amountIn6Decimals = bancorExchangeProvider.getAmountIn({ ++ exchangeId: exchangeId6Decimals, ++ tokenIn: address(reserveTokenWith6Decimals), ++ tokenOut: address(token), ++ amountOut: amountOut ++ }); ++ ++ bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); ++ bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); ++ ++ uint256 amountIn18Decimals = bancorExchangeProvider.getAmountIn({ ++ exchangeId: exchangeId18Decimals, ++ tokenIn: address(reserveToken), ++ tokenOut: address(token), ++ amountOut: amountOut ++ }); ++ ++ assertTrue(amountIn18Decimals <= amountIn6Decimals * 1e12); ++ assertEq(amountIn6Decimals, (amountIn18Decimals / 1e12) + 1); ++ } ++ ++ function test_getAmountIn_whenTokenInHas6DecimalsButNoRoundingNeeded_shouldNotRoundUp() public { ++ bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); ++ ++ uint256 amountOut = bancorExchangeProvider.getAmountOut({ ++ exchangeId: exchangeId, ++ tokenIn: address(token), ++ tokenOut: address(reserveToken), ++ amountIn: 15e17 ++ }); ++ ++ uint256 amountIn18Decimals = bancorExchangeProvider.getAmountIn({ ++ exchangeId: exchangeId, ++ tokenIn: address(token), ++ tokenOut: address(reserveToken), ++ amountOut: amountOut ++ }); ++ ++ assertEq(amountIn18Decimals, 15e17); ++ ++ bancorExchangeProvider.destroyExchange(exchangeId, 0); ++ bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange4); ++ ++ uint256 amountIn6Decimals = bancorExchangeProvider.getAmountIn({ ++ exchangeId: exchangeId6Decimals, ++ tokenIn: address(tokenWith6Decimals), ++ tokenOut: address(reserveToken), ++ amountOut: amountOut ++ }); ++ ++ assertEq(amountIn6Decimals, amountIn18Decimals / 1e12); ++ } + } + + contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { +@@ -998,18 +1116,6 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { + }); + } + +- function test_getAmountOut_whenTokenInIsReserveAssetAndReserveBalanceIsZero_shouldRevert() public { +- poolExchange1.reserveBalance = 0; +- bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); +- vm.expectRevert("ERR_INVALID_RESERVE_BALANCE"); +- bancorExchangeProvider.getAmountOut({ +- exchangeId: exchangeId, +- tokenIn: address(reserveToken), +- tokenOut: address(token), +- amountIn: 1e18 +- }); +- } +- + function test_getAmountOut_whenTokenInIsReserveAssetAndAmountInIsZero_shouldReturnZero() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = bancorExchangeProvider.getAmountOut({ +@@ -1037,41 +1143,6 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { + assertEq(amountOut, expectedAmountOut); + } + +- function test_getAmountOut_whenTokenInIsTokenAndSupplyIsZero_shouldRevert() public { +- poolExchange1.tokenSupply = 0; +- bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); +- vm.expectRevert("ERR_INVALID_SUPPLY"); +- bancorExchangeProvider.getAmountOut({ +- exchangeId: exchangeId, +- tokenIn: address(token), +- tokenOut: address(reserveToken), +- amountIn: 1e18 +- }); +- } +- +- function test_getAmountOut_whenTokenInIsTokenAndReserveBalanceIsZero_shouldRevert() public { +- poolExchange1.reserveBalance = 0; +- bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); +- vm.expectRevert("ERR_INVALID_RESERVE_BALANCE"); +- bancorExchangeProvider.getAmountOut({ +- exchangeId: exchangeId, +- tokenIn: address(token), +- tokenOut: address(reserveToken), +- amountIn: 1e18 +- }); +- } +- +- function test_getAmountOut_whenTokenInIsTokenAndAmountLargerSupply_shouldRevert() public { +- bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); +- vm.expectRevert("ERR_INVALID_AMOUNT"); +- bancorExchangeProvider.getAmountOut({ +- exchangeId: exchangeId, +- tokenIn: address(token), +- tokenOut: address(reserveToken), +- amountIn: poolExchange1.tokenSupply + 1 +- }); +- } +- + function test_getAmountOut_whenTokenInIsTokenAndAmountIsZero_shouldReturnZero() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = bancorExchangeProvider.getAmountOut({ +@@ -1083,25 +1154,23 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { + assertEq(amountOut, 0); + } + +- function test_getAmountOut_whenTokenInIsTokenAndAmountIsSupply_shouldReturnReserveBalanceMinusExitContribution() +- public +- { ++ function test_getAmountOut_whenTokenInIsTokenAndAmountInIsLargerOrEqualSupply_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); +- uint256 amountOut = bancorExchangeProvider.getAmountOut({ ++ vm.expectRevert("amountIn is greater than tokenSupply"); ++ bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), +- amountIn: poolExchange1.tokenSupply ++ amountIn: (poolExchange1.tokenSupply) + }); +- assertEq(amountOut, (poolExchange1.reserveBalance * (1e8 - poolExchange1.exitContribution)) / 1e8); + } + + function test_getAmountOut_whenTokenInIsTokenAndReserveRatioIs100Percent_shouldReturnCorrectAmount() public { + poolExchange1.reserveRatio = 1e8; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountIn = 1e18; +- // formula: amountOut = (reserveBalance * amountIn / tokenSupply) * (1-e) +- // calculation: (60_000 * 1 / 300_000) * 0.99 = 0.198 ++ // formula: amountOut = (reserveBalance * amountIn * (1-e)) / tokenSupply ++ // calculation: (60_000 * 1 * 0.99) / 300_000 = 0.198 + uint256 expectedAmountOut = 198000000000000000; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, +@@ -1128,13 +1198,13 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { + + function test_getAmountOut_whenTokenInIsToken_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); +- // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio)) +- // formula: amountOut = ---------------------------------------------------------------------------------- * (1 - e) +- // formula: = (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio) ++ // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn*(1-e)))^(1/reserveRatio)) ++ // formula: amountOut = ---------------------------------------------------------------------------------- ++ // formula: = (tokenSupply/(tokenSupply - amountIn*(1-e)))^(1/reserveRatio) + +- // calculation: ((60_000 *(-1+(300_000/(300_000-1))^5) ) / (300_000/(300_000-1))^5)*0.99 = 0.989993400021999963 ++ // calculation: ((60_000 *(-1+(300_000/(300_000-1*0.99))^5) ) / (300_000/(300_000-1*0.99))^5) = 0.989993466021562164 + // 1 wei difference due to precision loss +- uint256 expectedAmountOut = 989993400021999962; ++ uint256 expectedAmountOut = 989993466021562164; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), +@@ -1162,11 +1232,11 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { + function test_getAmountOut_whenTokenInIsTokenAndAmountOutIsSmall_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountIn = 1e12; // 0.000001 token +- // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio)) +- // formula: amountOut = ---------------------------------------------------------------------------------- * (1 - e) +- // formula: = (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio) ++ // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn * (1-e)))^(1/reserveRatio)) ++ // formula: amountOut = ---------------------------------------------------------------------------------- ++ // formula: = (tokenSupply/(tokenSupply - amountIn * (1-e)))^(1/reserveRatio) + +- // calculation: ((60_000 *(-1+(300_000/(300_000-0.000001))^5) )/(300_000/(300_000-0.000001))^5)*0.99 ≈ 0.0000009899999999934 ++ // calculation: ((60_000 *(-1+(300_000/(300_000-0.000001*0.99))^5) )/(300_000/(300_000-0.000001*0.99))^5) = 0.0000009899999999934 + uint256 expectedAmountOut = 989999999993; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, +@@ -1195,12 +1265,13 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { + function test_getAmountOut_whenTokenInIsTokenAndAmountInIsLarge_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountIn = 299_000 * 1e18; // 299,000 tokens only 300k supply +- // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio)) +- // formula: amountOut = ---------------------------------------------------------------------------------- * (1 - e) +- // formula: = (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio) ++ // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn * (1-e)))^(1/reserveRatio)) ++ // formula: amountOut = ---------------------------------------------------------------------------------- ++ // formula: = (tokenSupply/(tokenSupply - amountIn * (1-e)))^(1/reserveRatio) + +- // calculation: ((60_000 *(-1+(300_000/(300_000-299_000))^5) ) / (300_000/(300_000-299_000))^5)*0.99 ≈ 59399.999999975555555555 +- uint256 expectedAmountOut = 59399999999975555555555; ++ // calculation: ((60_000 *(-1+(300_000/(300_000-299_000*0.99))^5) ) / (300_000/(300_000-299_000 *0.99))^5) = 59999999975030522464200 ++ // 1 wei difference due to precision loss ++ uint256 expectedAmountOut = 59999999975030522464200; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), +@@ -1208,8 +1279,8 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { + amountIn: amountIn + }); + +- // we allow up to 1% difference due to precision loss +- assertApproxEqRel(amountOut, expectedAmountOut, 1e18 * 0.01); ++ // we allow up to 0.1% difference due to precision loss ++ assertApproxEqRel(amountOut, expectedAmountOut, 1e18 * 0.001); + } + + function test_getAmountOut_whenTokenInIsTokenAndExitContributionIsNonZero_shouldReturnCorrectAmount( +@@ -1223,6 +1294,7 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { + bancorExchangeProvider.setExitContribution(exchangeId2, 0); + + amountIn = bound(amountIn, 100, 299_000 * 1e18); ++ + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), +@@ -1233,9 +1305,9 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { + exchangeId: exchangeId2, + tokenIn: address(token2), + tokenOut: address(reserveToken), +- amountIn: amountIn ++ amountIn: (amountIn * 99) / 100 + }); +- assertEq(amountOut, (amountOut2 * 99) / 100); ++ assertEq(amountOut, amountOut2); + } + + function test_getAmountOut_whenDifferentTokenDecimals_shouldReturnCorrectAmount() public { +@@ -1262,7 +1334,7 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { + reserveAsset: address(reserveToken6), + tokenAddress: address(stableToken18), + tokenSupply: 100_000 * 1e18, // 100,000 +- reserveBalance: 50_000 * 1e18, // 50,000 ++ reserveBalance: 50_000 * 1e6, // 50,000 + reserveRatio: 1e8 * 0.5, // 50% + exitContribution: 0 + }); +@@ -1465,6 +1537,56 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { + // we allow up to 1% difference due to precision loss + assertApproxEqRel(reversedAmountIn, amountIn, 1e18 * 0.01); + } ++ ++ function test_getAmountOut_whenTokenOutIsTokenWith6TokenDecimals_shouldRoundDownInFavorOfReserve() public { ++ bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange4); ++ ++ uint256 amountIn = 55e18; ++ uint256 amountOut6Decimals = bancorExchangeProvider.getAmountOut({ ++ exchangeId: exchangeId6Decimals, ++ tokenIn: address(reserveToken), ++ tokenOut: address(tokenWith6Decimals), ++ amountIn: amountIn ++ }); ++ ++ bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); ++ bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); ++ ++ uint256 amountOut18Decimals = bancorExchangeProvider.getAmountOut({ ++ exchangeId: exchangeId18Decimals, ++ tokenIn: address(reserveToken), ++ tokenOut: address(token), ++ amountIn: amountIn ++ }); ++ ++ assertTrue(amountOut6Decimals * 1e12 < amountOut18Decimals); ++ assertEq(amountOut6Decimals, (amountOut18Decimals / 1e12)); ++ } ++ ++ function test_getAmountOut_whenTokenOutIsReserveAssetWith6TokenDecimals_shouldRoundDownInFavorOfReserve() public { ++ bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange3); ++ ++ uint256 amountIn = 55e18; ++ uint256 amountOut6Decimals = bancorExchangeProvider.getAmountOut({ ++ exchangeId: exchangeId6Decimals, ++ tokenIn: address(token), ++ tokenOut: address(reserveTokenWith6Decimals), ++ amountIn: amountIn ++ }); ++ ++ bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); ++ bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); ++ ++ uint256 amountOut18Decimals = bancorExchangeProvider.getAmountOut({ ++ exchangeId: exchangeId18Decimals, ++ tokenIn: address(token), ++ tokenOut: address(reserveToken), ++ amountIn: amountIn ++ }); ++ ++ assertTrue(amountOut6Decimals * 1e12 < amountOut18Decimals); ++ assertEq(amountOut6Decimals, (amountOut18Decimals / 1e12)); ++ } + } + + contract BancorExchangeProviderTest_currentPrice is BancorExchangeProviderTest { +@@ -1554,6 +1685,14 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { + bancorExchangeProvider.swapIn(exchangeId, address(token), address(token), 1e18); + } + ++ function test_swapIn_whenTokenInIsTokenAndAmountIsLargerOrEqualSupply_shouldRevert() public { ++ BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); ++ bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); ++ vm.prank(brokerAddress); ++ vm.expectRevert("amountIn is greater than tokenSupply"); ++ bancorExchangeProvider.swapIn(exchangeId, address(token), address(reserveToken), poolExchange1.tokenSupply); ++ } ++ + function test_swapIn_whenTokenInIsReserveAsset_shouldSwapIn() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountIn = 1e18; +@@ -1601,6 +1768,121 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { + assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); + assertEq(tokenSupplyAfter, tokenSupplyBefore - amountIn); + } ++ ++ function test_swapIn_whenTokenIsTokenWith6Decimals_shouldSwapIn() public { ++ BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); ++ uint256 amountIn = 1e18; ++ ++ bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange4); ++ (, , uint256 tokenSupplyBefore, uint256 reserveBalanceBefore, , ) = bancorExchangeProvider.exchanges(exchangeId); ++ ++ uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut({ ++ exchangeId: exchangeId, ++ tokenIn: address(reserveToken), ++ tokenOut: address(tokenWith6Decimals), ++ amountIn: amountIn ++ }); ++ vm.prank(brokerAddress); ++ uint256 amountOut = bancorExchangeProvider.swapIn( ++ exchangeId, ++ address(reserveToken), ++ address(tokenWith6Decimals), ++ amountIn ++ ); ++ assertEq(amountOut, expectedAmountOut); ++ ++ (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); ++ ++ assertEq(reserveBalanceAfter, reserveBalanceBefore + amountIn); ++ assertApproxEqRel(tokenSupplyAfter, tokenSupplyBefore + amountOut * 1e12, 1e18 * 0.0001); ++ } ++ ++ function test_swapIn_whenTokenInIsTokenAndExitContributionIsNonZero_shouldReturnSameAmountWhenSellIsDoneInMultipleSteps() ++ public ++ { ++ BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); ++ uint256 amountToSell = 100_000 * 1e18; ++ bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); ++ vm.prank(brokerAddress); ++ uint256 amountOutInOneSell = bancorExchangeProvider.swapIn( ++ exchangeId, ++ address(token), ++ address(reserveToken), ++ amountToSell ++ ); ++ ++ // destroy and recreate the exchange to reset everything ++ vm.prank(bancorExchangeProvider.owner()); ++ bancorExchangeProvider.destroyExchange(exchangeId, 0); ++ exchangeId = bancorExchangeProvider.createExchange(poolExchange1); ++ ++ uint256 amountOutInMultipleSells; ++ for (uint256 i = 0; i < 100_000; i++) { ++ vm.prank(brokerAddress); ++ amountOutInMultipleSells += bancorExchangeProvider.swapIn( ++ exchangeId, ++ address(token), ++ address(reserveToken), ++ 1e18 ++ ); ++ } ++ // we allow up to 0.1% difference due to precision loss on exitContribution accounting ++ assertApproxEqRel(amountOutInOneSell, amountOutInMultipleSells, 1e18 * 0.001); ++ } ++ ++ function test_swapIn_whenTokenOutIsTokenWith6Decimals_shouldRoundDownInFavorOfReserve() public { ++ BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); ++ uint256 amountIn = 55e18; ++ ++ bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange4); ++ vm.prank(brokerAddress); ++ uint256 amountOut6Decimals = bancorExchangeProvider.swapIn( ++ exchangeId6Decimals, ++ address(reserveToken), ++ address(tokenWith6Decimals), ++ amountIn ++ ); ++ ++ bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); ++ bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); ++ vm.prank(brokerAddress); ++ uint256 amountOut18Decimals = bancorExchangeProvider.swapIn( ++ exchangeId18Decimals, ++ address(reserveToken), ++ address(token), ++ amountIn ++ ); ++ ++ assertTrue(amountOut6Decimals * 1e12 < amountOut18Decimals); ++ assertEq(amountOut6Decimals, (amountOut18Decimals / 1e12)); ++ } ++ ++ function test_swapIn_whenTokenOutIsReserveAssetWith6Decimals_shouldRoundDownInFavorOfReserve() public { ++ BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); ++ uint256 amountIn = 55e18; ++ ++ bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange3); ++ vm.prank(brokerAddress); ++ uint256 amountOut6Decimals = bancorExchangeProvider.swapIn( ++ exchangeId6Decimals, ++ address(token), ++ address(reserveTokenWith6Decimals), ++ amountIn ++ ); ++ ++ bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); ++ bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); ++ vm.prank(brokerAddress); ++ uint256 amountOut18Decimals = bancorExchangeProvider.swapIn( ++ exchangeId18Decimals, ++ address(token), ++ address(reserveToken), ++ amountIn ++ ); ++ ++ assertTrue(amountOut6Decimals * 1e12 < amountOut18Decimals); ++ assertEq(amountOut6Decimals, (amountOut18Decimals / 1e12)); ++ } + } + + contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { +@@ -1619,7 +1901,7 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { + bancorExchangeProvider.swapOut("0xexchangeId", address(reserveToken), address(token), 1e18); + } + +- function test_swapOut_whenTokenInNotInexchange_shouldRevert() public { ++ function test_swapOut_whenTokenInNotInExchange_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); +@@ -1627,7 +1909,7 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { + bancorExchangeProvider.swapOut(exchangeId, address(token2), address(token), 1e18); + } + +- function test_swapOut_whenTokenOutNotInexchange_shouldRevert() public { ++ function test_swapOut_whenTokenOutNotInExchange_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); +@@ -1643,6 +1925,14 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { + bancorExchangeProvider.swapOut(exchangeId, address(token), address(token), 1e18); + } + ++ function test_swapOut_whenTokenInIsTokenAndAmountInIsLargerOrEqualSupply_shouldRevert() public { ++ BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); ++ bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); ++ vm.prank(brokerAddress); ++ vm.expectRevert("amountIn required is greater than tokenSupply"); ++ bancorExchangeProvider.swapOut(exchangeId, address(token), address(reserveToken), poolExchange1.reserveBalance); ++ } ++ + function test_swapOut_whenTokenInIsReserveAsset_shouldSwapOut() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountOut = 1e18; +@@ -1690,4 +2008,146 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { + assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); + assertEq(tokenSupplyAfter, tokenSupplyBefore - amountIn); + } ++ ++ function test_swapOut_whenTokenInIsTokenWith6Decimals_shouldSwapIn() public { ++ BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); ++ uint256 amountOut = 1e18; ++ ++ bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange4); ++ (, , uint256 tokenSupplyBefore, uint256 reserveBalanceBefore, , ) = bancorExchangeProvider.exchanges(exchangeId); ++ ++ uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn({ ++ exchangeId: exchangeId, ++ tokenIn: address(tokenWith6Decimals), ++ tokenOut: address(reserveToken), ++ amountOut: amountOut ++ }); ++ vm.prank(brokerAddress); ++ uint256 amountIn = bancorExchangeProvider.swapOut( ++ exchangeId, ++ address(tokenWith6Decimals), ++ address(reserveToken), ++ amountOut ++ ); ++ assertEq(amountIn, expectedAmountIn); ++ ++ (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); ++ ++ assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); ++ assertApproxEqRel(tokenSupplyAfter, tokenSupplyBefore - amountIn * 1e12, 1e18 * 0.0001); ++ } ++ function test_swapOut_whenTokenInIsTokenAndExitContributionIsNonZero_shouldReturnSameAmountWhenSellIsDoneInMultipleSteps() ++ public ++ { ++ BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); ++ uint256 amountToBuy = 50_000 * 1e18; ++ bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); ++ vm.prank(brokerAddress); ++ uint256 amountInInOneBuy = bancorExchangeProvider.swapOut( ++ exchangeId, ++ address(token), ++ address(reserveToken), ++ amountToBuy ++ ); ++ ++ bancorExchangeProvider.destroyExchange(exchangeId, 0); ++ exchangeId = bancorExchangeProvider.createExchange(poolExchange1); ++ ++ uint256 amountInInMultipleBuys; ++ for (uint256 i = 0; i < 50_000; i++) { ++ vm.prank(brokerAddress); ++ amountInInMultipleBuys += bancorExchangeProvider.swapOut(exchangeId, address(token), address(reserveToken), 1e18); ++ } ++ // we allow up to 0.1% difference due to precision loss on exitContribution accounting ++ assertApproxEqRel(amountInInOneBuy, amountInInMultipleBuys, 1e18 * 0.001); ++ } ++ ++ function test_swapOut_whenTokenInIsTokenWith6Decimals_shouldRoundUpInFavorOfReserve() public { ++ BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); ++ uint256 amountOut = 55e18; ++ ++ bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange4); ++ vm.prank(brokerAddress); ++ uint256 amountIn6Decimals = bancorExchangeProvider.swapOut( ++ exchangeId6Decimals, ++ address(tokenWith6Decimals), ++ address(reserveToken), ++ amountOut ++ ); ++ ++ bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); ++ bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); ++ vm.prank(brokerAddress); ++ uint256 amountIn18Decimals = bancorExchangeProvider.swapOut( ++ exchangeId18Decimals, ++ address(token), ++ address(reserveToken), ++ amountOut ++ ); ++ ++ assertTrue(amountIn18Decimals < amountIn6Decimals * 1e12); ++ assertEq(amountIn6Decimals, (amountIn18Decimals / 1e12) + 1); ++ } ++ ++ function test_swapOut_whenTokenInIsReserveAssetWith6Decimals_shouldRoundUpInFavorOfReserve() public { ++ BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); ++ uint256 amountOut = 55e18; ++ ++ bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange3); ++ vm.prank(brokerAddress); ++ uint256 amountIn6Decimals = bancorExchangeProvider.swapOut( ++ exchangeId6Decimals, ++ address(reserveTokenWith6Decimals), ++ address(token), ++ amountOut ++ ); ++ ++ bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); ++ bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); ++ vm.prank(brokerAddress); ++ uint256 amountIn18Decimals = bancorExchangeProvider.swapOut( ++ exchangeId18Decimals, ++ address(reserveToken), ++ address(token), ++ amountOut ++ ); ++ ++ assertTrue(amountIn18Decimals < amountIn6Decimals * 1e12); ++ assertEq(amountIn6Decimals, (amountIn18Decimals / 1e12) + 1); ++ } ++ ++ function test_swapOut_whenTokenInHas6DecimalsButNoRoundingNeeded_shouldNotRoundUp() public { ++ BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); ++ bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); ++ ++ uint256 amountOut = bancorExchangeProvider.getAmountOut({ ++ exchangeId: exchangeId, ++ tokenIn: address(token), ++ tokenOut: address(reserveToken), ++ amountIn: 15e17 ++ }); ++ ++ vm.prank(brokerAddress); ++ uint256 amountIn18Decimals = bancorExchangeProvider.swapOut( ++ exchangeId, ++ address(token), ++ address(reserveToken), ++ amountOut ++ ); ++ ++ assertEq(amountIn18Decimals, 15e17); ++ ++ bancorExchangeProvider.destroyExchange(exchangeId, 0); ++ exchangeId = bancorExchangeProvider.createExchange(poolExchange4); ++ ++ vm.prank(brokerAddress); ++ uint256 amountIn6Decimals = bancorExchangeProvider.swapOut( ++ exchangeId, ++ address(tokenWith6Decimals), ++ address(reserveToken), ++ amountOut ++ ); ++ ++ assertEq(amountIn6Decimals, amountIn18Decimals / 1e12); ++ } + } diff --git a/mento-core/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol.rej b/mento-core/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol.rej new file mode 100644 index 0000000..8fa4115 --- /dev/null +++ b/mento-core/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol.rej @@ -0,0 +1,277 @@ +diff a/mento-core/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol b/mento-core/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol (rejected hunks) +@@ -349,11 +349,12 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP + function test_mintFromExpansion_whenValidReserveRatioScalar_shouldReturnCorrectAmountAndEmit() public { + // reserveRatioScalar is (1-0.000288617289022312) based of 10% yearly expansion rate + // Formula: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio +- // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * (1-0.000288617289022312) = 0.285631817919071438 +- // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.285631817919071438) / 0.285631817919071438 +- // ≈ 2_020_904,291074052815139287 ++ // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * (1-0.000288617289022312) ++ // newRatio = 0.28563181 (only 8 decimals) ++ // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.28563181) / 0.28563181 ++ // ≈ 2_021_098,420375517698816528 + uint32 expectedReserveRatio = 28563181; +- uint256 expectedAmountToMint = 2020904291074052815139287; ++ uint256 expectedAmountToMint = 2021098420375517698816528; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); +@@ -371,18 +372,17 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP + "Token supply should increase by minted amount" + ); + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); +- // 0.01% relative error tolerance because of precision loss when new reserve ratio is calculated +- assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); ++ assertEq(priceBefore, priceAfter, "Price should remain unchanged"); + } + + function test_mintFromExpansion_withSmallReserveRatioScalar_shouldReturnCorrectAmount() public { + uint256 smallReserveRatioScalar = 1e18 * 0.00001; // 0.001% + // Formula: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio +- // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * 1e13/1e18 = 0.0000028571428 +- // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.0000028571428) /0.0000028571428 +- // amountToMint ≈ 699993000000000 ++ // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * 1e13/1e18 = 0.00000285 (only 8 decimals) ++ // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.00000285) /0.00000285 ++ // amountToMint ≈ 701.747.371.929.824,561403508771929824 + uint32 expectedReserveRatio = 285; +- uint256 expectedAmountToMint = 699993000000000 * 1e18; ++ uint256 expectedAmountToMint = 701747371929824561403508771929824; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); +@@ -400,18 +400,17 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP + "Token supply should increase by minted amount" + ); + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); +- // 1% relative error tolerance because of precision loss when new reserve ratio is calculated +- assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.01, "Price should remain within 1% of initial price"); ++ assertEq(priceBefore, priceAfter, "Price should remain unchanged"); + } + + function test_mintFromExpansion_withLargeReserveRatioScalar_shouldReturnCorrectAmount() public { + uint256 largeReserveRatioScalar = 1e18 - 1; // Just below 100% + // Formula: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio +- // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * (1e18 -1)/1e18 ≈ 0.285714279999999999 +- // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.285714279999999999) /0.285714279999999999 +- // amountToMint ≈ 0.00000002450000049000 ++ // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * (1e18 -1)/1e18 ≈ 0.28571427 (only 8 decimals) ++ // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.28571427) /0.28571427 ++ // amountToMint ≈ 245.00001347500074112504 + uint32 expectedReserveRatio = 28571427; +- uint256 expectedAmountToMint = 24500000490; ++ uint256 expectedAmountToMint = 245000013475000741125; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); +@@ -429,8 +428,7 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP + "Token supply should increase by minted amount" + ); + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); +- // 0.01% relative error tolerance because of precision loss when new reserve ratio is calculated +- assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); ++ assertEq(priceBefore, priceAfter, "Price should remain unchanged"); + } + + function test_mintFromExpansion_withMultipleConsecutiveExpansions_shouldMintCorrectly() public { +@@ -469,23 +467,23 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP + 1e18 * 0.0001, // 0.01% relative error tolerance because of precision loss when new reserve ratio is calculated + "Reserve ratio should be updated correctly within 0.01% tolerance" + ); +- assertApproxEqRel(initialPrice, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); ++ assertEq(initialPrice, priceAfter, "Price should remain unchanged"); + } + +- function testFuzz_mintFromExpansion(uint256 reserveRatioScalar) public { ++ function testFuzz_mintFromExpansion(uint256 _reserveRatioScalar) public { + // 0.001% to 100% +- reserveRatioScalar = bound(reserveRatioScalar, 1e18 * 0.00001, 1e18); ++ _reserveRatioScalar = bound(_reserveRatioScalar, 1e18 * 0.00001, 1e18); + + uint256 initialTokenSupply = poolExchange.tokenSupply; + uint32 initialReserveRatio = poolExchange.reserveRatio; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + +- uint256 expectedReserveRatio = (uint256(initialReserveRatio) * reserveRatioScalar) / 1e18; ++ uint256 expectedReserveRatio = (uint256(initialReserveRatio) * _reserveRatioScalar) / 1e18; + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, uint32(expectedReserveRatio)); + vm.prank(expansionControllerAddress); +- uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar); ++ uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, _reserveRatioScalar); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); +@@ -497,8 +495,7 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP + initialTokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); +- // 1% relative error tolerance because of precision loss when new reserve ratio is calculated +- assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.01, "Price should remain within 1% of initial price"); ++ assertEq(priceBefore, priceAfter, "Price should remain unchanged"); + } + } + +@@ -683,28 +680,37 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan + exchangeId = exchangeProvider.createExchange(poolExchange); + } + ++ function test_updateRatioForReward_whenNewRatioIsZero_shouldRevert() public { ++ // Use a very large reward that will make the denominator massive compared to numerator ++ uint256 veryLargeReward = type(uint256).max / 1e20; // Large but not large enough to overflow ++ ++ vm.expectRevert("New ratio must be greater than 0"); ++ vm.prank(expansionControllerAddress); ++ exchangeProvider.updateRatioForReward(exchangeId, veryLargeReward, 1e8); ++ } ++ + function test_updateRatioForReward_whenCallerIsNotExpansionController_shouldRevert() public { + vm.prank(makeAddr("NotExpansionController")); + vm.expectRevert("Only ExpansionController can call this function"); +- exchangeProvider.updateRatioForReward(exchangeId, reward); ++ exchangeProvider.updateRatioForReward(exchangeId, reward, 1e8); + } + + function test_updateRatioForReward_whenExchangeIdIsInvalid_shouldRevert() public { + vm.prank(expansionControllerAddress); + vm.expectRevert("Exchange does not exist"); +- exchangeProvider.updateRatioForReward(bytes32(0), reward); ++ exchangeProvider.updateRatioForReward(bytes32(0), reward, 1e8); + } + + function test_updateRatioForReward_whenRewardLarger0_shouldReturnCorrectRatioAndEmit() public { +- // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) +- // reserveRatio = 200_000 / ((7_000_000_000 + 1_000) * 0.000100000002) ≈ 0.28571423... ++ // formula newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward) ++ // formula: newRatio = (7_000_000_000 * 0.28571428) / (7_000_000_000 + 1_000) = 0.28571423 + uint32 expectedReserveRatio = 28571423; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); +- exchangeProvider.updateRatioForReward(exchangeId, reward); ++ exchangeProvider.updateRatioForReward(exchangeId, reward, 1e8); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); +@@ -720,16 +726,17 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan + } + + function test_updateRatioForReward_whenRewardIsSmall_shouldReturnCorrectRatioAndEmit() public { +- uint256 reward = 1e18; // 1 token +- // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) +- // reserveRatio = 200_000 / ((7_000_000_000 + 1) * 0.000100000002) ≈ 0.2857142799 ++ uint256 _reward = 1e18; // 1 token ++ // formula newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward) ++ // formula: newRatio = (7_000_000_000 * 0.28571428) / (7_000_000_000 + 1) = 0.28571427 ++ + uint32 expectedReserveRatio = 28571427; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); +- exchangeProvider.updateRatioForReward(exchangeId, reward); ++ exchangeProvider.updateRatioForReward(exchangeId, _reward, 1e8); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); +@@ -737,16 +744,16 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); + assertEq( + poolExchangeAfter.tokenSupply, +- poolExchange.tokenSupply + reward, ++ poolExchange.tokenSupply + _reward, + "Token supply should increase by reward amount" + ); + assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + } + + function test_updateRatioForReward_whenRewardIsLarge_shouldReturnCorrectRatioAndEmit() public { +- uint256 reward = 1_000_000_000 * 1e18; // 1 billion tokens +- // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) +- // reserveRatio = 200_000 / ((7_000_000_000 + 1_000_000_000) * 0.000100000002) ≈ 0.2499999950000... ++ uint256 _reward = 1_000_000_000 * 1e18; // 1 billion tokens ++ // formula newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward) ++ // formula: newRatio = (7_000_000_000 * 0.28571428) / (7_000_000_000 + 1_000_000_000) = 0.249999995 + + uint32 expectedReserveRatio = 24999999; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); +@@ -754,7 +761,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); +- exchangeProvider.updateRatioForReward(exchangeId, reward); ++ exchangeProvider.updateRatioForReward(exchangeId, _reward, 1e8); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); +@@ -762,12 +769,30 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); + assertEq( + poolExchangeAfter.tokenSupply, +- poolExchange.tokenSupply + reward, ++ poolExchange.tokenSupply + _reward, + "Token supply should increase by reward amount" + ); + assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + } + ++ function test_updateRatioForReward_whenSlippageIsHigherThanAccepted_shouldRevert() public { ++ uint256 _reward = 1_000_000_000 * 1e18; // 1 billion tokens ++ // formula newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward) ++ // formula: newRatio = (7_000_000_000 * 0.28571428) / (7_000_000_000 + 1_000_000_000) = 0.249999995 ++ // slippage = (newRatio - reserveRatio) / reserveRatio = (0.249999995 - 0.28571428) / 0.28571428 ~= -0.125 ++ ++ uint32 expectedReserveRatio = 24999999; ++ ++ vm.prank(expansionControllerAddress); ++ vm.expectRevert("Slippage exceeded"); ++ exchangeProvider.updateRatioForReward(exchangeId, _reward, 12 * 1e6); ++ ++ vm.expectEmit(true, true, true, true); ++ emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); ++ vm.prank(expansionControllerAddress); ++ exchangeProvider.updateRatioForReward(exchangeId, _reward, 13 * 1e6); ++ } ++ + function test_updateRatioForReward_withMultipleConsecutiveRewards() public { + uint256 totalReward = 0; + uint256 initialTokenSupply = poolExchange.tokenSupply; +@@ -777,7 +802,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan + + vm.startPrank(expansionControllerAddress); + for (uint256 i = 0; i < 5; i++) { +- exchangeProvider.updateRatioForReward(exchangeId, reward); ++ exchangeProvider.updateRatioForReward(exchangeId, reward, 1e8); + totalReward += reward; + } + vm.stopPrank(); +@@ -805,7 +830,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.prank(expansionControllerAddress); +- exchangeProvider.updateRatioForReward(exchangeId, fuzzedReward); ++ exchangeProvider.updateRatioForReward(exchangeId, fuzzedReward, 1e8); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); +@@ -870,7 +895,7 @@ contract GoodDollarExchangeProviderTest_pausable is GoodDollarExchangeProviderTe + exchangeProvider.mintFromInterest(exchangeId, 1e18); + + vm.expectRevert("Pausable: paused"); +- exchangeProvider.updateRatioForReward(exchangeId, 1e18); ++ exchangeProvider.updateRatioForReward(exchangeId, 1e18, 100); + } + + function test_unpause_whenCallerIsAvatar_shouldUnpauseAndEnableExchange() public { +@@ -891,6 +916,6 @@ contract GoodDollarExchangeProviderTest_pausable is GoodDollarExchangeProviderTe + + exchangeProvider.mintFromExpansion(exchangeId, 1e18); + exchangeProvider.mintFromInterest(exchangeId, 1e18); +- exchangeProvider.updateRatioForReward(exchangeId, 1e18); ++ exchangeProvider.updateRatioForReward(exchangeId, 1e18, 1e8); + } + } diff --git a/mento-core/test/unit/goodDollar/GoodDollarExpansionController.t.sol b/mento-core/test/unit/goodDollar/GoodDollarExpansionController.t.sol index 23366d9..7aaf662 100644 --- a/mento-core/test/unit/goodDollar/GoodDollarExpansionController.t.sol +++ b/mento-core/test/unit/goodDollar/GoodDollarExpansionController.t.sol @@ -564,6 +564,9 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp abi.encodeWithSelector(IDistributionHelper(distributionHelper).onDistribution.selector, amountToMint) ); + vm.expectEmit(true, true, true, true); + emit ExpansionUBIMinted(exchangeId, amountToMint); + uint256 amountMinted = expansionController.mintUBIFromExpansion(exchangeId); IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( exchangeId @@ -680,6 +683,101 @@ contract GoodDollarExpansionControllerTest_mintRewardFromReserveRatio is GoodDol assertEq(token.balanceOf(to), toBalanceBefore + amountToMint); } + + function test_mintRewardFromReserveRatio_whenCustomSlippage_shouldMintAndEmit() public { + uint256 amountToMint = 1000e18; + address to = makeAddr("To"); + uint256 toBalanceBefore = token.balanceOf(to); + + vm.expectEmit(true, true, true, true); + emit RewardMinted(exchangeId, to, amountToMint); + + vm.prank(avatarAddress); + expansionController.mintRewardFromReserveRatio(exchangeId, to, amountToMint, 1); + + assertEq(token.balanceOf(to), toBalanceBefore + amountToMint); + } +} + +contract GoodDollarExpansionControllerIntegrationTest is GoodDollarExpansionControllerTest { + address brokerAddress = makeAddr("Broker"); + GoodDollarExpansionController _expansionController; + GoodDollarExchangeProvider _exchangeProvider; + ERC20DecimalsMock reserveToken6DecimalsMock; + + function setUp() public override { + super.setUp(); + _exchangeProvider = new GoodDollarExchangeProvider(false); + _expansionController = new GoodDollarExpansionController(false); + + _expansionController.initialize(address(_exchangeProvider), distributionHelper, reserveAddress, avatarAddress); + _exchangeProvider.initialize(brokerAddress, reserveAddress, address(_expansionController), avatarAddress); + + reserveToken6DecimalsMock = new ERC20DecimalsMock("Reserve Token", "RES", 6); + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken6DecimalsMock), + tokenAddress: address(token), + tokenSupply: 7 * 1e9 * 1e18, + reserveBalance: 200_000 * 1e6, + reserveRatio: 0.2 * 1e8, // 20% + exitContribution: 0.1 * 1e8 // 10% + }); + + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token)), + abi.encode(true) + ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken6DecimalsMock)), + abi.encode(true) + ); + vm.prank(avatarAddress); + exchangeId = _exchangeProvider.createExchange(poolExchange); + } + + function test_mintUBIFromReserveBalance_whenReserveTokenHas6Decimals_shouldMintAndEmit() public { + uint256 reserveInterest = 1000e6; + // amountToMint = reserveInterest * tokenSupply / reserveBalance + uint256 amountToMint = 35_000_000e18; + + deal(address(reserveToken6DecimalsMock), reserveAddress, 200_000 * 1e6 + reserveInterest); + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.expectEmit(true, true, true, true); + emit InterestUBIMinted(exchangeId, amountToMint); + uint256 amountMinted = _expansionController.mintUBIFromReserveBalance(exchangeId); + + assertEq(amountMinted, amountToMint); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + } + + function test_mintUBIFromInterest_whenReserveTokenHas6Decimals_shouldMintAndEmit() public { + uint256 reserveInterest = 1000e6; + // amountToMint = reserveInterest * tokenSupply / reserveBalance + uint256 amountToMint = 35_000_000e18; + address interestCollector = makeAddr("InterestCollector"); + + deal(address(reserveToken6DecimalsMock), interestCollector, reserveInterest); + + vm.startPrank(interestCollector); + reserveToken6DecimalsMock.approve(address(_expansionController), reserveInterest); + + uint256 interestCollectorBalanceBefore = reserveToken6DecimalsMock.balanceOf(interestCollector); + uint256 reserveBalanceBefore = reserveToken6DecimalsMock.balanceOf(reserveAddress); + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.expectEmit(true, true, true, true); + emit InterestUBIMinted(exchangeId, amountToMint); + uint256 amountMinted = _expansionController.mintUBIFromInterest(exchangeId, reserveInterest); + + assertEq(amountMinted, amountToMint); + + assertEq(reserveToken6DecimalsMock.balanceOf(reserveAddress), reserveBalanceBefore + reserveInterest); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + assertEq(reserveToken6DecimalsMock.balanceOf(interestCollector), interestCollectorBalanceBefore - reserveInterest); + } } contract GoodDollarExpansionControllerIntegrationTest is GoodDollarExpansionControllerTest { diff --git a/mento-core/test/unit/goodDollar/GoodDollarExpansionController.t.sol.rej b/mento-core/test/unit/goodDollar/GoodDollarExpansionController.t.sol.rej new file mode 100644 index 0000000..78f8e04 --- /dev/null +++ b/mento-core/test/unit/goodDollar/GoodDollarExpansionController.t.sol.rej @@ -0,0 +1,175 @@ +diff a/mento-core/test/unit/goodDollar/GoodDollarExpansionController.t.sol b/mento-core/test/unit/goodDollar/GoodDollarExpansionController.t.sol (rejected hunks) +@@ -5,12 +5,15 @@ pragma solidity 0.8.18; + + import { Test } from "forge-std/Test.sol"; + import { ERC20Mock } from "openzeppelin-contracts-next/contracts/mocks/ERC20Mock.sol"; ++import { ERC20DecimalsMock } from "openzeppelin-contracts-next/contracts/mocks/ERC20DecimalsMock.sol"; + import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarExpansionController.sol"; ++import { GoodDollarExchangeProvider } from "contracts/goodDollar/GoodDollarExchangeProvider.sol"; + + import { IGoodDollarExpansionController } from "contracts/interfaces/IGoodDollarExpansionController.sol"; + import { IGoodDollarExchangeProvider } from "contracts/interfaces/IGoodDollarExchangeProvider.sol"; + import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; + import { IDistributionHelper } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; ++import { IReserve } from "contracts/interfaces/IReserve.sol"; + + import { GoodDollarExpansionControllerHarness } from "test/utils/harnesses/GoodDollarExpansionControllerHarness.sol"; + +@@ -326,6 +329,37 @@ contract GoodDollarExpansionControllerTest_mintUBIFromReserveBalance is GoodDoll + assertEq(amountMinted, 0); + } + ++ function test_mintUBIFromReserveBalance_whenReserveAssetDecimalsIsLessThan18_shouldScaleCorrectly() public { ++ ERC20DecimalsMock reserveToken6DecimalsMock = new ERC20DecimalsMock("Reserve Token", "RES", 6); ++ IBancorExchangeProvider.PoolExchange memory pool2 = IBancorExchangeProvider.PoolExchange({ ++ reserveAsset: address(reserveToken6DecimalsMock), ++ tokenAddress: address(token), ++ tokenSupply: 7 * 1e9 * 1e18, ++ reserveBalance: 200_000 * 1e18, // internally scaled to 18 decimals ++ reserveRatio: 0.2 * 1e8, // 20% ++ exitContribution: 0.1 * 1e8 // 10% ++ }); ++ ++ uint256 reserveInterest = 1000e6; ++ deal(address(reserveToken6DecimalsMock), reserveAddress, 200_000 * 1e6 + reserveInterest); ++ ++ vm.mockCall( ++ address(exchangeProvider), ++ abi.encodeWithSelector(IBancorExchangeProvider(exchangeProvider).getPoolExchange.selector, exchangeId), ++ abi.encode(pool2) ++ ); ++ ++ vm.expectCall( ++ address(exchangeProvider), ++ abi.encodeWithSelector( ++ IGoodDollarExchangeProvider(exchangeProvider).mintFromInterest.selector, ++ exchangeId, ++ reserveInterest * 1e12 ++ ) ++ ); ++ expansionController.mintUBIFromReserveBalance(exchangeId); ++ } ++ + function test_mintUBIFromReserveBalance_whenAdditionalReserveBalanceIsLargerThan0_shouldMintAndEmit() public { + uint256 amountToMint = 1000e18; + uint256 additionalReserveBalance = 1000e18; +@@ -370,7 +404,7 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp + expansionController.mintUBIFromExpansion("NotSetExchangeId"); + } + +- function test_mintUBIFromExpansion_whenShouldNotExpand_shouldNotExpand() public { ++ function test_mintUBIFromExpansion_whenLessThanExpansionFrequencyPassed_shouldNotExpand() public { + // doing one initial expansion to not be first expansion + // since on first expansion the expansion is always applied once. + expansionController.mintUBIFromExpansion(exchangeId); +@@ -378,8 +412,7 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( + exchangeId + ); +- uint256 lastExpansion = config.lastExpansion; +- skip(lastExpansion + config.expansionFrequency - 1); ++ skip(config.expansionFrequency - 1); + + assertEq(expansionController.mintUBIFromExpansion(exchangeId), 0); + } +@@ -465,14 +498,11 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp + // 1 day has passed since last expansion and expansion rate is 1% so the rate passed to the exchangeProvider + // should be 0.99^1 = 0.99 + uint256 reserveRatioScalar = 1e18 * 0.99; +- skip(expansionFrequency + 1); ++ skip(expansionFrequency); + + uint256 amountToMint = 1000e18; + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + +- vm.expectEmit(true, true, true, true); +- emit ExpansionUBIMinted(exchangeId, amountToMint); +- + vm.expectCall( + exchangeProvider, + abi.encodeWithSelector( +@@ -496,7 +529,7 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp + assertEq(config.lastExpansion, block.timestamp); + } + +- function test_mintUBIFromExpansion_whenMultipleDaysPassed_shouldCalculateCorrectRateAndExpand() public { ++ function test_mintUBIFromExpansion_whenThreeAndAHalfDaysPassed_shouldMintCorrectAmountAndSetLastExpansion() public { + // doing one initial expansion to not be first expansion + // since on first expansion the expansion is always applied once. + expansionController.mintUBIFromExpansion(exchangeId); +@@ -505,7 +538,12 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp + // should be 0.99^3 = 0.970299 + uint256 reserveRatioScalar = 1e18 * 0.970299; + +- skip(3 * expansionFrequency + 1); ++ IGoodDollarExpansionController.ExchangeExpansionConfig memory stateBefore = expansionController.getExpansionConfig( ++ exchangeId ++ ); ++ ++ // 3.5 days have passed since last expansion ++ skip((7 * expansionFrequency) / 2); + + uint256 amountToMint = 1000e18; + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); +@@ -533,7 +571,7 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp + + assertEq(amountMinted, amountToMint); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); +- assertEq(config.lastExpansion, block.timestamp); ++ assertEq(config.lastExpansion, stateBefore.lastExpansion + expansionFrequency * 3); + } + } + +@@ -543,18 +581,14 @@ contract GoodDollarExpansionControllerTest_getExpansionScalar is GoodDollarExpan + function setUp() public override { + super.setUp(); + expansionController = new GoodDollarExpansionControllerHarness(false); ++ expansionController.initialize(exchangeProvider, distributionHelper, reserveAddress, avatarAddress); + } + +- function test_getExpansionScaler_whenExpansionRateIs0_shouldReturn1e18() public { +- IGoodDollarExpansionController.ExchangeExpansionConfig memory config = IGoodDollarExpansionController +- .ExchangeExpansionConfig(0, 1, 0); +- assertEq(expansionController.exposed_getReserveRatioScalar(config), 1e18); +- } +- +- function test_getExpansionScaler_whenExpansionRateIs1_shouldReturn1() public { +- IGoodDollarExpansionController.ExchangeExpansionConfig memory config = IGoodDollarExpansionController +- .ExchangeExpansionConfig(1e18 - 1, 1, 0); +- assertEq(expansionController.exposed_getReserveRatioScalar(config), 1); ++ function test_getExpansionScaler_whenStepReserveRatioScalerIs1_shouldReturn1() public { ++ vm.prank(avatarAddress); ++ expansionController.setExpansionConfig(exchangeId, 1e18 - 1, 1); ++ // stepReserveRatioScalar is 1e18 - expansionRate = 1e18 - (1e18 - 1) = 1 ++ assertEq(expansionController.exposed_getReserveRatioScalar(exchangeId), 1); + } + + function testFuzz_getExpansionScaler( +@@ -570,9 +604,11 @@ contract GoodDollarExpansionControllerTest_getExpansionScalar is GoodDollarExpan + + skip(lastExpansion + timeDelta); + +- IGoodDollarExpansionController.ExchangeExpansionConfig memory config = IGoodDollarExpansionController +- .ExchangeExpansionConfig(expansionRate, expansionFrequency, lastExpansion); +- uint256 scaler = expansionController.exposed_getReserveRatioScalar(config); ++ vm.prank(avatarAddress); ++ expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); ++ expansionController.setLastExpansion(exchangeId, lastExpansion); ++ uint256 scaler = expansionController.exposed_getReserveRatioScalar(exchangeId); ++ + assert(scaler >= 0 && scaler <= 1e18); + } + } +@@ -611,6 +647,12 @@ contract GoodDollarExpansionControllerTest_mintRewardFromReserveRatio is GoodDol + expansionController.mintRewardFromReserveRatio(exchangeId, makeAddr("To"), 0); + } + ++ function test_mintRewardFromReserveRatio_whenSlippageIsGreaterThan100_shouldRevert() public { ++ vm.prank(avatarAddress); ++ vm.expectRevert("Max slippage percentage cannot be greater than 100%"); ++ expansionController.mintRewardFromReserveRatio(exchangeId, makeAddr("To"), 1000e18, 1e8 + 1); ++ } ++ + function test_mintRewardFromReserveRatio_whenCallerIsAvatar_shouldMintAndEmit() public { + uint256 amountToMint = 1000e18; + address to = makeAddr("To"); diff --git a/mento-core/test/unit/libraries/TradingLimits.t.sol b/mento-core/test/unit/libraries/TradingLimits.t.sol index e9681f7..9a2bed0 100644 --- a/mento-core/test/unit/libraries/TradingLimits.t.sol +++ b/mento-core/test/unit/libraries/TradingLimits.t.sol @@ -266,6 +266,13 @@ contract TradingLimitsTest is Test { assertEq(state.netflowGlobal, 0); } + function test_update_withZeroDeltaFlow_doesNotUpdate() public { + state = harness.update(state, configL0L1LG(300, 1000, 1 days, 10000, 1000000), 0, 18); + assertEq(state.netflow0, 0); + assertEq(state.netflow1, 0); + assertEq(state.netflowGlobal, 0); + } + function test_update_withL0_updatesActive() public { state = harness.update(state, configL0(500, 1000), 100 * 1e18, 18); assertEq(state.netflow0, 100); diff --git a/mento-core/test/unit/libraries/TradingLimits.t.sol.rej b/mento-core/test/unit/libraries/TradingLimits.t.sol.rej new file mode 100644 index 0000000..57c08e3 --- /dev/null +++ b/mento-core/test/unit/libraries/TradingLimits.t.sol.rej @@ -0,0 +1,35 @@ +diff a/mento-core/test/unit/libraries/TradingLimits.t.sol b/mento-core/test/unit/libraries/TradingLimits.t.sol (rejected hunks) +@@ -301,12 +308,31 @@ contract TradingLimitsTest is Test { + state = harness.update(state, configLG(500000), 3 * 10e32, 18); + } + ++ function test_update_withTooSmallAmount_reverts() public { ++ int256 tooSmall = (type(int48).min - int256(1)) * 1e18; ++ vm.expectRevert(bytes("dFlow too small")); ++ state = harness.update(state, configLG(500000), tooSmall, 18); ++ } ++ + function test_update_withOverflowOnAdd_reverts() public { + ITradingLimits.Config memory config = configLG(int48(uint48(2 ** 47))); +- int256 maxFlow = int256(uint256(type(uint48).max / 2)); ++ int256 maxFlow = int256(type(int48).max); + + state = harness.update(state, config, (maxFlow - 1000) * 1e18, 18); ++ state = harness.update(state, config, 1000 * 1e18, 18); ++ ++ vm.expectRevert(bytes("int48 addition overflow")); ++ state = harness.update(state, config, 1 * 1e18, 18); ++ } ++ ++ function test_update_withUnderflowOnAdd_reverts() public { ++ ITradingLimits.Config memory config = configLG(int48(uint48(2 ** 47))); ++ int256 minFlow = int256(type(int48).min); ++ ++ state = harness.update(state, config, (minFlow + 1000) * 1e18, 18); ++ state = harness.update(state, config, -1000 * 1e18, 18); ++ + vm.expectRevert(bytes("int48 addition overflow")); +- state = harness.update(state, config, 1002 * 10e18, 18); ++ state = harness.update(state, config, -1 * 1e18, 18); + } + } diff --git a/mento-core/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol.rej b/mento-core/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol.rej new file mode 100644 index 0000000..a49b258 --- /dev/null +++ b/mento-core/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol.rej @@ -0,0 +1,15 @@ +diff a/mento-core/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol b/mento-core/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol (rejected hunks) +@@ -7,7 +7,11 @@ import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarEx + contract GoodDollarExpansionControllerHarness is GoodDollarExpansionController { + constructor(bool disabled) GoodDollarExpansionController(disabled) {} + +- function exposed_getReserveRatioScalar(ExchangeExpansionConfig calldata config) external returns (uint256) { +- return _getReserveRatioScalar(config); ++ function exposed_getReserveRatioScalar(bytes32 exchangeId) external returns (uint256) { ++ return _getReserveRatioScalar(exchangeId); ++ } ++ ++ function setLastExpansion(bytes32 exchangeId, uint32 lastExpansion) external { ++ exchangeExpansionConfigs[exchangeId].lastExpansion = lastExpansion; + } + }