From c38925069a593bcb860d86aadcd59a46794996c5 Mon Sep 17 00:00:00 2001 From: philbow61 Date: Fri, 18 Oct 2024 15:48:41 +0200 Subject: [PATCH 1/7] test: add BancorExchangeProvider pricing tests --- .../goodDollar/BancorExchangeProvider.sol | 14 +- contracts/goodDollar/BancorFormula.sol | 58 +- .../goodDollar/BancorExchangeProvider.t.sol | 1051 ++++++++++++++++- 3 files changed, 1071 insertions(+), 52 deletions(-) diff --git a/contracts/goodDollar/BancorExchangeProvider.sol b/contracts/goodDollar/BancorExchangeProvider.sol index 3bd82b8c..fc99a84f 100644 --- a/contracts/goodDollar/BancorExchangeProvider.sol +++ b/contracts/goodDollar/BancorExchangeProvider.sol @@ -343,14 +343,9 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B if (tokenIn == exchange.reserveAsset) { scaledAmountIn = fundCost(exchange.tokenSupply, exchange.reserveBalance, exchange.reserveRatio, scaledAmountOut); } else { - scaledAmountIn = fundSupplyAmount( - exchange.tokenSupply, - exchange.reserveBalance, - exchange.reserveRatio, - scaledAmountOut - ); - - scaledAmountIn = (scaledAmountIn * MAX_WEIGHT) / (MAX_WEIGHT - exchange.exitContribution); + // apply exit contribution + scaledAmountOut = (scaledAmountOut * MAX_WEIGHT) / (MAX_WEIGHT - exchange.exitContribution); + scaledAmountIn = saleCost(exchange.tokenSupply, exchange.reserveBalance, exchange.reserveRatio, scaledAmountOut); } } @@ -376,13 +371,14 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B scaledAmountIn ); } else { - scaledAmountIn = (scaledAmountIn * (MAX_WEIGHT - exchange.exitContribution)) / MAX_WEIGHT; scaledAmountOut = saleTargetAmount( exchange.tokenSupply, exchange.reserveBalance, exchange.reserveRatio, scaledAmountIn ); + // apply exit contribution + scaledAmountOut = (scaledAmountOut * (MAX_WEIGHT - exchange.exitContribution)) / MAX_WEIGHT; } } diff --git a/contracts/goodDollar/BancorFormula.sol b/contracts/goodDollar/BancorFormula.sol index efc176c0..e083ae12 100644 --- a/contracts/goodDollar/BancorFormula.sol +++ b/contracts/goodDollar/BancorFormula.sol @@ -10,6 +10,7 @@ pragma solidity 0.8.18; * - bumped solidity version to 0.8.18 and removed SafeMath * - removed unused functions and variables * - scaled max weight from 1e6 to 1e8 reran all const python scripts for increased precision + * - added the saleCost() function that returns the amounIn of tokens required to receive a given amountOut of reserve tokens * */ @@ -218,7 +219,15 @@ contract BancorFormula { * calculates the target amount for a given conversion (in the reserve token) * * Formula: - * return = _reserveBalance * (1 - (1 - _amount / _supply) ^ (1000000 / _reserveWeight)) + * return = _reserveBalance * (1 - (1 - _amount / _supply) ^ (MAX_WEIGHT / _reserveWeight)) + * + * @dev by MentoLabs: This function actually calculates a different formula that is equivalent to the one above. + * But ensures the base of the power function is larger than 1, which is required by the power function. + * The formula is: + * = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn ))^(MAX_WEIGHT/reserveRatio)) + * formula: amountOut = ---------------------------------------------------------------------------------- + * = (tokenSupply/(tokenSupply - amountIn ))^(MAX_WEIGHT/reserveRatio) + * * * @param _supply liquid token supply * @param _reserveBalance reserve balance @@ -297,42 +306,55 @@ contract BancorFormula { } /** - * @dev given a pool token supply, reserve balance, reserve ratio and an amount of reserve tokens to fund with, - * calculates the amount of pool tokens received for purchasing with the given amount of reserve tokens + * Added by MentoLabs: + * @notice This function calculates the amount of tokens required to purchase a given amount of reserve tokens. + * @dev this formula was derived from the actual saleTargetAmount() function, and also ensures that the base of the power function is larger than 1. + * + * + * = tokenSupply * (-1 + (reserveBalance / (reserveBalance - amountOut) )^(reserveRatio/MAX_WEIGHT) ) + * Formula: amountIn = ------------------------------------------------------------------------------------------------ + * = (reserveBalance / (reserveBalance - amountOut) )^(reserveRatio/MAX_WEIGHT) * - * Formula: - * return = _supply * ((_amount / _reserveBalance + 1) ^ (_reserveRatio / MAX_WEIGHT) - 1) * * @param _supply pool token supply * @param _reserveBalance reserve balance - * @param _reserveRatio reserve ratio, represented in ppm (2-2000000) - * @param _amount amount of reserve tokens to fund with + * @param _reserveWeight reserve weight, represented in ppm + * @param _amount amount of reserve tokens to get the target amount for * - * @return pool token amount + * @return reserve token amount */ - function fundSupplyAmount( + function saleCost( uint256 _supply, uint256 _reserveBalance, - uint32 _reserveRatio, + uint32 _reserveWeight, uint256 _amount ) internal view returns (uint256) { // validate input require(_supply > 0, "ERR_INVALID_SUPPLY"); require(_reserveBalance > 0, "ERR_INVALID_RESERVE_BALANCE"); - require(_reserveRatio > 1 && _reserveRatio <= MAX_WEIGHT * 2, "ERR_INVALID_RESERVE_RATIO"); + require(_reserveWeight > 0 && _reserveWeight <= MAX_WEIGHT, "ERR_INVALID_RESERVE_WEIGHT"); - // special case for 0 amount + require(_amount <= _reserveBalance, "ERR_INVALID_AMOUNT"); + + // special case for 0 sell amount if (_amount == 0) return 0; - // special case if the reserve ratio = 100% - if (_reserveRatio == MAX_WEIGHT) return (_amount * _supply) / _reserveBalance; + // special case for selling the entire supply + if (_amount == _reserveBalance) return _supply; + + // special case if the weight = 100% + // base formula can be simplified to: + // Formula: amountIn = amountOut * supply / reserveBalance + // the +1 and -1 are to ensure that this function rounds up which is required to prevent protocol loss. + if (_reserveWeight == MAX_WEIGHT) return (_supply * _amount - 1) / _reserveBalance + 1; uint256 result; uint8 precision; - uint256 baseN = _reserveBalance + _amount; - (result, precision) = power(baseN, _reserveBalance, _reserveRatio, MAX_WEIGHT); - uint256 temp = (_supply * result) >> precision; - return temp - _supply; + uint256 baseD = _reserveBalance - _amount; + (result, precision) = power(_reserveBalance, baseD, _reserveWeight, MAX_WEIGHT); + uint256 temp1 = _supply * result; + uint256 temp2 = _supply << precision; + return (temp1 - temp2 - 1) / result + 1; } /** diff --git a/test/unit/goodDollar/BancorExchangeProvider.t.sol b/test/unit/goodDollar/BancorExchangeProvider.t.sol index 873dbdc2..0ffd6ebc 100644 --- a/test/unit/goodDollar/BancorExchangeProvider.t.sol +++ b/test/unit/goodDollar/BancorExchangeProvider.t.sol @@ -399,25 +399,181 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { function test_getAmountIn_whenExchangeDoesNotExist_shouldRevert() public { bytes32 exchangeId = "0xexchangeId"; vm.expectRevert("An exchange with the specified id does not exist"); - bancorExchangeProvider.getAmountIn(exchangeId, address(reserveToken), address(token), 1e18); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: 1e18 + }); } function test_getAmountIn_whenTokenInNotInExchange_shouldRevert() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); vm.expectRevert("tokenIn and tokenOut must match exchange"); - bancorExchangeProvider.getAmountOut(exchangeId, address(token2), address(token), 1e18); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token2), + tokenOut: address(token), + amountOut: 1e18 + }); } function test_getAmountIn_whenTokenOutNotInExchange_shouldRevert() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); vm.expectRevert("tokenIn and tokenOut must match exchange"); - bancorExchangeProvider.getAmountOut(exchangeId, address(token), address(token2), 1e18); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(token2), + amountOut: 1e18 + }); } - function test_getAmountIn_whenTokenInEqualsTokenOut_itReverts() public { + function test_getAmountIn_whenTokenInEqualsTokenOut_shouldRevert() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); vm.expectRevert("tokenIn and tokenOut must match exchange"); - bancorExchangeProvider.getAmountOut(exchangeId, address(token), address(token), 1e18); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(token), + amountOut: 1e18 + }); + } + + function test_getAmountIn_whenTokenInIsTokenAndTokenSupplyIsZero_shouldRevert() public { + poolExchange1.tokenSupply = 0; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + vm.expectRevert("ERR_INVALID_SUPPLY"); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: 1e18 + }); + } + + 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"); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: poolExchange1.reserveBalance + 1 + }); + } + + function test_getAmountIn_whenTokenInIsTokenAndAmountOutZero_shouldReturnZero() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: 0 + }); + 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; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 expectedAmountIn = poolExchange1.tokenSupply; + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: poolExchange1.reserveBalance + }); + assertEq(amountIn, expectedAmountIn); + } + + function test_getAmountIn_whenTokenInIsTokenAndReserveRatioIs100Percent_shouldReturnCorrectAmount() public { + poolExchange1.reserveRatio = 1e8; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = 12e18; + // formula: amountIn = (amountOut / (1-e)) * tokenSupply / reserveBalance + // calculation: (12 / 0.99) * 300_000 / 60_000 = 60.60606060606060606060606060606060606060 + uint256 expectedAmountIn = 60606060606060606060; + + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + assertEq(amountIn, expectedAmountIn); + } + + function test_getAmountIn_whenTokenInIsReserveAssetAndSupplyIsZero_shouldRevert() public { + poolExchange1.tokenSupply = 0; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + vm.expectRevert("ERR_INVALID_SUPPLY"); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: 1e18 + }); + } + + 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({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: 0 + }); + assertEq(amountIn, 0); + } + + function test_getAmountIn_whenTokenInIsReserveAssetAndReserveRatioIs100Percent_shouldReturnCorrectAmount() public { + poolExchange1.reserveRatio = 1e8; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = 12e18; + // formula: amountIn = (amountOut * reserveBalance - 1/1e18 ) / supply + 1/1e18 + // calculation: (12 * 60_000 - 1/1e18) / 300_000 + 1/1e18 = 2.4 + uint256 expectedAmountIn = 1e18 * 2.4; + + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + + assertEq(amountIn, expectedAmountIn); } function test_getAmountIn_whenTokenInIsReserveAsset_shouldReturnCorrectAmount() public { @@ -425,17 +581,357 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { // formula: amountIn = reserveBalance * (( (tokenSupply + amountOut) / tokenSupply) ^ (1/reserveRatio) - 1) // calculation: 60_000 * ((300_001/300_000)^(1/0.2) - 1) ≈ 1.000006666688888926 uint256 expectedAmountIn = 1000006666688888926; - uint256 amountIn = bancorExchangeProvider.getAmountIn(exchangeId, address(reserveToken), address(token), 1e18); + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: 1e18 + }); assertEq(amountIn, expectedAmountIn); } function test_getAmountIn_whenTokenInIsToken_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - // formula: amountIn = (tokenSupply * (( (amountOut + reserveBalance) / reserveBalance) ^ (reserveRatio) - 1)) \ exitContribution - // calculation: (300_000 * ( (60_001/60_000) ^0.2 - 1)) / (0.99) ≈ 1.010094276161615375 - uint256 expectedAmountIn = 1010094276161615375; - uint256 amountIn = bancorExchangeProvider.getAmountIn(exchangeId, address(token), address(reserveToken), 1e18); + // formula: = tokenSupply * (-1 + (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ) + // formula: amountIn = ------------------------------------------------------------------------------------------------ this is a fractional line + // formula: = (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio + + // calculation: (300000 * ( -1 + (60000 / (60000-(1/0.99)))^0.2))/(60000 / (60000-(1/0.99)))^0.2 = 1.010107812196722301 + uint256 expectedAmountIn = 1010107812196722302; + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: 1e18 + }); + assertEq(amountIn, expectedAmountIn); + } + + function test_getAmountIn_whenTokenInIsReserveAssetAndAmountOutIsSmall_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = 1e12; // 0.000001 token + // formula: amountIn = reserveBalance * ((amountOut/tokenSupply + 1)^(1/reserveRatio) - 1) + // calculation: 60_000 * ((0.000001/300_000 + 1)^(1/0.2) - 1) ≈ 0.00000100000000000666666 + uint256 expectedAmountIn = 1000000000007; + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + assertEq(amountIn, expectedAmountIn); + } + + 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 + + // calculation: (300000 * ( -1 + (60000 / (60000-(0.000001/0.99)))^0.2))/(60000 / (60000-(0.000001/0.99)))^0.2 ≈ 0.000001010101010107 + uint256 expectedAmountIn = 1010101010108; + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + assertEq(amountIn, expectedAmountIn); + } + + function test_getAmountIn_whenTokenInIsReserveAssetAndAmountOutIsLarge_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = 1000000e18; // 1,000,000 tokens + // formula: amountIn = reserveBalance * ((amountOut/tokenSupply + 1)^(1/reserveRatio) - 1) + // calculation: 60_000 * ((1_000_000/300_000 + 1)^(1/0.2) - 1) ≈ 91617283.9506172839506172839 + // 1 wei difference due to precision loss + uint256 expectedAmountIn = 91617283950617283950617284; + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + assertEq(amountIn, expectedAmountIn); + } + + 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 + + // calculation: (300000 * ( -1 + (60000 / (60000-(59000/0.99)))^0.2))/(60000 / (60000-(59000/0.99)))^0.2 = 189649.078540006525698460 + uint256 expectedAmountIn = 189649078540006525698460; + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + // we allow up to 1% difference due to precision loss + assertApproxEqRel(amountIn, expectedAmountIn, 1e18 * 0.01); + } + + function test_getAmountIn_whenTokenInIsTokenAndExitContributionIsNonZero_shouldReturnCorrectAmount() public { + // Set exit contribution to 1% (1e6 out of 1e8) for exchange 1 and 0 for exchange 2 + // all other parameters are the same + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + 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), + tokenOut: address(reserveToken), + 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 + }); + assertEq(amountIn, amountIn2); + } + + function test_getAmountIn_whenDifferentTokenDecimals_shouldReturnCorrectAmount() public { + // Create new tokens with different decimals + ERC20 reserveToken6 = new ERC20("Reserve6", "RSV6"); + ERC20 stableToken18 = new ERC20("Stable18", "STB18"); + + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(stableToken18)), + abi.encode(true) + ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken6)), + abi.encode(true) + ); + + // Mock decimals for these tokens + vm.mockCall(address(reserveToken6), abi.encodeWithSelector(reserveToken6.decimals.selector), abi.encode(6)); + vm.mockCall(address(stableToken18), abi.encodeWithSelector(stableToken18.decimals.selector), abi.encode(18)); + + IBancorExchangeProvider.PoolExchange memory newPoolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken6), + tokenAddress: address(stableToken18), + tokenSupply: 100_000 * 1e18, // 100,000 + reserveBalance: 50_000 * 1e18, // 50,000 + reserveRatio: 1e8 * 0.5, // 50% + exitContribution: 0 + }); + + bytes32 newExchangeId = bancorExchangeProvider.createExchange(newPoolExchange); + + uint256 amountOut = 1e18; // 1 StableToken out + + // Formula: reserveBalance * ((amountOut/tokenSupply + 1) ^ (1/reserveRatio) - 1) + // calculation: 50_000 * ((1/100_000 + 1) ^ (1/0.5) - 1) = 1.000005 in 6 decimals = 1000005 + uint256 expectedAmountIn = 1000005; + + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: newExchangeId, + tokenIn: address(reserveToken6), + tokenOut: address(stableToken18), + amountOut: amountOut + }); assertEq(amountIn, expectedAmountIn); + // 100_000 * ((1 + 1.000005/50000)^0.5 - 1) = 1.000005 in 18 decimals = 1000005000000000000 + uint256 reversedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: newExchangeId, + tokenIn: address(reserveToken6), + tokenOut: address(stableToken18), + amountIn: amountIn + }); + // we allow a 10 wei difference due to rounding errors + assertApproxEqAbs(amountOut, reversedAmountOut, 10); + } + + function test_getAmountIn_whenTokenInIsReserveAsset_fuzz(uint256 amountOut) public { + // these values are closed to the ones in the real exchange will be initialized with + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: 7_000_000_000 * 1e18, + reserveBalance: 200_000 * 1e18, + reserveRatio: uint32(28571428), + exitContribution: 1e7 + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + // amountOut range between 1 and 10_000_000 tokens + amountOut = bound(amountOut, 1e18, 10_000_000 * 1e18); + + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + + // Basic sanity checks + assertTrue(amountIn > 0, "Amount in should be positive"); + + // Verify the reverse swap + uint256 reversedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); + + // we allow up to 1% difference due to precision loss + assertApproxEqRel(reversedAmountOut, amountOut, 1e18 * 0.0001); + } + + function test_getAmountIn_whenTokenInIsToken_fuzz(uint256 amountOut) public { + // these values are closed to the ones in the real exchange will be initialized with + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: 7_000_000_000 * 1e18, + reserveBalance: 200_000 * 1e18, + reserveRatio: uint32(28571428), + exitContribution: 1e7 + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + // reserve balance is 200_000 and you can't get more than 90% of it because of the exit contribution + amountOut = bound(amountOut, 1e18, (200_000 * 1e18 * 90) / 100); + + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + // Basic sanity checks + assertTrue(0 < amountIn, "Amount in should be positive"); + + uint256 reversedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + + // we allow up to 0.01% difference due to precision loss + assertApproxEqRel(reversedAmountOut, amountOut, 1e18 * 0.0001); + } + + function test_getAmountIn_whenTokenInIsToken_fullFuzz( + uint256 amountOut, + uint256 reserveBalance, + uint256 tokenSupply, + uint256 reserveRatio, + uint256 exitContribution + ) public { + // reserveBalance range between 100 tokens and 10_000_000 tokens + reserveBalance = bound(reserveBalance, 100e18, 100_000_000 * 1e18); + // tokenSupply range between 100 tokens and 100_000_000 tokens + tokenSupply = bound(tokenSupply, 100e18, 100_000_000 * 1e18); + // reserveRatio range between 1% and 100% + reserveRatio = bound(reserveRatio, 1e6, 1e8); + // exitContribution range between 0% and 20% + exitContribution = bound(exitContribution, 0, 2e7); + + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: tokenSupply, + reserveBalance: reserveBalance, + reserveRatio: uint32(reserveRatio), + exitContribution: uint32(exitContribution) + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + // amountOut range between 0.0001 tokens and 70% of reserveBalance + amountOut = bound(amountOut, 0.0001e18, (reserveBalance * 7) / 10); + + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + // Basic sanity checks + assertTrue(amountIn > 0, "Amount in should be positive"); + + uint256 reversedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + + // we allow up to 0.01% difference due to precision loss + assertApproxEqRel(reversedAmountOut, amountOut, 1e18 * 0.0001); + } + + function test_getAmountIn_whenTokenInIsReserveAsset_fullFuzz( + uint256 amountOut, + uint256 reserveBalance, + uint256 tokenSupply, + uint256 reserveRatio + ) public { + // tokenSupply range between 100 tokens and 10_000_000 tokens + tokenSupply = bound(tokenSupply, 100e18, 10_000_000 * 1e18); + // reserveBalance range between 100 tokens and 10_000_000 tokens + reserveBalance = bound(reserveBalance, 100e18, 10_000_000 * 1e18); + // reserveRatio range between 5% and 100% + reserveRatio = bound(reserveRatio, 5e6, 1e8); + + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: tokenSupply, + reserveBalance: reserveBalance, + reserveRatio: uint32(reserveRatio), + exitContribution: 0 // no exit contribution because reserveToken in + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + // amountOut range between 0.0001 tokens and 3 times the current tokenSupply + amountOut = bound(amountOut, 0.0001e18, tokenSupply * 3); + + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + + // Basic sanity checks + assertTrue(amountIn > 0, "Amount in should be positive"); + + uint256 reversedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); + + // we allow up to 1% difference due to precision loss + assertApproxEqRel(reversedAmountOut, amountOut, 1e18 * 0.01); } } @@ -450,43 +946,525 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { function test_getAmountOut_whenExchangeDoesNotExist_shouldRevert() public { bytes32 exchangeId = "0xexchangeId"; vm.expectRevert("An exchange with the specified id does not exist"); - bancorExchangeProvider.getAmountOut(exchangeId, address(reserveToken), address(token), 1e18); + bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: 1e18 + }); } function test_getAmountOut_whenTokenInNotInExchange_shouldRevert() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); vm.expectRevert("tokenIn and tokenOut must match exchange"); - bancorExchangeProvider.getAmountOut(exchangeId, address(token2), address(token), 1e18); + bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token2), + tokenOut: address(token), + amountIn: 1e18 + }); } function test_getAmountOut_whenTokenOutNotInExchange_shouldRevert() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); vm.expectRevert("tokenIn and tokenOut must match exchange"); - bancorExchangeProvider.getAmountOut(exchangeId, address(token), address(token2), 1e18); + bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(token2), + amountIn: 1e18 + }); } - function test_getAmountOut_whenTokenInEqualsTokenOut_itReverts() public { + function test_getAmountOut_whenTokenInEqualTokenOut_shouldRevert() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); vm.expectRevert("tokenIn and tokenOut must match exchange"); - bancorExchangeProvider.getAmountOut(exchangeId, address(token), address(token), 1e18); + bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(token), + amountIn: 1e18 + }); + } + + function test_getAmountOut_whenTokenInIsReserveAssetAndTokenSupplyIsZero_shouldRevert() public { + poolExchange1.tokenSupply = 0; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("ERR_INVALID_SUPPLY"); + bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: 1e18 + }); + } + + 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({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: 0 + }); + assertEq(amountOut, 0); + } + + function test_getAmountOut_whenTokenInIsReserveAssetAndReserveRatioIs100Percent_shouldReturnCorrectAmount() public { + poolExchange1.reserveRatio = 1e8; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountIn = 1e18; + // formula: amountOut = tokenSupply * amountIn / reserveBalance + // calculation: 300_000 * 1 / 60_000 = 5 + uint256 expectedAmountOut = 5e18; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); + 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({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: 0 + }); + assertEq(amountOut, 0); + } + + function test_getAmountOut_whenTokenInIsTokenAndAmountIsSupply_shouldReturnReserveBalanceMinusExitContribution() + public + { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + 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 + uint256 expectedAmountOut = 198000000000000000; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + assertEq(amountOut, expectedAmountOut); } function test_getAmountOut_whenTokenInIsReserveAsset_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - // formula: amountOut = Supply * ( (1 + amountIn/reserveBalance)^collateralRatio - 1) - // calculation: 300_000 * ( (1 + 1/60_000)^0.2 - 1) ≈ 0.999993333399999222 - uint256 expectedAmountIn = 999993333399999222; - uint256 amountIn = bancorExchangeProvider.getAmountOut(exchangeId, address(reserveToken), address(token), 1e18); - assertEq(amountIn, expectedAmountIn); + // formula: amountOut = tokenSupply * ((1 + amountIn / reserveBalance) ^ reserveRatio - 1) + // calculation: 300_000 * ((1 + 1 / 60_000) ^ 0.2 - 1) ≈ 0.999993333399999222 + uint256 expectedAmountOut = 999993333399999222; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: 1e18 + }); + assertEq(amountOut, expectedAmountOut); } function test_getAmountOut_whenTokenInIsToken_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - // formula: amountOut = reserveBalance * ((1 + (amountIn * exitContribution)/tokenSupply)^(1/collateralRatio) -1) - // calculation: 60_000 * ((1 + (1 * (1-0.01))/300_000)^(1/0.2) -1) ≈ 0.990006534021562235 - uint256 expectedAmountIn = 989993466021562164; - uint256 amountIn = bancorExchangeProvider.getAmountOut(exchangeId, address(token), address(reserveToken), 1e18); - assertEq(amountIn, expectedAmountIn); + // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio)) + // formula: amountOut = ---------------------------------------------------------------------------------- * (1 - e) + // formula: = (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio) + + // calculation: ((60_000 *(-1+(300_000/(300_000-1))^5) ) / (300_000/(300_000-1))^5)*0.99 = 0.989993400021999963 + // 1 wei difference due to precision loss + uint256 expectedAmountOut = 989993400021999962; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: 1e18 + }); + assertEq(amountOut, expectedAmountOut); + } + + function test_getAmountOut_whenTokenInIsReserveAssetAndAmountOutIsSmall_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountIn = 1e12; // 0.000001 reserve token + // formula: amountOut = tokenSupply * ((1 + amountIn / reserveBalance) ^ reserveRatio - 1) + // calculation: 300_000 * ((1 + 0.000001 / 60_000) ^ 0.2 - 1) ≈ 0.00000099999999999333 + uint256 expectedAmountOut = 999999999993; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); + assertEq(amountOut, expectedAmountOut); + } + + 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) + + // calculation: ((60_000 *(-1+(300_000/(300_000-0.000001))^5) )/(300_000/(300_000-0.000001))^5)*0.99 ≈ 0.0000009899999999934 + uint256 expectedAmountOut = 989999999993; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + assertEq(amountOut, expectedAmountOut); + } + + function test_getAmountOut_whenTokenInIsReserveAssetAndAmountInIsLarge_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountIn = 1000000e18; // 1,000,000 reserve tokens + // formula: amountOut = tokenSupply * ((1 + amountIn / reserveBalance) ^ reserveRatio - 1) + // calculation: 300_000 * ((1 + 1_000_000 / 60_000) ^ 0.2 - 1) ≈ 232785.231205449318288038 + uint256 expectedAmountOut = 232785231205449318288038; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); + assertEq(amountOut, expectedAmountOut); + } + + 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) + + // 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; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + + // we allow up to 1% difference due to precision loss + assertApproxEqRel(amountOut, expectedAmountOut, 1e18 * 0.01); + } + + function test_getAmountOut_whenTokenInIsTokenAndExitContributionIsNonZero_shouldReturnCorrectAmount( + uint256 amountIn + ) public { + // Set exit contribution to 1% (1e6 out of 1e8) for exchange 1 and 0 for exchange 2 + // all other parameters are the same + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + bancorExchangeProvider.setExitContribution(exchangeId, 1e6); + bytes32 exchangeId2 = bancorExchangeProvider.createExchange(poolExchange2); + bancorExchangeProvider.setExitContribution(exchangeId2, 0); + + amountIn = bound(amountIn, 100, 299_000 * 1e18); + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + uint256 amountOut2 = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId2, + tokenIn: address(token2), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + assertEq(amountOut, (amountOut2 * 99) / 100); + } + + function test_getAmountOut_whenDifferentTokenDecimals_shouldReturnCorrectAmount() public { + // Create new tokens with different decimals + ERC20 reserveToken6 = new ERC20("Reserve6", "RSV6"); + ERC20 stableToken18 = new ERC20("Stable18", "STB18"); + + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(stableToken18)), + abi.encode(true) + ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken6)), + abi.encode(true) + ); + + // Mock decimals for these tokens + vm.mockCall(address(reserveToken6), abi.encodeWithSelector(reserveToken6.decimals.selector), abi.encode(6)); + vm.mockCall(address(stableToken18), abi.encodeWithSelector(stableToken18.decimals.selector), abi.encode(18)); + + IBancorExchangeProvider.PoolExchange memory newPoolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken6), + tokenAddress: address(stableToken18), + tokenSupply: 100_000 * 1e18, // 100,000 + reserveBalance: 50_000 * 1e18, // 50,000 + reserveRatio: 1e8 * 0.5, // 50% + exitContribution: 0 + }); + + bytes32 newExchangeId = bancorExchangeProvider.createExchange(newPoolExchange); + + uint256 amountIn = 1000000; // 1 ReserveToken in 6 decimals + + // formula: amountOut = tokenSupply * (-1 + (1 + (amountIn/reserveBalance))^reserveRatio) + // calculation: 100_000 * (-1 + (1+ (1 / 50_000))^0.5) ≈ 0.999995000049999375 in 18 decimals + uint256 expectedAmountOut = 999995000049999375; + + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: newExchangeId, + tokenIn: address(reserveToken6), + tokenOut: address(stableToken18), + amountIn: amountIn + }); + assertEq(amountOut, expectedAmountOut); + + uint256 reversedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: newExchangeId, + tokenIn: address(reserveToken6), + tokenOut: address(stableToken18), + amountOut: amountOut + }); + // we allow a 1 wei difference due to precision loss + assertApproxEqAbs(amountIn, reversedAmountIn, 1); + } + + function test_getAmountOut_whenTokenInIsReserveAsset_fuzz(uint256 amountIn) public { + // these values are closed to the ones in the real exchange will be initialized with + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: 7_000_000_000 * 1e18, + reserveBalance: 200_000 * 1e18, + reserveRatio: uint32(28571428), + exitContribution: 1e7 + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + // amountIn range between 1 and 10_000_000 tokens + amountIn = bound(amountIn, 1e18, 10_000_000 * 1e18); + + uint256 amountOut = bancorExchangeProvider.getAmountOut( + exchangeId, + address(reserveToken), + address(token), + amountIn + ); + + // Basic sanity checks + assertTrue(0 < amountOut, "Amount out should be positive"); + + // Verify the reverse swap + uint256 reversedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + // we allow up to 10 wei due to precision loss + assertApproxEqAbs(reversedAmountIn, amountIn, 10, "Reversed swap should approximately equal original amount in"); + } + + function test_getAmountOut_whenTokenInIsToken_fuzz(uint256 amountIn) public { + // these values are closed to the ones in the real exchange will be initialized with + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: 7_000_000_000 * 1e18, + reserveBalance: 200_000 * 1e18, + reserveRatio: uint32(28571428), + exitContribution: 1e7 + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + // amountIn range between 10_000wei and 3_500_000_000tokens + amountIn = bound(amountIn, 1e18, (poolExchange.tokenSupply * 5) / 10); + + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + + // Basic sanity checks + assertTrue(amountOut > 0, "Amount out should be positive"); + + uint256 reversedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + // we allow up to 0.1% difference due to precision loss + assertApproxEqRel(reversedAmountIn, amountIn, 1e18 * 0.001); + } + + function test_getAmountOut_whenTokenInIsReserveAsset_fullFuzz( + uint256 amountIn, + uint256 reserveBalance, + uint256 tokenSupply, + uint256 reserveRatio + ) public { + // tokenSupply range between 100 tokens and 10_000_000 tokens + tokenSupply = bound(tokenSupply, 100e18, 10_000_000 * 1e18); + // reserveBalance range between 100 tokens and 10_000_000 tokens + reserveBalance = bound(reserveBalance, 100e18, 10_000_000 * 1e18); + // reserveRatio range between 1% and 100% + reserveRatio = bound(reserveRatio, 1e6, 1e8); + + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: tokenSupply, + reserveBalance: reserveBalance, + reserveRatio: uint32(reserveRatio), + exitContribution: 0 // no exit contribution because reserveToken in + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + // amountIn range between 0.0001 tokens and 1_000_000 tokens + amountIn = bound(amountIn, 0.0001e18, 1_000_000 * 1e18); + + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); + + // Basic sanity checks + assertTrue(amountOut > 0, "Amount out should be positive"); + + uint256 reversedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + + // we allow up to 0.01% difference due to precision loss + assertApproxEqRel(reversedAmountIn, amountIn, 1e18 * 0.0001); + } + + function test_getAmountOut_whenTokenInIsToken_fullFuzz( + uint256 amountIn, + uint256 reserveBalance, + uint256 tokenSupply, + uint256 reserveRatio, + uint256 exitContribution + ) public { + // reserveBalance range between 100 tokens and 10_000_000 tokens + reserveBalance = bound(reserveBalance, 100e18, 10_000_000 * 1e18); + // tokenSupply range between 100 tokens and 100_000_000 tokens + tokenSupply = bound(tokenSupply, 100e18, 10_000_000 * 1e18); + // reserveRatio range between 5% and 100% + reserveRatio = bound(reserveRatio, 5e6, 1e8); + // exitContribution range between 0% and 20% + exitContribution = bound(exitContribution, 0, 2e7); + + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: tokenSupply, + reserveBalance: reserveBalance, + reserveRatio: uint32(reserveRatio), + exitContribution: uint32(exitContribution) + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + // amountIn range between 0.0001 tokens and 80% of tokenSupply + // if we would allow 100% of the tokenSupply, the precision loss can get higher + amountIn = bound(amountIn, 0.0001e18, (tokenSupply * 8) / 10); + + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + // Basic sanity checks + assertTrue(amountIn > 0, "Amount in should be positive"); + + uint256 reversedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + // we allow up to 1% difference due to precision loss + assertApproxEqRel(reversedAmountIn, amountIn, 1e18 * 0.01); } } @@ -512,6 +1490,29 @@ contract BancorExchangeProviderTest_currentPrice is BancorExchangeProviderTest { 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); + // tokenSupply range between 1 token and 10_000_000 tokens + tokenSupply = bound(tokenSupply, 1e18, 10_000_000 * 1e18); + // reserveRatio range between 1% and 100% + reserveRatio = bound(reserveRatio, 1e6, 1e8); + + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: tokenSupply, + reserveBalance: reserveBalance, + reserveRatio: uint32(reserveRatio), + exitContribution: 0 + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + uint256 price = bancorExchangeProvider.currentPrice(exchangeId); + assertTrue(0 < price, "Price should be positive"); + } } contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { From d45a22ea951623f7ead901089d1328d0140ab81f Mon Sep 17 00:00:00 2001 From: philbow61 Date: Fri, 18 Oct 2024 16:05:32 +0200 Subject: [PATCH 2/7] chore: merge latest base branch --- .github/workflows/echidna.yaml | 2 +- .github/workflows/lint_test.yaml | 50 ++-- .prettierrc.yml | 35 +-- .solhint.json | 30 +-- .../goodDollar/BancorExchangeProvider.sol | 255 ++++++++---------- contracts/goodDollar/BancorFormula.sol | 7 + .../goodDollar/GoodDollarExchangeProvider.sol | 102 +++---- .../GoodDollarExpansionController.sol | 121 ++++----- .../goodDollar/interfaces/IGoodProtocol.sol | 1 + .../interfaces/IBancorExchangeProvider.sol | 76 ++++-- contracts/interfaces/IBiPoolManager.sol | 3 +- contracts/interfaces/IBroker.sol | 144 +++++----- contracts/interfaces/IExchangeProvider.sol | 2 +- .../IGoodDollarExchangeProvider.sol | 54 ++-- .../IGoodDollarExpansionController.sol | 65 +++-- contracts/swap/Broker.sol | 104 ++----- package.json | 1 + test/fork/BaseForkTest.sol | 1 - .../protocol/GoodDollarIntegration.t.sol | 4 +- .../goodDollar/BancorExchangeProvider.t.sol | 44 ++- .../GoodDollarExchangeProvider.t.sol | 163 ++++++++++- .../GoodDollarExpansionController.t.sol | 47 ++-- test/unit/swap/Broker.t.sol | 138 ++++++++-- 23 files changed, 834 insertions(+), 615 deletions(-) diff --git a/.github/workflows/echidna.yaml b/.github/workflows/echidna.yaml index a79c75bf..f73d4b66 100644 --- a/.github/workflows/echidna.yaml +++ b/.github/workflows/echidna.yaml @@ -56,7 +56,7 @@ jobs: "test/integration/**/*" \ "test/unit/**/*" \ "test/utils/**/*" \ - "script/**/" + "contracts/**/*" - name: "Run Echidna" uses: crytic/echidna-action@v2 diff --git a/.github/workflows/lint_test.yaml b/.github/workflows/lint_test.yaml index 3fd28969..7a9a9705 100644 --- a/.github/workflows/lint_test.yaml +++ b/.github/workflows/lint_test.yaml @@ -1,55 +1,59 @@ -name: "CI" +name: CI env: - FOUNDRY_PROFILE: "ci" + FOUNDRY_PROFILE: ci + ALFAJORES_RPC_URL: ${{secrets.ALFAJORES_RPC_URL}} + CELO_MAINNET_RPC_URL: ${{secrets.CELO_MAINNET_RPC_URL}} on: workflow_dispatch: pull_request: push: branches: - - "main" - - "develop" + - main + - develop + +permissions: read-all jobs: lint_and_test: name: Lint & Test - runs-on: "ubuntu-latest" + runs-on: ubuntu-latest steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" + - name: Check out the repo + uses: actions/checkout@v3 with: - submodules: "recursive" + submodules: recursive - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 - - name: "Install Node.js" - uses: "actions/setup-node@v3" + - name: Install Node.js + uses: actions/setup-node@v3 with: - cache: "yarn" + cache: yarn node-version: "20" - - name: "Install the Node.js dependencies" - run: "yarn install --immutable" + - name: Install the Node.js dependencies + run: yarn install --immutable - - name: "Lint the contracts" - run: "yarn lint" + - name: Lint the contracts + run: yarn lint - - name: "Add lint summary" + - name: Add lint summary run: | echo "## Lint" >> $GITHUB_STEP_SUMMARY echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - - name: "Show the Foundry config" - run: "forge config" + - name: Show the Foundry config + run: forge config - - name: "Run the tests" - run: "forge test" + - name: Run the tests + run: forge test - - name: "Check contract sizes" - run: "yarn run check-contract-sizes" + - name: Check contract sizes + run: yarn run check-contract-sizes - - name: "Add test summary" + - name: Add test summary run: | echo "## Tests" >> $GITHUB_STEP_SUMMARY diff --git a/.prettierrc.yml b/.prettierrc.yml index 028dcf34..699e7964 100644 --- a/.prettierrc.yml +++ b/.prettierrc.yml @@ -9,10 +9,28 @@ trailingComma: all plugins: [prettier-plugin-solidity] overrides: + # General Config - files: ["*.sol"] options: compiler: 0.5.17 - - files: [contracts/interfaces/*.sol] + - files: [test/**/*.sol] + options: + compiler: "" + + # File-specific Config + - files: + [ + contracts/common/IERC20MintableBurnable.sol, + contracts/common/SafeERC20MintableBurnable.sol, + contracts/goodDollar/**/*.sol, + contracts/governance/**/*.sol, + contracts/interfaces/*.sol, + contracts/libraries/TradingLimits.sol, + contracts/oracles/Chainlink*.sol, + contracts/swap/Broker.sol, + contracts/tokens/patched/*.sol, + contracts/tokens/StableTokenV2.sol, + ] options: compiler: 0.8.18 - files: @@ -21,18 +39,3 @@ overrides: - contracts/interfaces/IExchange.sol options: compiler: 0.5.17 - - files: [contracts/tokens/patched/*.sol] - options: - compiler: 0.8.18 - - files: [contracts/tokens/StableTokenV2.sol] - options: - compiler: 0.8.18 - - files: [contracts/governance/**/*.sol] - options: - compiler: 0.8.18 - - files: [test/**/*.sol] - options: - compiler: "" - - files: [contracts/oracles/Chainlink*.sol] - options: - compiler: 0.8.18 diff --git a/.solhint.json b/.solhint.json index 4d94dc31..6b11569e 100644 --- a/.solhint.json +++ b/.solhint.json @@ -2,31 +2,17 @@ "extends": "solhint:recommended", "plugins": ["prettier"], "rules": { - "no-global-import": "off", - "no-console": "off", "code-complexity": ["error", 8], "compiler-version": ["error", ">=0.5.13"], - "func-visibility": [ - "error", - { - "ignoreConstructors": true - } - ], - "max-line-length": ["error", 121], - "not-rely-on-time": "off", + "func-visibility": ["error", { "ignoreConstructors": true }], "function-max-lines": ["error", 120], + "gas-custom-errors": "off", + "max-line-length": ["error", 121], + "no-console": "off", "no-empty-blocks": "off", - "prettier/prettier": [ - "error", - { - "endOfLine": "auto" - } - ], - "reason-string": [ - "warn", - { - "maxLength": 64 - } - ] + "no-global-import": "off", + "not-rely-on-time": "off", + "prettier/prettier": ["error", { "endOfLine": "auto" }], + "reason-string": ["warn", { "maxLength": 64 }] } } diff --git a/contracts/goodDollar/BancorExchangeProvider.sol b/contracts/goodDollar/BancorExchangeProvider.sol index fc99a84f..d607a947 100644 --- a/contracts/goodDollar/BancorExchangeProvider.sol +++ b/contracts/goodDollar/BancorExchangeProvider.sol @@ -16,7 +16,9 @@ import { UD60x18, unwrap, wrap } from "prb/math/UD60x18.sol"; * @notice Provides exchange functionality for Bancor pools. */ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, BancorFormula, OwnableUpgradeable { + /* ========================================================= */ /* ==================== State Variables ==================== */ + /* ========================================================= */ // Address of the broker contract. address public broker; @@ -29,17 +31,16 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B mapping(bytes32 => PoolExchange) public exchanges; bytes32[] public exchangeIds; - // Token precision multiplier used to normalize values to the - // same precision when calculating amounts. + // Token precision multiplier used to normalize values to the same precision when calculating amounts. mapping(address => uint256) public tokenPrecisionMultipliers; + /* ===================================================== */ /* ==================== Constructor ==================== */ + /* ===================================================== */ /** - * @dev Should be called with disable=true in deployments when - * it's accessed through a Proxy. - * Call this with disable=false during testing, when used - * without a proxy. + * @dev Should be called with disable=true in deployments when it's accessed through a Proxy. + * Call this with disable=false during testing, when used without a proxy. * @param disable Set to true to run `_disableInitializers()` inherited from * openzeppelin-contracts-upgradeable/Initializable.sol */ @@ -49,11 +50,7 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B } } - /** - * @notice Allows the contract to be upgradable via the proxy. - * @param _broker The address of the broker contract. - * @param _reserve The address of the reserve contract. - */ + /// @inheritdoc IBancorExchangeProvider function initialize(address _broker, address _reserve) public initializer { _initialize(_broker, _reserve); } @@ -66,7 +63,9 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B setReserve(_reserve); } + /* =================================================== */ /* ==================== Modifiers ==================== */ + /* =================================================== */ modifier onlyBroker() { require(msg.sender == broker, "Caller is not the Broker"); @@ -82,28 +81,24 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B _; } + /* ======================================================== */ /* ==================== View Functions ==================== */ + /* ======================================================== */ - /** - * @notice Get a PoolExchange from storage. - * @param exchangeId the exchange id - */ + /// @inheritdoc IBancorExchangeProvider function getPoolExchange(bytes32 exchangeId) public view returns (PoolExchange memory exchange) { exchange = exchanges[exchangeId]; - require(exchange.tokenAddress != address(0), "An exchange with the specified id does not exist"); + require(exchange.tokenAddress != address(0), "Exchange does not exist"); return exchange; } - /** - * @notice Get all exchange IDs. - * @return exchangeIds List of the exchangeIds. - */ + /// @inheritdoc IBancorExchangeProvider function getExchangeIds() external view returns (bytes32[] memory) { return exchangeIds; } /** - * @notice Get all exchanges (used by interfaces) + * @inheritdoc IExchangeProvider * @dev We don't expect the number of exchanges to grow to * astronomical values so this is safe gas-wise as is. */ @@ -118,14 +113,7 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B } } - /** - * @notice Calculate amountOut of tokenOut received for a given amountIn of tokenIn - * @param exchangeId The id of the exchange i.e PoolExchange to use - * @param tokenIn The token to be sold - * @param tokenOut The token to be bought - * @param amountIn The amount of tokenIn to be sold - * @return amountOut The amount of tokenOut to be bought - */ + /// @inheritdoc IExchangeProvider function getAmountOut( bytes32 exchangeId, address tokenIn, @@ -134,19 +122,12 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B ) external view virtual returns (uint256 amountOut) { PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledAmountIn = amountIn * tokenPrecisionMultipliers[tokenIn]; - uint256 scaledAmountOut = _getAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); + uint256 scaledAmountOut = _getScaledAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); amountOut = scaledAmountOut / tokenPrecisionMultipliers[tokenOut]; return amountOut; } - /** - * @notice Calculate amountIn of tokenIn for a given amountOut of tokenOut - * @param exchangeId The id of the exchange i.e PoolExchange to use - * @param tokenIn The token to be sold - * @param tokenOut The token to be bought - * @param amountOut The amount of tokenOut to be bought - * @return amountIn The amount of tokenIn to be sold - */ + /// @inheritdoc IExchangeProvider function getAmountIn( bytes32 exchangeId, address tokenIn, @@ -155,16 +136,12 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B ) external view virtual returns (uint256 amountIn) { PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledAmountOut = amountOut * tokenPrecisionMultipliers[tokenOut]; - uint256 scaledAmountIn = _getAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); + uint256 scaledAmountIn = _getScaledAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); amountIn = scaledAmountIn / tokenPrecisionMultipliers[tokenIn]; return amountIn; } - /** - * @notice Get the current price of the pool. - * @param exchangeId The id of the pool to get the price for. - * @return price The current continous price of the pool. - */ + /// @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"); @@ -175,96 +152,43 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B return price; } + /* ============================================================ */ /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ - /** - * @notice Sets the address of the broker contract. - * @param _broker The new address of the broker contract. - */ + /// @inheritdoc IBancorExchangeProvider function setBroker(address _broker) public onlyOwner { require(_broker != address(0), "Broker address must be set"); broker = _broker; emit BrokerUpdated(_broker); } - /** - * @notice Sets the address of the reserve contract. - * @param _reserve The new address of the reserve contract. - */ + /// @inheritdoc IBancorExchangeProvider function setReserve(address _reserve) public onlyOwner { require(address(_reserve) != address(0), "Reserve address must be set"); reserve = IReserve(_reserve); emit ReserveUpdated(address(_reserve)); } - /** - * @notice Sets the exit contribution for a pool. - * @param exchangeId The id of the pool. - * @param exitContribution The exit contribution. - */ - function setExitContribution(bytes32 exchangeId, uint32 exitContribution) external onlyOwner { - require(exchanges[exchangeId].reserveAsset != address(0), "Exchange does not exist"); - require(exitContribution <= MAX_WEIGHT, "Invalid exit contribution"); - - PoolExchange storage exchange = exchanges[exchangeId]; - exchange.exitContribution = exitContribution; - emit ExitContributionSet(exchangeId, exitContribution); + /// @inheritdoc IBancorExchangeProvider + function setExitContribution(bytes32 exchangeId, uint32 exitContribution) external virtual onlyOwner { + return _setExitContribution(exchangeId, exitContribution); } - /** - * @notice Creates a new exchange using the given parameters. - * @param _exchange the PoolExchange to create. - * @return exchangeId The id of the newly created exchange. - */ - function createExchange(PoolExchange calldata _exchange) external onlyOwner returns (bytes32 exchangeId) { - PoolExchange memory exchange = _exchange; - validate(exchange); - - exchangeId = keccak256( - abi.encodePacked(IERC20(exchange.reserveAsset).symbol(), IERC20(exchange.tokenAddress).symbol()) - ); - require(exchanges[exchangeId].reserveAsset == address(0), "Exchange already exists"); - - uint256 reserveAssetDecimals = IERC20(exchange.reserveAsset).decimals(); - uint256 tokenDecimals = IERC20(exchange.tokenAddress).decimals(); - require(reserveAssetDecimals <= 18, "reserve token decimals must be <= 18"); - require(tokenDecimals <= 18, "token decimals must be <= 18"); - - tokenPrecisionMultipliers[exchange.reserveAsset] = 10 ** (18 - uint256(reserveAssetDecimals)); - tokenPrecisionMultipliers[exchange.tokenAddress] = 10 ** (18 - uint256(tokenDecimals)); - - exchanges[exchangeId] = exchange; - exchangeIds.push(exchangeId); - emit ExchangeCreated(exchangeId, exchange.reserveAsset, exchange.tokenAddress); + /// @inheritdoc IBancorExchangeProvider + function createExchange(PoolExchange calldata _exchange) external virtual onlyOwner returns (bytes32 exchangeId) { + return _createExchange(_exchange); } - /** - * @notice Destroys a exchange with the given parameters if it exists. - * @param exchangeId the id of the exchange to destroy - * @param exchangeIdIndex The index of the exchangeId in the ids array - * @return destroyed A boolean indicating whether or not the exchange was successfully destroyed. - */ - function destroyExchange(bytes32 exchangeId, uint256 exchangeIdIndex) external onlyOwner returns (bool destroyed) { - require(exchangeIdIndex < exchangeIds.length, "exchangeIdIndex not in range"); - require(exchangeIds[exchangeIdIndex] == exchangeId, "exchangeId at index doesn't match"); - PoolExchange memory exchange = exchanges[exchangeId]; - - delete exchanges[exchangeId]; - exchangeIds[exchangeIdIndex] = exchangeIds[exchangeIds.length - 1]; - exchangeIds.pop(); - destroyed = true; - - emit ExchangeDestroyed(exchangeId, exchange.reserveAsset, exchange.tokenAddress); + /// @inheritdoc IBancorExchangeProvider + function destroyExchange( + bytes32 exchangeId, + uint256 exchangeIdIndex + ) external virtual onlyOwner returns (bool destroyed) { + return _destroyExchange(exchangeId, exchangeIdIndex); } - /** - * @notice Execute a token swap with fixed amountIn - * @param exchangeId The id of exchange, i.e. PoolExchange to use - * @param tokenIn The token to be sold - * @param tokenOut The token to be bought - * @param amountIn The amount of tokenIn to be sold - * @return amountOut The amount of tokenOut to be bought - */ + /// @inheritdoc IExchangeProvider function swapIn( bytes32 exchangeId, address tokenIn, @@ -273,21 +197,14 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B ) public virtual onlyBroker returns (uint256 amountOut) { PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledAmountIn = amountIn * tokenPrecisionMultipliers[tokenIn]; - uint256 scaledAmountOut = _getAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); + uint256 scaledAmountOut = _getScaledAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); executeSwap(exchangeId, tokenIn, scaledAmountIn, scaledAmountOut); amountOut = scaledAmountOut / tokenPrecisionMultipliers[tokenOut]; return amountOut; } - /** - * @notice Execute a token swap with fixed amountOut - * @param exchangeId The id of exchange, i.e. PoolExchange to use - * @param tokenIn The token to be sold - * @param tokenOut The token to be bought - * @param amountOut The amount of tokenOut to be bought - * @return amountIn The amount of tokenIn to be sold - */ + /// @inheritdoc IExchangeProvider function swapOut( bytes32 exchangeId, address tokenIn, @@ -296,22 +213,68 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B ) public virtual onlyBroker returns (uint256 amountIn) { PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledAmountOut = amountOut * tokenPrecisionMultipliers[tokenOut]; - uint256 scaledAmountIn = _getAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); + uint256 scaledAmountIn = _getScaledAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); executeSwap(exchangeId, tokenIn, scaledAmountIn, scaledAmountOut); amountIn = scaledAmountIn / tokenPrecisionMultipliers[tokenIn]; return amountIn; } + /* =========================================================== */ /* ==================== Private Functions ==================== */ + /* =========================================================== */ + + function _createExchange(PoolExchange calldata _exchange) internal returns (bytes32 exchangeId) { + PoolExchange memory exchange = _exchange; + validateExchange(exchange); + + // slither-disable-next-line encode-packed-collision + exchangeId = keccak256( + abi.encodePacked(IERC20(exchange.reserveAsset).symbol(), IERC20(exchange.tokenAddress).symbol()) + ); + require(exchanges[exchangeId].reserveAsset == address(0), "Exchange already exists"); + + uint256 reserveAssetDecimals = IERC20(exchange.reserveAsset).decimals(); + uint256 tokenDecimals = IERC20(exchange.tokenAddress).decimals(); + require(reserveAssetDecimals <= 18, "Reserve asset decimals must be <= 18"); + require(tokenDecimals <= 18, "Token decimals must be <= 18"); + + tokenPrecisionMultipliers[exchange.reserveAsset] = 10 ** (18 - uint256(reserveAssetDecimals)); + tokenPrecisionMultipliers[exchange.tokenAddress] = 10 ** (18 - uint256(tokenDecimals)); + + exchanges[exchangeId] = exchange; + exchangeIds.push(exchangeId); + emit ExchangeCreated(exchangeId, exchange.reserveAsset, exchange.tokenAddress); + } + + function _destroyExchange(bytes32 exchangeId, uint256 exchangeIdIndex) internal returns (bool destroyed) { + require(exchangeIdIndex < exchangeIds.length, "exchangeIdIndex not in range"); + require(exchangeIds[exchangeIdIndex] == exchangeId, "exchangeId at index doesn't match"); + PoolExchange memory exchange = exchanges[exchangeId]; + + delete exchanges[exchangeId]; + exchangeIds[exchangeIdIndex] = exchangeIds[exchangeIds.length - 1]; + exchangeIds.pop(); + destroyed = true; + + emit ExchangeDestroyed(exchangeId, exchange.reserveAsset, exchange.tokenAddress); + } + + 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"); + + PoolExchange storage exchange = exchanges[exchangeId]; + exchange.exitContribution = exitContribution; + emit ExitContributionSet(exchangeId, exitContribution); + } /** - * @notice Execute a swap against the in memory exchange and write - * the new exchange state to storage. - * @param exchangeId The id of the exchange + * @notice Execute a swap against the in-memory exchange and write the new exchange state to storage. + * @param exchangeId The ID of the pool * @param tokenIn The token to be sold - * @param scaledAmountIn The amount of tokenIn to be sold scaled to 18 decimals - * @param scaledAmountOut The amount of tokenOut to be bought scaled to 18 decimals + * @param scaledAmountIn The amount of tokenIn to be sold, scaled to 18 decimals + * @param scaledAmountOut The amount of tokenOut to be bought, scaled to 18 decimals */ function executeSwap(bytes32 exchangeId, address tokenIn, uint256 scaledAmountIn, uint256 scaledAmountOut) internal { PoolExchange memory exchange = getPoolExchange(exchangeId); @@ -327,14 +290,14 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B } /** - * @notice Calculate amountIn of tokenIn for a given amountOut of tokenOut - * @param exchange The exchange to operate on + * @notice Calculate the scaledAmountIn of tokenIn for a given scaledAmountOut of tokenOut + * @param exchange The pool exchange to operate on * @param tokenIn The token to be sold * @param tokenOut The token to be bought - * @param scaledAmountOut The amount of tokenOut to be bought scaled to 18 decimals - * @return scaledAmountIn The amount of tokenIn to be sold scaled to 18 decimals + * @param scaledAmountOut The amount of tokenOut to be bought, scaled to 18 decimals + * @return scaledAmountIn The amount of tokenIn to be sold, scaled to 18 decimals */ - function _getAmountIn( + function _getScaledAmountIn( PoolExchange memory exchange, address tokenIn, address tokenOut, @@ -350,14 +313,14 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B } /** - * @notice Calculate amountOut of tokenOut received for a given amountIn of tokenIn - * @param exchange The exchange to operate on + * @notice Calculate the scaledAmountOut of tokenOut received for a given scaledAmountIn of tokenIn + * @param exchange The pool exchange to operate on * @param tokenIn The token to be sold * @param tokenOut The token to be bought - * @param scaledAmountIn The amount of tokenIn to be sold scaled to 18 decimals - * @return scaledAmountOut The amount of tokenOut to be bought scaled to 18 decimals + * @param scaledAmountIn The amount of tokenIn to be sold, scaled to 18 decimals + * @return scaledAmountOut The amount of tokenOut to be bought, scaled to 18 decimals */ - function _getAmountOut( + function _getScaledAmountOut( PoolExchange memory exchange, address tokenIn, address tokenOut, @@ -383,20 +346,20 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B } /** - * @notice Valitates a PoolExchange's parameters and configuration + * @notice Validates a PoolExchange's parameters and configuration * @dev Reverts if not valid * @param exchange The PoolExchange to validate */ - function validate(PoolExchange memory exchange) internal view { + function validateExchange(PoolExchange memory exchange) internal view { require(exchange.reserveAsset != address(0), "Invalid reserve asset"); require( reserve.isCollateralAsset(exchange.reserveAsset), - "reserve asset must be a collateral registered with the reserve" + "Reserve asset must be a collateral registered with the reserve" ); require(exchange.tokenAddress != address(0), "Invalid token address"); - require(reserve.isStableAsset(exchange.tokenAddress), "token must be a stable registered with the reserve"); - require(exchange.reserveRatio > 1, "Invalid reserve ratio"); - require(exchange.reserveRatio <= MAX_WEIGHT, "Invalid reserve ratio"); - require(exchange.exitContribution <= MAX_WEIGHT, "Invalid exit contribution"); + require(reserve.isStableAsset(exchange.tokenAddress), "Token must be a stable registered with the reserve"); + 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"); } } diff --git a/contracts/goodDollar/BancorFormula.sol b/contracts/goodDollar/BancorFormula.sol index e083ae12..29eaa918 100644 --- a/contracts/goodDollar/BancorFormula.sol +++ b/contracts/goodDollar/BancorFormula.sol @@ -559,9 +559,13 @@ contract BancorFormula { * - The natural logarithm of the input is calculated by summing up the intermediate results above * - For example: log(250) = log(e^4 * e^1 * e^0.5 * 1.021692859) = 4 + 1 + 0.5 + log(1 + 0.021692859) */ + // We're choosing to trust Bancor's audited Math + // slither-disable-start divide-before-multiply function optimalLog(uint256 x) internal pure returns (uint256) { uint256 res = 0; + // slither false positive, y is initialized as z = y = ... + // slither-disable-next-line uninitialized-local uint256 y; uint256 z; uint256 w; @@ -634,6 +638,8 @@ contract BancorFormula { function optimalExp(uint256 x) internal pure returns (uint256) { uint256 res = 0; + // slither false positive, y is initialized as z = y = ... + // slither-disable-next-line uninitialized-local uint256 y; uint256 z; @@ -696,3 +702,4 @@ contract BancorFormula { return res; } } +// slither-disable-end divide-before-multiply diff --git a/contracts/goodDollar/GoodDollarExchangeProvider.sol b/contracts/goodDollar/GoodDollarExchangeProvider.sol index 1c19c1c6..e52cc103 100644 --- a/contracts/goodDollar/GoodDollarExchangeProvider.sol +++ b/contracts/goodDollar/GoodDollarExchangeProvider.sol @@ -14,7 +14,9 @@ import { UD60x18, unwrap, wrap } from "prb/math/UD60x18.sol"; * @notice Provides exchange functionality for the GoodDollar system. */ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchangeProvider, PausableUpgradeable { + /* ========================================================= */ /* ==================== State Variables ==================== */ + /* ========================================================= */ // Address of the Expansion Controller contract. IGoodDollarExpansionController public expansionController; @@ -23,25 +25,19 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan // solhint-disable-next-line var-name-mixedcase address public AVATAR; + /* ===================================================== */ /* ==================== Constructor ==================== */ + /* ===================================================== */ /** - * @dev Should be called with disable=true in deployments when - * it's accessed through a Proxy. - * Call this with disable=false during testing, when used - * without a proxy. + * @dev Should be called with disable=true in deployments when it's accessed through a Proxy. + * Call this with disable=false during testing, when used without a proxy. * @param disable Set to true to run `_disableInitializers()` inherited from * openzeppelin-contracts-upgradeable/Initializable.sol */ constructor(bool disable) BancorExchangeProvider(disable) {} - /** - * @notice Initializes the contract with the given parameters. - * @param _broker The address of the Broker contract. - * @param _reserve The address of the Reserve contract. - * @param _expansionController The address of the ExpansionController contract. - * @param _avatar The address of the GoodDollar DAO contract. - */ + /// @inheritdoc IGoodDollarExchangeProvider function initialize( address _broker, address _reserve, @@ -55,7 +51,9 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan setAvatar(_avatar); } + /* =================================================== */ /* ==================== Modifiers ==================== */ + /* =================================================== */ modifier onlyAvatar() { require(msg.sender == AVATAR, "Only Avatar can call this function"); @@ -67,22 +65,18 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan _; } + /* ============================================================ */ /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ - /** - * @notice Sets the address of the GoodDollar DAO contract. - * @param _avatar The address of the DAO contract. - */ + /// @inheritdoc IGoodDollarExchangeProvider function setAvatar(address _avatar) public onlyOwner { require(_avatar != address(0), "Avatar address must be set"); AVATAR = _avatar; emit AvatarUpdated(_avatar); } - /** - * @notice Sets the address of the Expansion Controller contract. - * @param _expansionController The address of the Expansion Controller contract. - */ + /// @inheritdoc IGoodDollarExchangeProvider function setExpansionController(address _expansionController) public onlyOwner { require(_expansionController != address(0), "ExpansionController address must be set"); expansionController = IGoodDollarExpansionController(_expansionController); @@ -90,13 +84,33 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan } /** - * @notice Execute a token swap with fixed amountIn - * @param exchangeId The id of exchange, i.e. PoolExchange to use - * @param tokenIn The token to be sold - * @param tokenOut The token to be bought - * @param amountIn The amount of tokenIn to be sold - * @return amountOut The amount of tokenOut to be bought + * @inheritdoc BancorExchangeProvider + * @dev Only callable by the GoodDollar DAO contract. + */ + function setExitContribution(bytes32 exchangeId, uint32 exitContribution) external override onlyAvatar { + return _setExitContribution(exchangeId, exitContribution); + } + + /** + * @inheritdoc BancorExchangeProvider + * @dev Only callable by the GoodDollar DAO contract. */ + function createExchange(PoolExchange calldata _exchange) external override onlyAvatar returns (bytes32 exchangeId) { + return _createExchange(_exchange); + } + + /** + * @inheritdoc BancorExchangeProvider + * @dev Only callable by the GoodDollar DAO contract. + */ + function destroyExchange( + bytes32 exchangeId, + uint256 exchangeIdIndex + ) external override onlyAvatar returns (bool destroyed) { + return _destroyExchange(exchangeId, exchangeIdIndex); + } + + /// @inheritdoc BancorExchangeProvider function swapIn( bytes32 exchangeId, address tokenIn, @@ -106,14 +120,7 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan amountOut = BancorExchangeProvider.swapIn(exchangeId, tokenIn, tokenOut, amountIn); } - /** - * @notice Execute a token swap with fixed amountOut - * @param exchangeId The id of exchange, i.e. PoolExchange to use - * @param tokenIn The token to be sold - * @param tokenOut The token to be bought - * @param amountOut The amount of tokenOut to be bought - * @return amountIn The amount of tokenIn to be sold - */ + /// @inheritdoc BancorExchangeProvider function swapOut( bytes32 exchangeId, address tokenIn, @@ -124,13 +131,10 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan } /** - * @notice Calculates the amount of tokens to be minted as a result of expansion. - * @dev Calculates the amount of tokens that need to be minted as a result of the expansion + * @inheritdoc IGoodDollarExchangeProvider + * @dev Calculates the amount of G$ tokens that need to be minted as a result of the expansion * while keeping the current price the same. * calculation: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio - * @param exchangeId The id of the pool to calculate expansion for. - * @param expansionScaler Scaler for calculating the new reserve ratio. - * @return amountToMint amount of tokens to be minted as a result of the expansion. */ function mintFromExpansion( bytes32 exchangeId, @@ -153,17 +157,15 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan amountToMint = scaledAmountToMint / tokenPrecisionMultipliers[exchange.tokenAddress]; emit ReserveRatioUpdated(exchangeId, newRatioUint); + return amountToMint; } /** - * @notice Calculates the amount of tokens to be minted as a result of collecting the reserve interest. - * @dev Calculates the amount of tokens that need to be minted as a result of the reserve interest + * @inheritdoc IGoodDollarExchangeProvider + * @dev Calculates the amount of G$ tokens that need to be minted as a result of the reserve interest * flowing into the reserve while keeping the current price the same. * calculation: amountToMint = reserveInterest * tokenSupply / reserveBalance - * @param exchangeId The id of the pool the reserve interest is added to. - * @param reserveInterest The amount of reserve tokens collected from interest. - * @return amountToMint amount of tokens to be minted as a result of the reserve interest. */ function mintFromInterest( bytes32 exchangeId, @@ -184,11 +186,9 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan } /** - * @notice Calculates the reserve ratio needed to mint the reward. - * @dev Calculates the new reserve ratio needed to mint the reward while keeping the current price the same. + * @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 - * @param exchangeId The id of the pool the reward is minted from. - * @param reward The amount of tokens to be minted as a reward. */ function updateRatioForReward(bytes32 exchangeId, uint256 reward) external onlyExpansionController whenNotPaused { PoolExchange memory exchange = getPoolExchange(exchangeId); @@ -212,16 +212,16 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan } /** - * @notice Pauses the contract. - * @dev Functions is only callable by the GoodDollar DAO contract. + * @inheritdoc IGoodDollarExchangeProvider + * @dev Only callable by the GoodDollar DAO contract. */ function pause() external virtual onlyAvatar { _pause(); } /** - * @notice Unpauses the contract. - * @dev Functions is only callable by the GoodDollar DAO contract. + * @inheritdoc IGoodDollarExchangeProvider + * @dev Only callable by the GoodDollar DAO contract. */ function unpause() external virtual onlyAvatar { _unpause(); diff --git a/contracts/goodDollar/GoodDollarExpansionController.sol b/contracts/goodDollar/GoodDollarExpansionController.sol index 2465e27a..300ab958 100644 --- a/contracts/goodDollar/GoodDollarExpansionController.sol +++ b/contracts/goodDollar/GoodDollarExpansionController.sol @@ -17,7 +17,9 @@ import { unwrap, wrap, powu } from "prb/math/UD60x18.sol"; * @notice Provides functionality to expand the supply of GoodDollars. */ contract GoodDollarExpansionController is IGoodDollarExpansionController, PausableUpgradeable, OwnableUpgradeable { + /* ========================================================= */ /* ==================== State Variables ==================== */ + /* ========================================================= */ // MAX_WEIGHT is the max rate that can be assigned to an exchange uint256 public constant MAX_WEIGHT = 1e18; @@ -32,19 +34,19 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab IGoodDollarExchangeProvider public goodDollarExchangeProvider; // Maps exchangeId to exchangeExpansionConfig - mapping(bytes32 => ExchangeExpansionConfig) public exchangeExpansionConfigs; + mapping(bytes32 exchangeId => ExchangeExpansionConfig) public exchangeExpansionConfigs; // Address of the GoodDollar DAO contract. // solhint-disable-next-line var-name-mixedcase address public AVATAR; + /* ===================================================== */ /* ==================== Constructor ==================== */ + /* ===================================================== */ /** - * @dev Should be called with disable=true in deployments when - * it's accessed through a Proxy. - * Call this with disable=false during testing, when used - * without a proxy. + * @dev Should be called with disable=true in deployments when it's accessed through a Proxy. + * Call this with disable=false during testing, when used without a proxy. * @param disable Set to true to run `_disableInitializers()` inherited from * openzeppelin-contracts-upgradeable/Initializable.sol */ @@ -54,13 +56,7 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab } } - /** - * @notice Initializes the contract with the given parameters. - * @param _goodDollarExchangeProvider The address of the GoodDollarExchangeProvider contract. - * @param _distributionHelper The address of the distribution helper contract. - * @param _reserve The address of the Reserve contract. - * @param _avatar The address of the GoodDollar DAO contract. - */ + /// @inheritdoc IGoodDollarExpansionController function initialize( address _goodDollarExchangeProvider, address _distributionHelper, @@ -71,78 +67,61 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab __Ownable_init(); setGoodDollarExchangeProvider(_goodDollarExchangeProvider); - setDistributionHelper(_distributionHelper); + _setDistributionHelper(_distributionHelper); setReserve(_reserve); setAvatar(_avatar); } + /* =================================================== */ /* ==================== Modifiers ==================== */ + /* =================================================== */ modifier onlyAvatar() { require(msg.sender == AVATAR, "Only Avatar can call this function"); _; } + /* ======================================================== */ /* ==================== View Functions ==================== */ + /* ======================================================== */ - /** - * @notice Returns the expansion config for the given exchange. - * @param exchangeId The id of the exchange to get the expansion config for. - * @return config The expansion config. - */ + /// @inheritdoc IGoodDollarExpansionController function getExpansionConfig(bytes32 exchangeId) public view returns (ExchangeExpansionConfig memory) { require(exchangeExpansionConfigs[exchangeId].expansionRate > 0, "Expansion config not set"); return exchangeExpansionConfigs[exchangeId]; } + /* ============================================================ */ /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ - /** - * @notice Sets the GoodDollarExchangeProvider address. - * @param _goodDollarExchangeProvider The address of the GoodDollarExchangeProvider contract. - */ + /// @inheritdoc IGoodDollarExpansionController function setGoodDollarExchangeProvider(address _goodDollarExchangeProvider) public onlyOwner { require(_goodDollarExchangeProvider != address(0), "GoodDollarExchangeProvider address must be set"); goodDollarExchangeProvider = IGoodDollarExchangeProvider(_goodDollarExchangeProvider); emit GoodDollarExchangeProviderUpdated(_goodDollarExchangeProvider); } - /** - * @notice Sets the distribution helper address. - * @param _distributionHelper The address of the distribution helper contract. - */ - function setDistributionHelper(address _distributionHelper) public onlyOwner { - require(_distributionHelper != address(0), "DistributionHelper address must be set"); - distributionHelper = IDistributionHelper(_distributionHelper); - emit DistributionHelperUpdated(_distributionHelper); + /// @inheritdoc IGoodDollarExpansionController + function setDistributionHelper(address _distributionHelper) public onlyAvatar { + return _setDistributionHelper(_distributionHelper); } - /** - * @notice Sets the reserve address. - * @param _reserve The address of the reserve contract. - */ + /// @inheritdoc IGoodDollarExpansionController function setReserve(address _reserve) public onlyOwner { require(_reserve != address(0), "Reserve address must be set"); reserve = _reserve; emit ReserveUpdated(_reserve); } - /** - * @notice Sets the AVATAR address. - * @param _avatar The address of the AVATAR contract. - */ + /// @inheritdoc IGoodDollarExpansionController function setAvatar(address _avatar) public onlyOwner { require(_avatar != address(0), "Avatar address must be set"); AVATAR = _avatar; emit AvatarUpdated(_avatar); } - /** - * @notice Sets the expansion config for the given exchange. - * @param exchangeId The id of the exchange to set the expansion config for. - * @param expansionRate The rate of expansion. - * @param expansionFrequency The frequency of expansion. - */ + /// @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 > 0, "Expansion rate must be greater than 0"); @@ -154,13 +133,9 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab emit ExpansionConfigSet(exchangeId, expansionRate, expansionFrequency); } - /** - * @notice Mints UBI for the given exchange from collecting reserve interest. - * @param exchangeId The id of the exchange to mint UBI for. - * @param reserveInterest The amount of reserve tokens collected from interest. - */ + /// @inheritdoc IGoodDollarExpansionController function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external { - require(reserveInterest > 0, "reserveInterest must be greater than 0"); + require(reserveInterest > 0, "Reserve interest must be greater than 0"); IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) .getPoolExchange(exchangeId); @@ -168,14 +143,13 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab require(IERC20(exchange.reserveAsset).transferFrom(msg.sender, reserve, reserveInterest), "Transfer failed"); IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountToMint); + + // Ignored, because contracts only interacts with trusted contracts and tokens + // slither-disable-next-line reentrancy-events emit InterestUBIMinted(exchangeId, amountToMint); } - /** - * @notice Mints UBI for the given exchange by comparing the reserve Balance of the contract to the virtual balance. - * @param exchangeId The id of the exchange to mint UBI for. - * @return amountMinted The amount of UBI tokens minted. - */ + /// @inheritdoc IGoodDollarExpansionController function mintUBIFromReserveBalance(bytes32 exchangeId) external returns (uint256 amountMinted) { IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) .getPoolExchange(exchangeId); @@ -186,15 +160,13 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab amountMinted = goodDollarExchangeProvider.mintFromInterest(exchangeId, additionalReserveBalance); 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, amountMinted); } } - /** - * @notice Mints UBI for the given exchange by calculating the expansion rate. - * @param exchangeId The id of the exchange to mint UBI for. - * @return amountMinted The amount of UBI tokens minted. - */ + /// @inheritdoc IGoodDollarExpansionController function mintUBIFromExpansion(bytes32 exchangeId) external returns (uint256 amountMinted) { ExchangeExpansionConfig memory config = getExpansionConfig(exchangeId); IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) @@ -204,7 +176,7 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab if (shouldExpand || config.lastExpansion == 0) { uint256 numberOfExpansions; - //special case for first expansion + // Special case for first expansion if (config.lastExpansion == 0) { numberOfExpansions = 1; } else { @@ -219,24 +191,35 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountMinted); distributionHelper.onDistribution(amountMinted); + + // Ignored, because contracts only interacts with trusted contracts and tokens + // slither-disable-next-line reentrancy-events emit ExpansionUBIMinted(exchangeId, amountMinted); } } - /** - * @notice Mints a reward of tokens for the given exchange. - * @param exchangeId The id of the exchange to mint reward. - * @param to The address of the recipient. - * @param amount The amount of tokens to mint. - */ - function mintRewardFromRR(bytes32 exchangeId, address to, uint256 amount) external onlyAvatar { - require(to != address(0), "Invalid to address"); + /// @inheritdoc IGoodDollarExpansionController + function mintRewardFromReserveRatio(bytes32 exchangeId, address to, uint256 amount) external onlyAvatar { + require(to != address(0), "Recipient address must be set"); require(amount > 0, "Amount must be greater than 0"); IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) .getPoolExchange(exchangeId); goodDollarExchangeProvider.updateRatioForReward(exchangeId, amount); IGoodDollar(exchange.tokenAddress).mint(to, amount); + + // Ignored, because contracts only interacts with trusted contracts and tokens + // slither-disable-next-line reentrancy-events emit RewardMinted(exchangeId, to, amount); } + + /* =========================================================== */ + /* ==================== Private Functions ==================== */ + /* =========================================================== */ + + function _setDistributionHelper(address _distributionHelper) internal { + require(_distributionHelper != address(0), "Distribution helper address must be set"); + distributionHelper = IDistributionHelper(_distributionHelper); + emit DistributionHelperUpdated(_distributionHelper); + } } diff --git a/contracts/goodDollar/interfaces/IGoodProtocol.sol b/contracts/goodDollar/interfaces/IGoodProtocol.sol index a7709f52..1c774079 100644 --- a/contracts/goodDollar/interfaces/IGoodProtocol.sol +++ b/contracts/goodDollar/interfaces/IGoodProtocol.sol @@ -15,6 +15,7 @@ interface IGoodDollar { function balanceOf(address account) external view returns (uint256); + // slither-disable-next-line erc721-interface function approve(address spender, uint256 amount) external returns (bool); } diff --git a/contracts/interfaces/IBancorExchangeProvider.sol b/contracts/interfaces/IBancorExchangeProvider.sol index ac2ab51c..9f491c97 100644 --- a/contracts/interfaces/IBancorExchangeProvider.sol +++ b/contracts/interfaces/IBancorExchangeProvider.sol @@ -12,7 +12,9 @@ interface IBancorExchangeProvider { uint32 exitContribution; } - /* ------- Events ------- */ + /* ========================================== */ + /* ================= Events ================= */ + /* ========================================== */ /** * @notice Emitted when the broker address is updated. @@ -27,16 +29,16 @@ interface IBancorExchangeProvider { event ReserveUpdated(address indexed newReserve); /** - * @notice Emitted when a new PoolExchange has been created. - * @param exchangeId The id of the new PoolExchange + * @notice Emitted when a new pool has been created. + * @param exchangeId The id of the new pool * @param reserveAsset The address of the reserve asset * @param tokenAddress The address of the token */ event ExchangeCreated(bytes32 indexed exchangeId, address indexed reserveAsset, address indexed tokenAddress); /** - * @notice Emitted when a PoolExchange has been destroyed. - * @param exchangeId The id of the PoolExchange + * @notice Emitted when a pool has been destroyed. + * @param exchangeId The id of the pool to destroy * @param reserveAsset The address of the reserve asset * @param tokenAddress The address of the token */ @@ -49,47 +51,71 @@ interface IBancorExchangeProvider { */ event ExitContributionSet(bytes32 indexed exchangeId, uint256 exitContribution); - /* ------- Functions ------- */ + /* ======================================================== */ + /* ==================== View Functions ==================== */ + /* ======================================================== */ + + /** + * @notice Allows the contract to be upgradable via the proxy. + * @param _broker The address of the broker contract. + * @param _reserve The address of the reserve contract. + */ + function initialize(address _broker, address _reserve) external; /** * @notice Retrieves the pool with the specified exchangeId. - * @param exchangeId The id of the pool to be retrieved. - * @return exchange The PoolExchange with that ID. + * @param exchangeId The ID of the pool to be retrieved. + * @return exchange The pool with that ID. */ function getPoolExchange(bytes32 exchangeId) external view returns (PoolExchange memory exchange); /** - * @notice Get all exchange IDs. - * @return exchangeIds List of the exchangeIds. + * @notice Gets all pool IDs. + * @return exchangeIds List of the pool IDs. */ function getExchangeIds() external view returns (bytes32[] memory exchangeIds); /** - * @notice Create a PoolExchange with the provided data. - * @param exchange The PoolExchange to be created. - * @return exchangeId The id of the exchange. + * @notice Gets the current price based of the Bancor formula + * @param exchangeId The ID of the pool to get the price for + * @return price The current continuous price of the pool */ - function createExchange(PoolExchange calldata exchange) external returns (bytes32 exchangeId); + function currentPrice(bytes32 exchangeId) external view returns (uint256 price); + /* ============================================================ */ + /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ /** - * @notice Delete a PoolExchange. - * @param exchangeId The PoolExchange to be created. - * @param exchangeIdIndex The index of the exchangeId in the exchangeIds array. - * @return destroyed - true on successful delition. + * @notice Sets the address of the broker contract. + * @param _broker The new address of the broker contract. */ - function destroyExchange(bytes32 exchangeId, uint256 exchangeIdIndex) external returns (bool destroyed); + function setBroker(address _broker) external; /** - * @notice Set the exit contribution for a given exchange - * @param exchangeId The id of the exchange + * @notice Sets the address of the reserve contract. + * @param _reserve The new address of the reserve contract. + */ + function setReserve(address _reserve) external; + + /** + * @notice Sets the exit contribution for a given pool + * @param exchangeId The ID of the pool * @param exitContribution The exit contribution to be set */ function setExitContribution(bytes32 exchangeId, uint32 exitContribution) external; /** - * @notice gets the current price based of the bancor formula - * @param exchangeId The id of the exchange to get the price for - * @return price the current continious price + * @notice Creates a new pool with the given parameters. + * @param exchange The pool to be created. + * @return exchangeId The ID of the new pool. + */ + function createExchange(PoolExchange calldata exchange) external returns (bytes32 exchangeId); + + /** + * @notice Destroys a pool with the given parameters if it exists. + * @param exchangeId The ID of the pool to be destroyed. + * @param exchangeIdIndex The index of the pool in the exchangeIds array. + * @return destroyed A boolean indicating whether or not the exchange was successfully destroyed. */ - function currentPrice(bytes32 exchangeId) external returns (uint256 price); + function destroyExchange(bytes32 exchangeId, uint256 exchangeIdIndex) external returns (bool destroyed); } diff --git a/contracts/interfaces/IBiPoolManager.sol b/contracts/interfaces/IBiPoolManager.sol index 0d17b9f8..56194d55 100644 --- a/contracts/interfaces/IBiPoolManager.sol +++ b/contracts/interfaces/IBiPoolManager.sol @@ -13,8 +13,7 @@ import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; /** * @title BiPool Manager interface - * @notice The two asset pool manager is responsible for - * managing the state of all two-asset virtual pools. + * @notice An exchange provider implementation managing the state of all two-asset virtual pools. */ interface IBiPoolManager { /** diff --git a/contracts/interfaces/IBroker.sol b/contracts/interfaces/IBroker.sol index 1df81930..00d8f913 100644 --- a/contracts/interfaces/IBroker.sol +++ b/contracts/interfaces/IBroker.sol @@ -38,76 +38,125 @@ interface IBroker { event TradingLimitConfigured(bytes32 exchangeId, address token, ITradingLimits.Config config); /** - * @notice Execute a token swap with fixed amountIn. + * @notice Allows the contract to be upgradable via the proxy. + * @param _exchangeProviders The addresses of the ExchangeProvider contracts. + * @param _reserves The address of the Reserve contract. + */ + function initialize(address[] calldata _exchangeProviders, address[] calldata _reserves) external; + + /** + * @notice Set the reserves for the exchange providers. + * @param _exchangeProviders The addresses of the ExchangeProvider contracts. + * @param _reserves The addresses of the Reserve contracts. + */ + function setReserves(address[] calldata _exchangeProviders, address[] calldata _reserves) external; + + /** + * @notice Add an exchange provider to the list of providers. + * @param exchangeProvider The address of the exchange provider to add. + * @param reserve The address of the reserve used by the exchange provider. + * @return index The index of the newly added specified exchange provider. + */ + function addExchangeProvider(address exchangeProvider, address reserve) external returns (uint256 index); + + /** + * @notice Remove an exchange provider from the list of providers. + * @param exchangeProvider The address of the exchange provider to remove. + * @param index The index of the exchange provider being removed. + */ + function removeExchangeProvider(address exchangeProvider, uint256 index) external; + + /** + * @notice Calculate amountIn of tokenIn needed for a given amountOut of tokenOut. * @param exchangeProvider the address of the exchange provider for the pair. * @param exchangeId The id of the exchange to use. * @param tokenIn The token to be sold. * @param tokenOut The token to be bought. - * @param amountIn The amount of tokenIn to be sold. - * @param amountOutMin Minimum amountOut to be received - controls slippage. - * @return amountOut The amount of tokenOut to be bought. + * @param amountOut The amount of tokenOut to be bought. + * @return amountIn The amount of tokenIn to be sold. */ - function swapIn( + function getAmountIn( address exchangeProvider, bytes32 exchangeId, address tokenIn, address tokenOut, - uint256 amountIn, - uint256 amountOutMin - ) external returns (uint256 amountOut); + uint256 amountOut + ) external view returns (uint256 amountIn); /** - * @notice Execute a token swap with fixed amountOut. + * @notice Calculate amountOut of tokenOut received for a given amountIn of tokenIn. * @param exchangeProvider the address of the exchange provider for the pair. * @param exchangeId The id of the exchange to use. * @param tokenIn The token to be sold. * @param tokenOut The token to be bought. - * @param amountOut The amount of tokenOut to be bought. - * @param amountInMax Maximum amount of tokenIn that can be traded. - * @return amountIn The amount of tokenIn to be sold. + * @param amountIn The amount of tokenIn to be sold. + * @return amountOut The amount of tokenOut to be bought. */ - function swapOut( + function getAmountOut( address exchangeProvider, bytes32 exchangeId, address tokenIn, address tokenOut, - uint256 amountOut, - uint256 amountInMax - ) external returns (uint256 amountIn); + uint256 amountIn + ) external view returns (uint256 amountOut); /** - * @notice Calculate amountOut of tokenOut received for a given amountIn of tokenIn. + * @notice Execute a token swap with fixed amountIn. * @param exchangeProvider the address of the exchange provider for the pair. * @param exchangeId The id of the exchange to use. * @param tokenIn The token to be sold. * @param tokenOut The token to be bought. * @param amountIn The amount of tokenIn to be sold. + * @param amountOutMin Minimum amountOut to be received - controls slippage. * @return amountOut The amount of tokenOut to be bought. */ - function getAmountOut( + function swapIn( address exchangeProvider, bytes32 exchangeId, address tokenIn, address tokenOut, - uint256 amountIn - ) external view returns (uint256 amountOut); + uint256 amountIn, + uint256 amountOutMin + ) external returns (uint256 amountOut); /** - * @notice Calculate amountIn of tokenIn needed for a given amountOut of tokenOut. + * @notice Execute a token swap with fixed amountOut. * @param exchangeProvider the address of the exchange provider for the pair. * @param exchangeId The id of the exchange to use. * @param tokenIn The token to be sold. * @param tokenOut The token to be bought. * @param amountOut The amount of tokenOut to be bought. + * @param amountInMax Maximum amount of tokenIn that can be traded. * @return amountIn The amount of tokenIn to be sold. */ - function getAmountIn( + function swapOut( address exchangeProvider, bytes32 exchangeId, address tokenIn, address tokenOut, - uint256 amountOut - ) external view returns (uint256 amountIn); + uint256 amountOut, + uint256 amountInMax + ) external returns (uint256 amountIn); + + /** + * @notice Permissionless way to burn stables from msg.sender directly. + * @param token The token getting burned. + * @param amount The amount of the token getting burned. + * @return True if transaction succeeds. + */ + function burnStableTokens(address token, uint256 amount) external returns (bool); + + /** + * @notice Configure trading limits for an (exchangeId, token) touple. + * @dev Will revert if the configuration is not valid according to the + * TradingLimits library. + * Resets existing state according to the TradingLimits library logic. + * Can only be called by owner. + * @param exchangeId the exchangeId to target. + * @param token the token to target. + * @param config the new trading limits config. + */ + function configureTradingLimit(bytes32 exchangeId, address token, ITradingLimits.Config calldata config) external; /** * @notice Get the list of registered exchange providers. @@ -116,44 +165,19 @@ interface IBroker { */ function getExchangeProviders() external view returns (address[] memory); - function burnStableTokens(address token, uint256 amount) external returns (bool); - /** - * @notice Allows the contract to be upgradable via the proxy. - * @param _exchangeProviders The addresses of the ExchangeProvider contracts. - * @param _reserves The address of the Reserve contract. + * @notice Get the address of the exchange provider at a given index. + * @dev Auto-generated getter for the exchangeProviders array. + * @param index The index of the exchange provider. + * @return exchangeProvider The address of the exchange provider. */ - function initialize(address[] calldata _exchangeProviders, address[] calldata _reserves) external; - - //TODO: Bogdan added these to the interface but when upgrading to 0.8.18, - // This is causing a compilation error. because they are defined in Ownable.sol as well. - // only way of fixing this is to remove them from the interface. or have the explicit - // implementation in the contract and add a override(IBroker, Ownable) to the functions. - - //function renounceOwnership() external; - - //function owner() external view returns (address); - - /// @notice IOwnable: - //function transferOwnership(address newOwner) external; - - /// @notice Getters: - //function reserve() external view returns (address); + function exchangeProviders(uint256 index) external view returns (address exchangeProvider); + /** + * @notice Check if a given address is an exchange provider. + * @dev Auto-generated getter for the isExchangeProvider mapping. + * @param exchangeProvider The address to check. + * @return isExchangeProvider True if the address is an exchange provider, false otherwise. + */ function isExchangeProvider(address exchangeProvider) external view returns (bool); - - /// @notice Setters: - function addExchangeProvider(address exchangeProvider, address reserve) external returns (uint256 index); - - function removeExchangeProvider(address exchangeProvider, uint256 index) external; - - function setReserves(address[] calldata _exchangeProviders, address[] calldata _reserves) external; - - function configureTradingLimit(bytes32 exchangeId, address token, ITradingLimits.Config calldata config) external; - - // function tradingLimitsConfig(bytes32 id) external view returns (ITradingLimits.Config memory); - - // function tradingLimitsState(bytes32 id) external view returns (ITradingLimits.State memory); - - function exchangeProviders(uint256 i) external view returns (address); } diff --git a/contracts/interfaces/IExchangeProvider.sol b/contracts/interfaces/IExchangeProvider.sol index b6f956fa..79c9353f 100644 --- a/contracts/interfaces/IExchangeProvider.sol +++ b/contracts/interfaces/IExchangeProvider.sol @@ -76,7 +76,7 @@ interface IExchangeProvider { /** * @notice Calculate amountIn of tokenIn needed for a given amountOut of tokenOut - * @param exchangeId The id of the exchange to use + * @param exchangeId The ID of the pool to use * @param tokenIn The token to be sold * @param tokenOut The token to be bought * @param amountOut The amount of tokenOut to be bought diff --git a/contracts/interfaces/IGoodDollarExchangeProvider.sol b/contracts/interfaces/IGoodDollarExchangeProvider.sol index 8b7b85eb..a4e92a40 100644 --- a/contracts/interfaces/IGoodDollarExchangeProvider.sol +++ b/contracts/interfaces/IGoodDollarExchangeProvider.sol @@ -3,7 +3,9 @@ pragma solidity >=0.5.17 <0.8.19; pragma experimental ABIEncoderV2; interface IGoodDollarExchangeProvider { - /* ------- Events ------- */ + /* ========================================== */ + /* ================= Events ================= */ + /* ========================================== */ /** * @notice Emitted when the ExpansionController address is updated. @@ -12,20 +14,22 @@ interface IGoodDollarExchangeProvider { event ExpansionControllerUpdated(address indexed expansionController); /** - * @notice Emitted when the AVATAR address is updated. - * @param AVATAR The address of the AVATAR contract. + * @notice Emitted when the GoodDollar DAO address is updated. + * @param AVATAR The address of the GoodDollar DAO contract. */ // solhint-disable-next-line var-name-mixedcase event AvatarUpdated(address indexed AVATAR); /** - * @notice Emitted when reserve ratio for exchange is updated. - * @param exchangeId The id of the exchange. + * @notice Emitted when the reserve ratio for a pool is updated. + * @param exchangeId The id of the pool. * @param reserveRatio The new reserve ratio. */ event ReserveRatioUpdated(bytes32 indexed exchangeId, uint32 reserveRatio); - /* ------- Functions ------- */ + /* =========================================== */ + /* ================ Functions ================ */ + /* =========================================== */ /** * @notice Initializes the contract with the given parameters. @@ -37,35 +41,47 @@ interface IGoodDollarExchangeProvider { function initialize(address _broker, address _reserve, address _expansionController, address _avatar) external; /** - * @notice calculates the amount of tokens to be minted as a result of expansion. - * @param exchangeId The id of the pool to calculate expansion for. + * @notice Sets the address of the GoodDollar DAO contract. + * @param _avatar The address of the DAO contract. + */ + function setAvatar(address _avatar) external; + + /** + * @notice Sets the address of the Expansion Controller contract. + * @param _expansionController The address of the Expansion Controller contract. + */ + function setExpansionController(address _expansionController) external; + + /** + * @notice Calculates the amount of G$ tokens to be minted as a result of the expansion. + * @param exchangeId The ID of the pool to calculate the expansion for. * @param expansionScaler Scaler for calculating the new reserve ratio. - * @return amountToMint amount of tokens to be minted as a result of the expansion. + * @return amountToMint Amount of G$ tokens to be minted as a result of the expansion. */ function mintFromExpansion(bytes32 exchangeId, uint256 expansionScaler) external returns (uint256 amountToMint); /** - * @notice calculates the amount of tokens to be minted as a result of the reserve interest. - * @param exchangeId The id of the pool the reserve interest is added to. - * @param reserveInterest The amount of reserve tokens collected from interest. - * @return amount of tokens to be minted as a result of the reserve interest. + * @notice Calculates the amount of G$ tokens to be minted as a result of the collected reserve interest. + * @param exchangeId The ID of the pool the collected reserve interest is added to. + * @param reserveInterest The amount of reserve asset tokens collected from interest. + * @return amountToMint The amount of G$ tokens to be minted as a result of the collected reserve interest. */ - function mintFromInterest(bytes32 exchangeId, uint256 reserveInterest) external returns (uint256); + function mintFromInterest(bytes32 exchangeId, uint256 reserveInterest) external returns (uint256 amountToMint); /** - * @notice calculates the reserve ratio needed to mint the reward. - * @param exchangeId The id of the pool the reward is minted from. - * @param reward The amount of tokens to be minted as a reward. + * @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. */ function updateRatioForReward(bytes32 exchangeId, uint256 reward) external; /** - * @notice pauses the Exchange disables minting. + * @notice Pauses the Exchange, disabling minting. */ function pause() external; /** - * @notice unpauses the Exchange enables minting again. + * @notice Unpauses the Exchange, enabling minting again. */ function unpause() external; } diff --git a/contracts/interfaces/IGoodDollarExpansionController.sol b/contracts/interfaces/IGoodDollarExpansionController.sol index b43c7315..2268ef97 100644 --- a/contracts/interfaces/IGoodDollarExpansionController.sol +++ b/contracts/interfaces/IGoodDollarExpansionController.sol @@ -6,8 +6,8 @@ interface IGoodDollarExpansionController { /** * @notice Struct holding the configuration for the expansion of an exchange. * @param expansionRate The rate of expansion in percentage with 1e18 being 100%. - * @param expansionfrequency The frequency of expansion in seconds. - * @param lastExpansion The last timestamp an expansion was done. + * @param expansionFrequency The frequency of expansion in seconds. + * @param lastExpansion The timestamp of the last prior expansion. */ struct ExchangeExpansionConfig { uint64 expansionRate; @@ -36,38 +36,38 @@ interface IGoodDollarExpansionController { event ReserveUpdated(address indexed reserve); /** - * @notice Emitted when the AVATAR address is updated. - * @param avatar The address of the new AVATAR. + * @notice Emitted when the GoodDollar DAO address is updated. + * @param avatar The new address of the GoodDollar DAO. */ event AvatarUpdated(address indexed avatar); /** - * @notice Emitted when the expansion config is set for an exchange. - * @param exchangeId The id of the exchange. + * @notice Emitted when the expansion config is set for an pool. + * @param exchangeId The ID of the pool. * @param expansionRate The rate of expansion. - * @param expansionfrequency The frequency of expansion. + * @param expansionFrequency The frequency of expansion. */ - event ExpansionConfigSet(bytes32 indexed exchangeId, uint64 expansionRate, uint32 expansionfrequency); + event ExpansionConfigSet(bytes32 indexed exchangeId, uint64 expansionRate, uint32 expansionFrequency); /** - * @notice Emitted when a reward is minted. - * @param exchangeId The id of the exchange. + * @notice Emitted when a G$ reward is minted. + * @param exchangeId The ID of the pool. * @param to The address of the recipient. - * @param amount The amount of tokens minted. + * @param amount The amount of G$ tokens minted. */ event RewardMinted(bytes32 indexed exchangeId, address indexed to, uint256 amount); /** * @notice Emitted when UBI is minted through collecting reserve interest. - * @param exchangeId The id of the exchange. - * @param amount Amount of tokens minted. + * @param exchangeId The ID of the pool. + * @param amount The amount of G$ tokens minted. */ event InterestUBIMinted(bytes32 indexed exchangeId, uint256 amount); /** * @notice Emitted when UBI is minted through expansion. - * @param exchangeId The id of the exchange. - * @param amount Amount of tokens minted. + * @param exchangeId The ID of the pool. + * @param amount The amount of G$ tokens minted. */ event ExpansionUBIMinted(bytes32 indexed exchangeId, uint256 amount); @@ -87,6 +87,13 @@ interface IGoodDollarExpansionController { address _avatar ) external; + /** + * @notice Returns the expansion config for the given exchange. + * @param exchangeId The id of the exchange to get the expansion config for. + * @return config The expansion config. + */ + function getExpansionConfig(bytes32 exchangeId) external returns (ExchangeExpansionConfig memory); + /** * @notice Sets the GoodDollarExchangeProvider address. * @param _goodDollarExchangeProvider The address of the GoodDollarExchangeProvider contract. @@ -112,39 +119,39 @@ interface IGoodDollarExpansionController { function setAvatar(address _avatar) external; /** - * @notice Sets the expansion config for the given exchange. - * @param exchangeId The id of the exchange to set the expansion config for. + * @notice Sets the expansion config for the given pool. + * @param exchangeId The ID of the pool to set the expansion config for. * @param expansionRate The rate of expansion. * @param expansionFrequency The frequency of expansion. */ function setExpansionConfig(bytes32 exchangeId, uint64 expansionRate, uint32 expansionFrequency) external; /** - * @notice Mints UBI for the given exchange from collecting reserve interest. - * @param exchangeId The id of the exchange to mint UBI for. + * @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. */ function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external; /** - * @notice Mints UBI for the given exchange by comparing the reserve Balance of the contract to the virtual balance. - * @param exchangeId The id of the exchange to mint UBI for. - * @return amountMinted The amount of UBI tokens minted. + * @notice Mints UBI as G$ tokens for a given pool by comparing the contract's reserve balance to the virtual balance. + * @param exchangeId The ID of the pool to mint UBI for. + * @return amountMinted The amount of G$ tokens minted. */ function mintUBIFromReserveBalance(bytes32 exchangeId) external returns (uint256 amountMinted); /** - * @notice Mints UBI for the given exchange by calculating the expansion rate. - * @param exchangeId The id of the exchange to mint UBI for. - * @return amountMinted The amount of UBI tokens minted. + * @notice Mints UBI as G$ tokens for a given pool by calculating the expansion rate. + * @param exchangeId The ID of the pool to mint UBI for. + * @return amountMinted The amount of G$ tokens minted. */ function mintUBIFromExpansion(bytes32 exchangeId) external returns (uint256 amountMinted); /** - * @notice Mints a reward of tokens for the given exchange. - * @param exchangeId The id of the exchange to mint reward. + * @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 tokens to mint. + * @param amount The amount of G$ tokens to mint. */ - function mintRewardFromRR(bytes32 exchangeId, address to, uint256 amount) external; + function mintRewardFromReserveRatio(bytes32 exchangeId, address to, uint256 amount) external; } diff --git a/contracts/swap/Broker.sol b/contracts/swap/Broker.sol index b6365c8d..24cdd43e 100644 --- a/contracts/swap/Broker.sol +++ b/contracts/swap/Broker.sol @@ -14,7 +14,6 @@ import { ITradingLimits } from "../interfaces/ITradingLimits.sol"; import { TradingLimits } from "../libraries/TradingLimits.sol"; import { Initializable } from "celo/contracts/common/Initializable.sol"; -// TODO: ensure we can use latest impl of openzeppelin import { ReentrancyGuard } from "openzeppelin-contracts-next/contracts/security/ReentrancyGuard.sol"; interface IERC20Metadata { @@ -35,12 +34,16 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar /* ==================== State Variables ==================== */ + /// @inheritdoc IBroker address[] public exchangeProviders; + + /// @inheritdoc IBroker mapping(address => bool) public isExchangeProvider; mapping(bytes32 => ITradingLimits.State) public tradingLimitsState; mapping(bytes32 => ITradingLimits.Config) public tradingLimitsConfig; - // Address of the reserve. + // Deprecated address of the reserve. Kept to keep storage layout consistent with previous versions. + // slither-disable-next-line constable-states uint256 public __deprecated0; // prev: IReserve public reserve; uint256 private constant MAX_INT256 = uint256(type(int256).max); @@ -54,11 +57,7 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar */ constructor(bool test) Initializable(test) {} - /** - * @notice Allows the contract to be upgradable via the proxy. - * @param _exchangeProviders The addresses of the ExchangeProvider contracts. - * @param _reserves The addresses of the Reserve contracts. - */ + /// @inheritdoc IBroker function initialize(address[] calldata _exchangeProviders, address[] calldata _reserves) external initializer { _transferOwnership(msg.sender); for (uint256 i = 0; i < _exchangeProviders.length; i++) { @@ -66,11 +65,7 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar } } - /** - * @notice Set the reserves for the exchange providers. - * @param _exchangeProviders The addresses of the ExchangeProvider contracts. - * @param _reserves The addresses of the Reserve contracts. - */ + /// @inheritdoc IBroker function setReserves( address[] calldata _exchangeProviders, address[] calldata _reserves @@ -85,12 +80,7 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar /* ==================== Mutative Functions ==================== */ - /** - * @notice Add an exchange provider to the list of providers. - * @param exchangeProvider The address of the exchange provider to add. - * @param reserve The address of the reserve used by the exchange provider. - * @return index The index of the newly added specified exchange provider. - */ + /// @inheritdoc IBroker function addExchangeProvider( address exchangeProvider, address reserve @@ -106,11 +96,7 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar index = exchangeProviders.length - 1; } - /** - * @notice Remove an exchange provider from the list of providers. - * @param exchangeProvider The address of the exchange provider to remove. - * @param index The index of the exchange provider being removed. - */ + /// @inheritdoc IBroker function removeExchangeProvider( address exchangeProvider, uint256 index @@ -123,15 +109,7 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar emit ExchangeProviderRemoved(exchangeProvider); } - /** - * @notice Calculate the amount of tokenIn to be sold for a given amountOut of tokenOut - * @param exchangeProvider the address of the exchange manager for the pair - * @param exchangeId The id of the exchange to use - * @param tokenIn The token to be sold - * @param tokenOut The token to be bought - * @param amountOut The amount of tokenOut to be bought - * @return amountIn The amount of tokenIn to be sold - */ + /// @inheritdoc IBroker function getAmountIn( address exchangeProvider, bytes32 exchangeId, @@ -140,18 +118,14 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar uint256 amountOut ) external view returns (uint256 amountIn) { require(isExchangeProvider[exchangeProvider], "ExchangeProvider does not exist"); + address reserve = exchangeReserve[exchangeProvider]; + if (IReserve(reserve).isCollateralAsset(tokenOut)) { + require(IERC20(tokenOut).balanceOf(reserve) >= amountOut, "Insufficient balance in reserve"); + } amountIn = IExchangeProvider(exchangeProvider).getAmountIn(exchangeId, tokenIn, tokenOut, amountOut); } - /** - * @notice Calculate the amount of tokenOut to be bought for a given amount of tokenIn to be sold - * @param exchangeProvider the address of the exchange manager for the pair - * @param exchangeId The id of the exchange to use - * @param tokenIn The token to be sold - * @param tokenOut The token to be bought - * @param amountIn The amount of tokenIn to be sold - * @return amountOut The amount of tokenOut to be bought - */ + /// @inheritdoc IBroker function getAmountOut( address exchangeProvider, bytes32 exchangeId, @@ -161,18 +135,13 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar ) external view returns (uint256 amountOut) { require(isExchangeProvider[exchangeProvider], "ExchangeProvider does not exist"); amountOut = IExchangeProvider(exchangeProvider).getAmountOut(exchangeId, tokenIn, tokenOut, amountIn); + address reserve = exchangeReserve[exchangeProvider]; + if (IReserve(reserve).isCollateralAsset(tokenOut)) { + require(IERC20(tokenOut).balanceOf(reserve) >= amountOut, "Insufficient balance in reserve"); + } } - /** - * @notice Execute a token swap with fixed amountIn. - * @param exchangeProvider the address of the exchange provider for the pair. - * @param exchangeId The id of the exchange to use. - * @param tokenIn The token to be sold. - * @param tokenOut The token to be bought. - * @param amountIn The amount of tokenIn to be sold. - * @param amountOutMin Minimum amountOut to be received - controls slippage. - * @return amountOut The amount of tokenOut to be bought. - */ + /// @inheritdoc IBroker function swapIn( address exchangeProvider, bytes32 exchangeId, @@ -193,16 +162,7 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar emit Swap(exchangeProvider, exchangeId, msg.sender, tokenIn, tokenOut, amountIn, amountOut); } - /** - * @notice Execute a token swap with fixed amountOut. - * @param exchangeProvider the address of the exchange provider for the pair. - * @param exchangeId The id of the exchange to use. - * @param tokenIn The token to be sold. - * @param tokenOut The token to be bought. - * @param amountOut The amount of tokenOut to be bought. - * @param amountInMax Maximum amount of tokenIn that can be traded. - * @return amountIn The amount of tokenIn to be sold. - */ + /// @inheritdoc IBroker function swapOut( address exchangeProvider, bytes32 exchangeId, @@ -223,35 +183,19 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar emit Swap(exchangeProvider, exchangeId, msg.sender, tokenIn, tokenOut, amountIn, amountOut); } - /** - * @notice Permissionless way to burn stables from msg.sender directly. - * @param token The token getting burned. - * @param amount The amount of the token getting burned. - * @return True if transaction succeeds. - */ + /// @inheritdoc IBroker function burnStableTokens(address token, uint256 amount) public returns (bool) { IERC20(token).safeTransferFrom(msg.sender, address(this), amount); IERC20(token).safeBurn(amount); return true; } - /** - * @notice Configure trading limits for an (exchangeId, token) touple. - * @dev Will revert if the configuration is not valid according to the - * TradingLimits library. - * Resets existing state according to the TradingLimits library logic. - * Can only be called by owner. - * @param exchangeId the exchangeId to target. - * @param token the token to target. - * @param config the new trading limits config. - */ - // TODO: Make this external with next update. - // slither-disable-next-line external-function + /// @inheritdoc IBroker function configureTradingLimit( bytes32 exchangeId, address token, ITradingLimits.Config memory config - ) public onlyOwner { + ) external onlyOwner { config.validate(); bytes32 limitId = exchangeId ^ bytes32(uint256(uint160(token))); diff --git a/package.json b/package.json index 502ac44d..6406d575 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "fork-test:celo-mainnet": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract Celo", "check-no-ir": "./bin/check-contracts.sh", "check-contract-sizes": "env FOUNDRY_PROFILE=optimized forge build --sizes --skip \"test/**/*\"", + "slither": "slither .", "todo": "git ls-files -c --exclude-standard | grep -v \"package.json\" | xargs -I {} sh -c 'grep -H -n -i --color \"TODO:\\|FIXME:\" \"{}\" 2>/dev/null || true'" }, "dependencies": { diff --git a/test/fork/BaseForkTest.sol b/test/fork/BaseForkTest.sol index ab4afbb8..4a2a5eaf 100644 --- a/test/fork/BaseForkTest.sol +++ b/test/fork/BaseForkTest.sol @@ -9,7 +9,6 @@ import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; import { IRegistry } from "celo/contracts/common/interfaces/IRegistry.sol"; import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; -import { IBroker } from "contracts/interfaces/IBroker.sol"; import { Broker } from "contracts/swap/Broker.sol"; import { IReserve } from "contracts/interfaces/IReserve.sol"; import { ISortedOracles } from "contracts/interfaces/ISortedOracles.sol"; diff --git a/test/integration/protocol/GoodDollarIntegration.t.sol b/test/integration/protocol/GoodDollarIntegration.t.sol index 072840b0..b5f1315f 100644 --- a/test/integration/protocol/GoodDollarIntegration.t.sol +++ b/test/integration/protocol/GoodDollarIntegration.t.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.18; pragma experimental ABIEncoderV2; import { Test } from "mento-std/Test.sol"; -import { console } from "forge-std/console.sol"; import { GoodDollarExchangeProvider } from "contracts/goodDollar/GoodDollarExchangeProvider.sol"; import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarExpansionController.sol"; @@ -141,6 +140,7 @@ contract GoodDollarIntegrationTest is Test { exitContribution: 0.01 * 1e8 }); + vm.prank(avatar); exchangeId = IBancorExchangeProvider(address(exchangeProvider)).createExchange(poolExchange1); } @@ -155,7 +155,6 @@ contract GoodDollarIntegrationTest is Test { // @notice manual minting of GD through avatar because foundry deal crashes on GoodDollar contract function mintGoodDollar(uint256 amount, address to) public { - console.log("mintGoodDollar"); vm.prank(GoodDollarAvatar); gdToken.mint(to, amount); } @@ -194,7 +193,6 @@ contract GoodDollarIntegrationTest is Test { } function test_SwapIn_gDollarToReserveToken() public { - console.log("test_SwapIn_gDollarToReserveToken"); uint256 amountIn = 1000 * 1e18; uint256 reserveBalanceBefore = reserveToken.balanceOf(address(reserve)); diff --git a/test/unit/goodDollar/BancorExchangeProvider.t.sol b/test/unit/goodDollar/BancorExchangeProvider.t.sol index 0ffd6ebc..24c1e0d2 100644 --- a/test/unit/goodDollar/BancorExchangeProvider.t.sol +++ b/test/unit/goodDollar/BancorExchangeProvider.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, max-line-length // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -import { Test, console } from "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; import { ERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/ERC20.sol"; import { BancorExchangeProvider } from "contracts/goodDollar/BancorExchangeProvider.sol"; import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; @@ -169,7 +169,7 @@ contract BancorExchangeProviderTest_initilizerSettersGetters is BancorExchangePr bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint32 maxWeight = bancorExchangeProvider.MAX_WEIGHT(); - vm.expectRevert("Invalid exit contribution"); + vm.expectRevert("Exit contribution is too high"); bancorExchangeProvider.setExitContribution(exchangeId, maxWeight + 1); } @@ -189,7 +189,7 @@ contract BancorExchangeProviderTest_initilizerSettersGetters is BancorExchangePr function test_getPoolExchange_whenExchangeDoesNotExist_shouldRevert() public { bytes32 exchangeId = "0xexchangeId"; - vm.expectRevert("An exchange with the specified id does not exist"); + vm.expectRevert("Exchange does not exist"); bancorExchangeProvider.getPoolExchange(exchangeId); } @@ -260,7 +260,7 @@ contract BancorExchangeProviderTest_createExchange is BancorExchangeProviderTest abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken)), abi.encode(false) ); - vm.expectRevert("reserve asset must be a collateral registered with the reserve"); + vm.expectRevert("Reserve asset must be a collateral registered with the reserve"); bancorExchangeProvider.createExchange(poolExchange1); } @@ -276,28 +276,28 @@ contract BancorExchangeProviderTest_createExchange is BancorExchangeProviderTest abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token)), abi.encode(false) ); - vm.expectRevert("token must be a stable registered with the reserve"); + vm.expectRevert("Token must be a stable registered with the reserve"); bancorExchangeProvider.createExchange(poolExchange1); } function test_createExchange_whenReserveRatioIsSmaller2_shouldRevert() public { poolExchange1.reserveRatio = 0; - vm.expectRevert("Invalid reserve ratio"); + vm.expectRevert("Reserve ratio is too low"); bancorExchangeProvider.createExchange(poolExchange1); poolExchange1.reserveRatio = 1; - vm.expectRevert("Invalid reserve ratio"); + vm.expectRevert("Reserve ratio is too low"); bancorExchangeProvider.createExchange(poolExchange1); } function test_createExchange_whenReserveRatioAbove100Percent_shouldRevert() public { poolExchange1.reserveRatio = bancorExchangeProvider.MAX_WEIGHT() + 1; - vm.expectRevert("Invalid reserve ratio"); + vm.expectRevert("Reserve ratio is too high"); bancorExchangeProvider.createExchange(poolExchange1); } function test_createExchange_whenExitContributionAbove100Percent_shouldRevert() public { poolExchange1.exitContribution = bancorExchangeProvider.MAX_WEIGHT() + 1; - vm.expectRevert("Invalid exit contribution"); + vm.expectRevert("Exit contribution is too high"); bancorExchangeProvider.createExchange(poolExchange1); } @@ -309,13 +309,13 @@ contract BancorExchangeProviderTest_createExchange is BancorExchangeProviderTest function test_createExchanges_whenReserveTokenHasMoreDecimalsThan18_shouldRevert() public { vm.mockCall(address(reserveToken), abi.encodeWithSelector(reserveToken.decimals.selector), abi.encode(19)); - vm.expectRevert("reserve token decimals must be <= 18"); + vm.expectRevert("Reserve asset decimals must be <= 18"); bancorExchangeProvider.createExchange(poolExchange1); } function test_createExchange_whenTokenHasMoreDecimalsThan18_shouldRevert() public { vm.mockCall(address(token), abi.encodeWithSelector(token.decimals.selector), abi.encode(19)); - vm.expectRevert("token decimals must be <= 18"); + vm.expectRevert("Token decimals must be <= 18"); bancorExchangeProvider.createExchange(poolExchange1); } @@ -398,13 +398,8 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { function test_getAmountIn_whenExchangeDoesNotExist_shouldRevert() public { bytes32 exchangeId = "0xexchangeId"; - vm.expectRevert("An exchange with the specified id does not exist"); - bancorExchangeProvider.getAmountIn({ - exchangeId: exchangeId, - tokenIn: address(reserveToken), - tokenOut: address(token), - amountOut: 1e18 - }); + vm.expectRevert("Exchange does not exist"); + bancorExchangeProvider.getAmountIn(exchangeId, address(reserveToken), address(token), 1e18); } function test_getAmountIn_whenTokenInNotInExchange_shouldRevert() public { @@ -945,13 +940,8 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { function test_getAmountOut_whenExchangeDoesNotExist_shouldRevert() public { bytes32 exchangeId = "0xexchangeId"; - vm.expectRevert("An exchange with the specified id does not exist"); - bancorExchangeProvider.getAmountOut({ - exchangeId: exchangeId, - tokenIn: address(reserveToken), - tokenOut: address(token), - amountIn: 1e18 - }); + vm.expectRevert("Exchange does not exist"); + bancorExchangeProvider.getAmountOut(exchangeId, address(reserveToken), address(token), 1e18); } function test_getAmountOut_whenTokenInNotInExchange_shouldRevert() public { @@ -1527,7 +1517,7 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { function test_swapIn_whenExchangeDoesNotExist_shouldRevert() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); vm.prank(brokerAddress); - vm.expectRevert("An exchange with the specified id does not exist"); + vm.expectRevert("Exchange does not exist"); bancorExchangeProvider.swapIn("0xexchangeId", address(reserveToken), address(token), 1e18); } @@ -1616,7 +1606,7 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { function test_swapOut_whenExchangeDoesNotExist_shouldRevert() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); vm.prank(brokerAddress); - vm.expectRevert("An exchange with the specified id does not exist"); + vm.expectRevert("Exchange does not exist"); bancorExchangeProvider.swapOut("0xexchangeId", address(reserveToken), address(token), 1e18); } diff --git a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol index af6226d4..9a6dae10 100644 --- a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol +++ b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -import { Test, console } from "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; import { GoodDollarExchangeProvider } from "contracts/goodDollar/GoodDollarExchangeProvider.sol"; import { ERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/ERC20.sol"; @@ -14,14 +14,18 @@ import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangePro contract GoodDollarExchangeProviderTest is Test { /* ------- Events from IGoodDollarExchangeProvider ------- */ - event ExchangeCreated(bytes32 indexed exchangeId, address indexed reserveAsset, address indexed tokenAddress); - event ExpansionControllerUpdated(address indexed expansionController); event AvatarUpdated(address indexed AVATAR); event ReserveRatioUpdated(bytes32 indexed exchangeId, uint32 reserveRatio); + event ExchangeCreated(bytes32 indexed exchangeId, address indexed reserveAsset, address indexed tokenAddress); + + event ExchangeDestroyed(bytes32 indexed exchangeId, address indexed reserveAsset, address indexed tokenAddress); + + event ExitContributionSet(bytes32 indexed exchangeId, uint256 exitContribution); + /* ------------------------------------------- */ ERC20 public reserveToken; @@ -34,6 +38,7 @@ contract GoodDollarExchangeProviderTest is Test { address public expansionControllerAddress; IBancorExchangeProvider.PoolExchange public poolExchange1; + IBancorExchangeProvider.PoolExchange public poolExchange2; function setUp() public virtual { reserveToken = new ERC20("cUSD", "cUSD"); @@ -54,6 +59,15 @@ contract GoodDollarExchangeProviderTest is Test { exitContribution: 0.01 * 1e8 }); + poolExchange2 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token2), + tokenSupply: 300_000 * 1e18, + reserveBalance: 60_000 * 1e18, + reserveRatio: 1e8 * 0.2, + exitContribution: 1e8 * 0.01 + }); + vm.mockCall( reserveAddress, abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token)), @@ -138,6 +152,139 @@ contract GoodDollarExchangeProviderTest_initializerSettersGetters is GoodDollarE assertEq(address(exchangeProvider.expansionController()), newExpansionController); } + + /* ---------- setExitContribution ---------- */ + /* Focuses only on access control, implementation details are covered in BancorExchangeProvider tests */ + function test_setExitContribution_whenSenderIsOwner_shouldRevert() public { + vm.expectRevert("Only Avatar can call this function"); + bytes32 exchangeId = "0xexchangeId"; + exchangeProvider.setExitContribution(exchangeId, 1e5); + } + + function test_setExitContribution_whenSenderIsNotAvatar_shouldRevert() public { + vm.startPrank(makeAddr("NotAvatarAndNotOwner")); + vm.expectRevert("Only Avatar can call this function"); + bytes32 exchangeId = "0xexchangeId"; + exchangeProvider.setExitContribution(exchangeId, 1e5); + vm.stopPrank(); + } + + function test_setExitContribution_whenSenderIsAvatar_shouldUpdateAndEmit() public { + vm.startPrank(avatarAddress); + bytes32 exchangeId = exchangeProvider.createExchange(poolExchange1); + uint32 newExitContribution = 1e3; + vm.expectEmit(true, true, true, true); + emit ExitContributionSet(exchangeId, newExitContribution); + exchangeProvider.setExitContribution(exchangeId, newExitContribution); + + IBancorExchangeProvider.PoolExchange memory poolExchange = exchangeProvider.getPoolExchange(exchangeId); + assertEq(poolExchange.exitContribution, newExitContribution); + vm.stopPrank(); + } + /* ---------- setExitContribution end ---------- */ +} + +/** + * @notice createExchange tests + * @dev These tests focus only on access control. The implementation details + * are covered in the BancorExchangeProvider tests. + */ +contract GoodDollarExchangeProviderTest_createExchange is GoodDollarExchangeProviderTest { + GoodDollarExchangeProvider exchangeProvider; + + function setUp() public override { + super.setUp(); + exchangeProvider = initializeGoodDollarExchangeProvider(); + } + + function test_createExchange_whenSenderIsNotAvatar_shouldRevert() public { + vm.prank(makeAddr("NotAvatar")); + vm.expectRevert("Only Avatar can call this function"); + exchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenSenderIsOwner_shouldRevert() public { + vm.expectRevert("Only Avatar can call this function"); + exchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenSenderIsAvatar_shouldCreateExchangeAndEmit() public { + vm.startPrank(avatarAddress); + vm.expectEmit(true, true, true, true); + bytes32 expectedExchangeId = keccak256(abi.encodePacked(reserveToken.symbol(), token.symbol())); + emit ExchangeCreated(expectedExchangeId, address(reserveToken), address(token)); + bytes32 exchangeId = exchangeProvider.createExchange(poolExchange1); + assertEq(exchangeId, expectedExchangeId); + + IBancorExchangeProvider.PoolExchange memory poolExchange = exchangeProvider.getPoolExchange(exchangeId); + assertEq(poolExchange.reserveAsset, poolExchange1.reserveAsset); + assertEq(poolExchange.tokenAddress, poolExchange1.tokenAddress); + assertEq(poolExchange.tokenSupply, poolExchange1.tokenSupply); + assertEq(poolExchange.reserveBalance, poolExchange1.reserveBalance); + assertEq(poolExchange.reserveRatio, poolExchange1.reserveRatio); + assertEq(poolExchange.exitContribution, poolExchange1.exitContribution); + + IExchangeProvider.Exchange[] memory exchanges = exchangeProvider.getExchanges(); + assertEq(exchanges.length, 1); + assertEq(exchanges[0].exchangeId, exchangeId); + + assertEq(exchangeProvider.tokenPrecisionMultipliers(address(reserveToken)), 1); + assertEq(exchangeProvider.tokenPrecisionMultipliers(address(token)), 1); + vm.stopPrank(); + } +} + +/** + * @notice destroyExchange tests + * @dev These tests focus only on access control. The implementation details + * are covered in the BancorExchangeProvider tests. + */ +contract GoodDollarExchangeProviderTest_destroyExchange is GoodDollarExchangeProviderTest { + GoodDollarExchangeProvider exchangeProvider; + + function setUp() public override { + super.setUp(); + exchangeProvider = initializeGoodDollarExchangeProvider(); + } + + function test_destroyExchange_whenSenderIsOwner_shouldRevert() public { + vm.startPrank(avatarAddress); + bytes32 exchangeId = exchangeProvider.createExchange(poolExchange1); + vm.stopPrank(); + vm.expectRevert("Only Avatar can call this function"); + exchangeProvider.destroyExchange(exchangeId, 0); + } + + function test_destroyExchange_whenSenderIsNotAvatar_shouldRevert() public { + vm.startPrank(avatarAddress); + bytes32 exchangeId = exchangeProvider.createExchange(poolExchange1); + vm.stopPrank(); + + vm.startPrank(makeAddr("NotAvatar")); + vm.expectRevert("Only Avatar can call this function"); + exchangeProvider.destroyExchange(exchangeId, 0); + vm.stopPrank(); + } + + function test_destroyExchange_whenExchangeExists_shouldDestroyExchangeAndEmit() public { + vm.startPrank(avatarAddress); + bytes32 exchangeId = exchangeProvider.createExchange(poolExchange1); + bytes32 exchangeId2 = exchangeProvider.createExchange(poolExchange2); + vm.stopPrank(); + + vm.startPrank(avatarAddress); + vm.expectEmit(true, true, true, true); + emit ExchangeDestroyed(exchangeId, poolExchange1.reserveAsset, poolExchange1.tokenAddress); + exchangeProvider.destroyExchange(exchangeId, 0); + + bytes32[] memory exchangeIds = exchangeProvider.getExchangeIds(); + assertEq(exchangeIds.length, 1); + + IExchangeProvider.Exchange[] memory exchanges = exchangeProvider.getExchanges(); + assertEq(exchanges.length, 1); + assertEq(exchanges[0].exchangeId, exchangeId2); + vm.stopPrank(); + } } contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeProviderTest { @@ -149,6 +296,7 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP super.setUp(); expansionRate = 1e18 * 0.99; exchangeProvider = initializeGoodDollarExchangeProvider(); + vm.prank(avatarAddress); exchangeId = exchangeProvider.createExchange(poolExchange1); } @@ -166,7 +314,7 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP function test_mintFromExpansion_whenExchangeIdIsInvalid_shouldRevert() public { vm.prank(expansionControllerAddress); - vm.expectRevert("An exchange with the specified id does not exist"); + vm.expectRevert("Exchange does not exist"); exchangeProvider.mintFromExpansion(bytes32(0), expansionRate); } @@ -214,6 +362,7 @@ contract GoodDollarExchangeProviderTest_mintFromInterest is GoodDollarExchangePr super.setUp(); reserveInterest = 1000 * 1e18; exchangeProvider = initializeGoodDollarExchangeProvider(); + vm.prank(avatarAddress); exchangeId = exchangeProvider.createExchange(poolExchange1); } @@ -225,7 +374,7 @@ contract GoodDollarExchangeProviderTest_mintFromInterest is GoodDollarExchangePr function test_mintFromInterest_whenExchangeIdIsInvalid_shouldRevert() public { vm.prank(expansionControllerAddress); - vm.expectRevert("An exchange with the specified id does not exist"); + vm.expectRevert("Exchange does not exist"); exchangeProvider.mintFromInterest(bytes32(0), reserveInterest); } @@ -270,6 +419,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan super.setUp(); reward = 1000 * 1e18; exchangeProvider = initializeGoodDollarExchangeProvider(); + vm.prank(avatarAddress); exchangeId = exchangeProvider.createExchange(poolExchange1); } @@ -281,7 +431,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan function test_updateRatioForReward_whenExchangeIdIsInvalid_shouldRevert() public { vm.prank(expansionControllerAddress); - vm.expectRevert("An exchange with the specified id does not exist"); + vm.expectRevert("Exchange does not exist"); exchangeProvider.updateRatioForReward(bytes32(0), reward); } @@ -308,6 +458,7 @@ contract GoodDollarExchangeProviderTest_pausable is GoodDollarExchangeProviderTe function setUp() public override { super.setUp(); exchangeProvider = initializeGoodDollarExchangeProvider(); + vm.prank(avatarAddress); exchangeId = exchangeProvider.createExchange(poolExchange1); } diff --git a/test/unit/goodDollar/GoodDollarExpansionController.t.sol b/test/unit/goodDollar/GoodDollarExpansionController.t.sol index ae055d3b..739c8961 100644 --- a/test/unit/goodDollar/GoodDollarExpansionController.t.sol +++ b/test/unit/goodDollar/GoodDollarExpansionController.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -import { Test, console } from "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; import { ERC20Mock } from "openzeppelin-contracts-next/contracts/mocks/ERC20Mock.sol"; import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarExpansionController.sol"; @@ -23,7 +23,7 @@ contract GoodDollarExpansionControllerTest is Test { event AvatarUpdated(address indexed avatar); - event ExpansionConfigSet(bytes32 indexed exchangeId, uint64 expansionRate, uint32 expansionfrequency); + event ExpansionConfigSet(bytes32 indexed exchangeId, uint64 expansionRate, uint32 expansionFrequency); event RewardMinted(bytes32 indexed exchangeId, address indexed to, uint256 amount); @@ -124,24 +124,33 @@ contract GoodDollarExpansionControllerTest_initializerSettersGetters is GoodDoll assertEq(address(expansionController.goodDollarExchangeProvider()), newExchangeProvider); } - function test_setDistributionHelper_whenSenderIsNotOwner_shouldRevert() public { - vm.prank(makeAddr("NotOwner")); - vm.expectRevert("Ownable: caller is not the owner"); + function test_setDistributionHelper_whenCallerIsNotAvatar_shouldRevert() public { + vm.prank(makeAddr("NotAvatar")); + vm.expectRevert("Only Avatar can call this function"); + expansionController.setDistributionHelper(makeAddr("NewDistributionHelper")); + } + + function test_setDistributionHelper_whenCallerIsOwner_shouldRevert() public { + vm.expectRevert("Only Avatar can call this function"); expansionController.setDistributionHelper(makeAddr("NewDistributionHelper")); } - function test_setDistributiomHelper_whenAddressIsZero_shouldRevert() public { - vm.expectRevert("DistributionHelper address must be set"); + function test_setDistributionHelper_whenAddressIsZero_shouldRevert() public { + vm.startPrank(avatarAddress); + vm.expectRevert("Distribution helper address must be set"); expansionController.setDistributionHelper(address(0)); + vm.stopPrank(); } - function test_setDistributionHelper_whenCallerIsOwner_shouldUpdateAndEmit() public { + function test_setDistributionHelper_whenCallerIsAvatar_shouldUpdateAndEmit() public { + vm.startPrank(avatarAddress); address newDistributionHelper = makeAddr("NewDistributionHelper"); vm.expectEmit(true, true, true, true); emit DistributionHelperUpdated(newDistributionHelper); expansionController.setDistributionHelper(newDistributionHelper); assertEq(address(expansionController.distributionHelper()), newDistributionHelper); + vm.stopPrank(); } function test_setReserve_whenSenderIsNotOwner_shouldRevert() public { @@ -262,7 +271,7 @@ contract GoodDollarExpansionControllerTest_mintUBIFromInterest is GoodDollarExpa } function test_mintUBIFromInterest_whenReserveInterestIs0_shouldRevert() public { - vm.expectRevert("reserveInterest must be greater than 0"); + vm.expectRevert("Reserve interest must be greater than 0"); expansionController.mintUBIFromInterest(exchangeId, 0); } @@ -555,7 +564,7 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp } } -contract GoodDollarExpansionControllerTest_mintRewardFromRR is GoodDollarExpansionControllerTest { +contract GoodDollarExpansionControllerTest_mintRewardFromReserveRatio is GoodDollarExpansionControllerTest { GoodDollarExpansionController expansionController; function setUp() public override { @@ -586,25 +595,25 @@ contract GoodDollarExpansionControllerTest_mintRewardFromRR is GoodDollarExpansi ); } - function test_mintRewardFromRR_whenCallerIsNotAvatar_shouldRevert() public { + function test_mintRewardFromReserveRatio_whenCallerIsNotAvatar_shouldRevert() public { vm.prank(makeAddr("NotAvatar")); vm.expectRevert("Only Avatar can call this function"); - expansionController.mintRewardFromRR(exchangeId, makeAddr("To"), 1000e18); + expansionController.mintRewardFromReserveRatio(exchangeId, makeAddr("To"), 1000e18); } - function test_mintRewardFromRR_whenToIsZero_shouldRevert() public { + function test_mintRewardFromReserveRatio_whenToIsZero_shouldRevert() public { vm.prank(avatarAddress); - vm.expectRevert("Invalid to address"); - expansionController.mintRewardFromRR(exchangeId, address(0), 1000e18); + vm.expectRevert("Recipient address must be set"); + expansionController.mintRewardFromReserveRatio(exchangeId, address(0), 1000e18); } - function test_mintRewardFromRR_whenAmountIs0_shouldRevert() public { + function test_mintRewardFromReserveRatio_whenAmountIs0_shouldRevert() public { vm.prank(avatarAddress); vm.expectRevert("Amount must be greater than 0"); - expansionController.mintRewardFromRR(exchangeId, makeAddr("To"), 0); + expansionController.mintRewardFromReserveRatio(exchangeId, makeAddr("To"), 0); } - function test_mintRewardFromRR_whenCallerIsAvatar_shouldMintAndEmit() public { + function test_mintRewardFromReserveRatio_whenCallerIsAvatar_shouldMintAndEmit() public { uint256 amountToMint = 1000e18; address to = makeAddr("To"); uint256 toBalanceBefore = token.balanceOf(to); @@ -613,7 +622,7 @@ contract GoodDollarExpansionControllerTest_mintRewardFromRR is GoodDollarExpansi emit RewardMinted(exchangeId, to, amountToMint); vm.prank(avatarAddress); - expansionController.mintRewardFromRR(exchangeId, to, amountToMint); + expansionController.mintRewardFromReserveRatio(exchangeId, to, amountToMint); assertEq(token.balanceOf(to), toBalanceBefore + amountToMint); } diff --git a/test/unit/swap/Broker.t.sol b/test/unit/swap/Broker.t.sol index 91ca31b8..98a57893 100644 --- a/test/unit/swap/Broker.t.sol +++ b/test/unit/swap/Broker.t.sol @@ -225,27 +225,135 @@ contract BrokerTest_getAmounts is BrokerTest { function test_getAmountIn_whenExchangeProviderWasNotSet_shouldRevert() public { vm.expectRevert("ExchangeProvider does not exist"); - broker.getAmountIn(randomExchangeProvider, exchangeId, address(stableAsset), address(collateralAsset), 1e24); + broker.getAmountIn({ + exchangeProvider: randomExchangeProvider, + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountOut: 1e24 + }); + } + + function test_getAmountIn_whenReserveBalanceIsLessThanAmountOut_shouldRevert() public { + assertEq(collateralAsset.balanceOf(address(reserve)), 0); + vm.expectRevert("Insufficient balance in reserve"); + broker.getAmountIn({ + exchangeProvider: address(exchangeProvider), + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountOut: 1e24 + }); + } + + function test_getAmountIn_whenReserveBalanceIsEqualToAmountOut_shouldReturnAmountIn() public { + uint256 amountOut = 1e18; + collateralAsset.mint(address(reserve), amountOut); + + uint256 amountIn = broker.getAmountIn({ + exchangeProvider: address(exchangeProvider), + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountOut: amountOut + }); + + assertEq(amountIn, 25e17); + } + + function test_getAmountIn_whenReserveBalanceIsLargerThanAmountOut_shouldReturnAmountIn() public { + uint256 amountOut = 1e18; + collateralAsset.mint(address(reserve), 1000e18); + + uint256 amountIn = broker.getAmountIn({ + exchangeProvider: address(exchangeProvider), + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountOut: amountOut + }); + + assertEq(amountIn, 25e17); } - function test_getAmountIn_whenExchangeProviderIsSet_shouldReceiveCall() public view { - uint256 amountIn = broker.getAmountIn( + function test_getAmountIn_whenExchangeProviderIsSet_shouldReceiveCall() public { + collateralAsset.mint(address(reserve), 1000e18); + vm.expectCall( address(exchangeProvider), - exchangeId, - address(stableAsset), - address(collateralAsset), - 1e18 + abi.encodeWithSelector( + exchangeProvider.getAmountIn.selector, + exchangeId, + address(stableAsset), + address(collateralAsset), + 1e18 + ) ); + uint256 amountIn = broker.getAmountIn({ + exchangeProvider: address(exchangeProvider), + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountOut: 1e18 + }); assertEq(amountIn, 25e17); } function test_getAmountOut_whenExchangeProviderWasNotSet_shouldRevert() public { vm.expectRevert("ExchangeProvider does not exist"); - broker.getAmountOut(randomExchangeProvider, exchangeId, randomAsset, randomAsset, 1e24); + broker.getAmountOut({ + exchangeProvider: randomExchangeProvider, + exchangeId: exchangeId, + tokenIn: randomAsset, + tokenOut: randomAsset, + amountIn: 1e24 + }); + } + + function test_getAmountOut_whenReserveBalanceIsLessThanAmountOut_shouldRevert() public { + assertEq(collateralAsset.balanceOf(address(reserve)), 0); + vm.expectRevert("Insufficient balance in reserve"); + broker.getAmountOut({ + exchangeProvider: address(exchangeProvider), + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountIn: 1e24 + }); + } + + function test_getAmountOut_whenReserveBalanceIsEqualAmountOut_shouldReturnAmountIn() public { + uint256 amountIn = 1e18; + collateralAsset.mint(address(reserve), amountIn); + + uint256 amountOut = broker.getAmountOut({ + exchangeProvider: address(exchangeProvider), + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountIn: amountIn + }); + + assertEq(amountOut, 4e17); + } + + function test_getAmountOut_whenReserveBalanceIsLargerThanAmountOut_shouldReturnAmountIn() public { + uint256 amountIn = 1e18; + collateralAsset.mint(address(reserve), 1000e18); + + uint256 amountOut = broker.getAmountOut({ + exchangeProvider: address(exchangeProvider), + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountIn: amountIn + }); + + assertEq(amountOut, 4e17); } function test_getAmountOut_whenExchangeProviderIsSet_shouldReceiveCall() public { + collateralAsset.mint(address(reserve), 1000e18); vm.expectCall( address(exchangeProvider), abi.encodeWithSelector( @@ -257,13 +365,13 @@ contract BrokerTest_getAmounts is BrokerTest { ) ); - uint256 amountOut = broker.getAmountOut( - address(exchangeProvider), - exchangeId, - address(stableAsset), - address(collateralAsset), - 1e18 - ); + uint256 amountOut = broker.getAmountOut({ + exchangeProvider: address(exchangeProvider), + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountIn: 1e18 + }); assertEq(amountOut, 4e17); } } From 27d36560cdd7de1338a3118e7ba041212876fdca Mon Sep 17 00:00:00 2001 From: philbow61 Date: Fri, 18 Oct 2024 16:40:22 +0200 Subject: [PATCH 3/7] Revert "chore: merge latest base branch" This reverts commit d45a22ea951623f7ead901089d1328d0140ab81f. --- .github/workflows/echidna.yaml | 2 +- .github/workflows/lint_test.yaml | 50 ++-- .prettierrc.yml | 35 ++- .solhint.json | 30 ++- .../goodDollar/BancorExchangeProvider.sol | 255 ++++++++++-------- contracts/goodDollar/BancorFormula.sol | 7 - .../goodDollar/GoodDollarExchangeProvider.sol | 102 +++---- .../GoodDollarExpansionController.sol | 121 +++++---- .../goodDollar/interfaces/IGoodProtocol.sol | 1 - .../interfaces/IBancorExchangeProvider.sol | 76 ++---- contracts/interfaces/IBiPoolManager.sol | 3 +- contracts/interfaces/IBroker.sol | 144 +++++----- contracts/interfaces/IExchangeProvider.sol | 2 +- .../IGoodDollarExchangeProvider.sol | 54 ++-- .../IGoodDollarExpansionController.sol | 65 ++--- contracts/swap/Broker.sol | 104 +++++-- package.json | 1 - test/fork/BaseForkTest.sol | 1 + .../protocol/GoodDollarIntegration.t.sol | 4 +- .../goodDollar/BancorExchangeProvider.t.sol | 44 +-- .../GoodDollarExchangeProvider.t.sol | 163 +---------- .../GoodDollarExpansionController.t.sol | 47 ++-- test/unit/swap/Broker.t.sol | 138 ++-------- 23 files changed, 615 insertions(+), 834 deletions(-) diff --git a/.github/workflows/echidna.yaml b/.github/workflows/echidna.yaml index f73d4b66..a79c75bf 100644 --- a/.github/workflows/echidna.yaml +++ b/.github/workflows/echidna.yaml @@ -56,7 +56,7 @@ jobs: "test/integration/**/*" \ "test/unit/**/*" \ "test/utils/**/*" \ - "contracts/**/*" + "script/**/" - name: "Run Echidna" uses: crytic/echidna-action@v2 diff --git a/.github/workflows/lint_test.yaml b/.github/workflows/lint_test.yaml index 7a9a9705..3fd28969 100644 --- a/.github/workflows/lint_test.yaml +++ b/.github/workflows/lint_test.yaml @@ -1,59 +1,55 @@ -name: CI +name: "CI" env: - FOUNDRY_PROFILE: ci - ALFAJORES_RPC_URL: ${{secrets.ALFAJORES_RPC_URL}} - CELO_MAINNET_RPC_URL: ${{secrets.CELO_MAINNET_RPC_URL}} + FOUNDRY_PROFILE: "ci" on: workflow_dispatch: pull_request: push: branches: - - main - - develop - -permissions: read-all + - "main" + - "develop" jobs: lint_and_test: name: Lint & Test - runs-on: ubuntu-latest + runs-on: "ubuntu-latest" steps: - - name: Check out the repo - uses: actions/checkout@v3 + - name: "Check out the repo" + uses: "actions/checkout@v3" with: - submodules: recursive + submodules: "recursive" - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 - - name: Install Node.js - uses: actions/setup-node@v3 + - name: "Install Node.js" + uses: "actions/setup-node@v3" with: - cache: yarn + cache: "yarn" node-version: "20" - - name: Install the Node.js dependencies - run: yarn install --immutable + - name: "Install the Node.js dependencies" + run: "yarn install --immutable" - - name: Lint the contracts - run: yarn lint + - name: "Lint the contracts" + run: "yarn lint" - - name: Add lint summary + - name: "Add lint summary" run: | echo "## Lint" >> $GITHUB_STEP_SUMMARY echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - - name: Show the Foundry config - run: forge config + - name: "Show the Foundry config" + run: "forge config" - - name: Run the tests - run: forge test + - name: "Run the tests" + run: "forge test" - - name: Check contract sizes - run: yarn run check-contract-sizes + - name: "Check contract sizes" + run: "yarn run check-contract-sizes" - - name: Add test summary + - name: "Add test summary" run: | echo "## Tests" >> $GITHUB_STEP_SUMMARY diff --git a/.prettierrc.yml b/.prettierrc.yml index 699e7964..028dcf34 100644 --- a/.prettierrc.yml +++ b/.prettierrc.yml @@ -9,28 +9,10 @@ trailingComma: all plugins: [prettier-plugin-solidity] overrides: - # General Config - files: ["*.sol"] options: compiler: 0.5.17 - - files: [test/**/*.sol] - options: - compiler: "" - - # File-specific Config - - files: - [ - contracts/common/IERC20MintableBurnable.sol, - contracts/common/SafeERC20MintableBurnable.sol, - contracts/goodDollar/**/*.sol, - contracts/governance/**/*.sol, - contracts/interfaces/*.sol, - contracts/libraries/TradingLimits.sol, - contracts/oracles/Chainlink*.sol, - contracts/swap/Broker.sol, - contracts/tokens/patched/*.sol, - contracts/tokens/StableTokenV2.sol, - ] + - files: [contracts/interfaces/*.sol] options: compiler: 0.8.18 - files: @@ -39,3 +21,18 @@ overrides: - contracts/interfaces/IExchange.sol options: compiler: 0.5.17 + - files: [contracts/tokens/patched/*.sol] + options: + compiler: 0.8.18 + - files: [contracts/tokens/StableTokenV2.sol] + options: + compiler: 0.8.18 + - files: [contracts/governance/**/*.sol] + options: + compiler: 0.8.18 + - files: [test/**/*.sol] + options: + compiler: "" + - files: [contracts/oracles/Chainlink*.sol] + options: + compiler: 0.8.18 diff --git a/.solhint.json b/.solhint.json index 6b11569e..4d94dc31 100644 --- a/.solhint.json +++ b/.solhint.json @@ -2,17 +2,31 @@ "extends": "solhint:recommended", "plugins": ["prettier"], "rules": { + "no-global-import": "off", + "no-console": "off", "code-complexity": ["error", 8], "compiler-version": ["error", ">=0.5.13"], - "func-visibility": ["error", { "ignoreConstructors": true }], - "function-max-lines": ["error", 120], - "gas-custom-errors": "off", + "func-visibility": [ + "error", + { + "ignoreConstructors": true + } + ], "max-line-length": ["error", 121], - "no-console": "off", - "no-empty-blocks": "off", - "no-global-import": "off", "not-rely-on-time": "off", - "prettier/prettier": ["error", { "endOfLine": "auto" }], - "reason-string": ["warn", { "maxLength": 64 }] + "function-max-lines": ["error", 120], + "no-empty-blocks": "off", + "prettier/prettier": [ + "error", + { + "endOfLine": "auto" + } + ], + "reason-string": [ + "warn", + { + "maxLength": 64 + } + ] } } diff --git a/contracts/goodDollar/BancorExchangeProvider.sol b/contracts/goodDollar/BancorExchangeProvider.sol index d607a947..fc99a84f 100644 --- a/contracts/goodDollar/BancorExchangeProvider.sol +++ b/contracts/goodDollar/BancorExchangeProvider.sol @@ -16,9 +16,7 @@ import { UD60x18, unwrap, wrap } from "prb/math/UD60x18.sol"; * @notice Provides exchange functionality for Bancor pools. */ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, BancorFormula, OwnableUpgradeable { - /* ========================================================= */ /* ==================== State Variables ==================== */ - /* ========================================================= */ // Address of the broker contract. address public broker; @@ -31,16 +29,17 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B mapping(bytes32 => PoolExchange) public exchanges; bytes32[] public exchangeIds; - // Token precision multiplier used to normalize values to the same precision when calculating amounts. + // Token precision multiplier used to normalize values to the + // same precision when calculating amounts. mapping(address => uint256) public tokenPrecisionMultipliers; - /* ===================================================== */ /* ==================== Constructor ==================== */ - /* ===================================================== */ /** - * @dev Should be called with disable=true in deployments when it's accessed through a Proxy. - * Call this with disable=false during testing, when used without a proxy. + * @dev Should be called with disable=true in deployments when + * it's accessed through a Proxy. + * Call this with disable=false during testing, when used + * without a proxy. * @param disable Set to true to run `_disableInitializers()` inherited from * openzeppelin-contracts-upgradeable/Initializable.sol */ @@ -50,7 +49,11 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B } } - /// @inheritdoc IBancorExchangeProvider + /** + * @notice Allows the contract to be upgradable via the proxy. + * @param _broker The address of the broker contract. + * @param _reserve The address of the reserve contract. + */ function initialize(address _broker, address _reserve) public initializer { _initialize(_broker, _reserve); } @@ -63,9 +66,7 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B setReserve(_reserve); } - /* =================================================== */ /* ==================== Modifiers ==================== */ - /* =================================================== */ modifier onlyBroker() { require(msg.sender == broker, "Caller is not the Broker"); @@ -81,24 +82,28 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B _; } - /* ======================================================== */ /* ==================== View Functions ==================== */ - /* ======================================================== */ - /// @inheritdoc IBancorExchangeProvider + /** + * @notice Get a PoolExchange from storage. + * @param exchangeId the exchange id + */ function getPoolExchange(bytes32 exchangeId) public view returns (PoolExchange memory exchange) { exchange = exchanges[exchangeId]; - require(exchange.tokenAddress != address(0), "Exchange does not exist"); + require(exchange.tokenAddress != address(0), "An exchange with the specified id does not exist"); return exchange; } - /// @inheritdoc IBancorExchangeProvider + /** + * @notice Get all exchange IDs. + * @return exchangeIds List of the exchangeIds. + */ function getExchangeIds() external view returns (bytes32[] memory) { return exchangeIds; } /** - * @inheritdoc IExchangeProvider + * @notice Get all exchanges (used by interfaces) * @dev We don't expect the number of exchanges to grow to * astronomical values so this is safe gas-wise as is. */ @@ -113,7 +118,14 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B } } - /// @inheritdoc IExchangeProvider + /** + * @notice Calculate amountOut of tokenOut received for a given amountIn of tokenIn + * @param exchangeId The id of the exchange i.e PoolExchange to use + * @param tokenIn The token to be sold + * @param tokenOut The token to be bought + * @param amountIn The amount of tokenIn to be sold + * @return amountOut The amount of tokenOut to be bought + */ function getAmountOut( bytes32 exchangeId, address tokenIn, @@ -122,12 +134,19 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B ) external view virtual returns (uint256 amountOut) { PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledAmountIn = amountIn * tokenPrecisionMultipliers[tokenIn]; - uint256 scaledAmountOut = _getScaledAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); + uint256 scaledAmountOut = _getAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); amountOut = scaledAmountOut / tokenPrecisionMultipliers[tokenOut]; return amountOut; } - /// @inheritdoc IExchangeProvider + /** + * @notice Calculate amountIn of tokenIn for a given amountOut of tokenOut + * @param exchangeId The id of the exchange i.e PoolExchange to use + * @param tokenIn The token to be sold + * @param tokenOut The token to be bought + * @param amountOut The amount of tokenOut to be bought + * @return amountIn The amount of tokenIn to be sold + */ function getAmountIn( bytes32 exchangeId, address tokenIn, @@ -136,12 +155,16 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B ) external view virtual returns (uint256 amountIn) { PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledAmountOut = amountOut * tokenPrecisionMultipliers[tokenOut]; - uint256 scaledAmountIn = _getScaledAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); + uint256 scaledAmountIn = _getAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); amountIn = scaledAmountIn / tokenPrecisionMultipliers[tokenIn]; return amountIn; } - /// @inheritdoc IBancorExchangeProvider + /** + * @notice Get the current price of the pool. + * @param exchangeId The id of the pool to get the price for. + * @return price The current continous price of the pool. + */ function currentPrice(bytes32 exchangeId) public view returns (uint256 price) { // calculates: reserveBalance / (tokenSupply * reserveRatio) require(exchanges[exchangeId].reserveAsset != address(0), "Exchange does not exist"); @@ -152,83 +175,51 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B return price; } - /* ============================================================ */ /* ==================== Mutative Functions ==================== */ - /* ============================================================ */ - /// @inheritdoc IBancorExchangeProvider + /** + * @notice Sets the address of the broker contract. + * @param _broker The new address of the broker contract. + */ function setBroker(address _broker) public onlyOwner { require(_broker != address(0), "Broker address must be set"); broker = _broker; emit BrokerUpdated(_broker); } - /// @inheritdoc IBancorExchangeProvider + /** + * @notice Sets the address of the reserve contract. + * @param _reserve The new address of the reserve contract. + */ function setReserve(address _reserve) public onlyOwner { require(address(_reserve) != address(0), "Reserve address must be set"); reserve = IReserve(_reserve); emit ReserveUpdated(address(_reserve)); } - /// @inheritdoc IBancorExchangeProvider - function setExitContribution(bytes32 exchangeId, uint32 exitContribution) external virtual onlyOwner { - return _setExitContribution(exchangeId, exitContribution); - } - - /// @inheritdoc IBancorExchangeProvider - function createExchange(PoolExchange calldata _exchange) external virtual onlyOwner returns (bytes32 exchangeId) { - return _createExchange(_exchange); - } - - /// @inheritdoc IBancorExchangeProvider - function destroyExchange( - bytes32 exchangeId, - uint256 exchangeIdIndex - ) external virtual onlyOwner returns (bool destroyed) { - return _destroyExchange(exchangeId, exchangeIdIndex); - } - - /// @inheritdoc IExchangeProvider - function swapIn( - bytes32 exchangeId, - address tokenIn, - address tokenOut, - uint256 amountIn - ) public virtual onlyBroker returns (uint256 amountOut) { - PoolExchange memory exchange = getPoolExchange(exchangeId); - uint256 scaledAmountIn = amountIn * tokenPrecisionMultipliers[tokenIn]; - uint256 scaledAmountOut = _getScaledAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); - executeSwap(exchangeId, tokenIn, scaledAmountIn, scaledAmountOut); - - amountOut = scaledAmountOut / tokenPrecisionMultipliers[tokenOut]; - return amountOut; - } - - /// @inheritdoc IExchangeProvider - function swapOut( - bytes32 exchangeId, - address tokenIn, - address tokenOut, - uint256 amountOut - ) public virtual onlyBroker returns (uint256 amountIn) { - PoolExchange memory exchange = getPoolExchange(exchangeId); - uint256 scaledAmountOut = amountOut * tokenPrecisionMultipliers[tokenOut]; - uint256 scaledAmountIn = _getScaledAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); - executeSwap(exchangeId, tokenIn, scaledAmountIn, scaledAmountOut); + /** + * @notice Sets the exit contribution for a pool. + * @param exchangeId The id of the pool. + * @param exitContribution The exit contribution. + */ + function setExitContribution(bytes32 exchangeId, uint32 exitContribution) external onlyOwner { + require(exchanges[exchangeId].reserveAsset != address(0), "Exchange does not exist"); + require(exitContribution <= MAX_WEIGHT, "Invalid exit contribution"); - amountIn = scaledAmountIn / tokenPrecisionMultipliers[tokenIn]; - return amountIn; + PoolExchange storage exchange = exchanges[exchangeId]; + exchange.exitContribution = exitContribution; + emit ExitContributionSet(exchangeId, exitContribution); } - /* =========================================================== */ - /* ==================== Private Functions ==================== */ - /* =========================================================== */ - - function _createExchange(PoolExchange calldata _exchange) internal returns (bytes32 exchangeId) { + /** + * @notice Creates a new exchange using the given parameters. + * @param _exchange the PoolExchange to create. + * @return exchangeId The id of the newly created exchange. + */ + function createExchange(PoolExchange calldata _exchange) external onlyOwner returns (bytes32 exchangeId) { PoolExchange memory exchange = _exchange; - validateExchange(exchange); + validate(exchange); - // slither-disable-next-line encode-packed-collision exchangeId = keccak256( abi.encodePacked(IERC20(exchange.reserveAsset).symbol(), IERC20(exchange.tokenAddress).symbol()) ); @@ -236,8 +227,8 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B uint256 reserveAssetDecimals = IERC20(exchange.reserveAsset).decimals(); uint256 tokenDecimals = IERC20(exchange.tokenAddress).decimals(); - require(reserveAssetDecimals <= 18, "Reserve asset decimals must be <= 18"); - require(tokenDecimals <= 18, "Token decimals must be <= 18"); + require(reserveAssetDecimals <= 18, "reserve token decimals must be <= 18"); + require(tokenDecimals <= 18, "token decimals must be <= 18"); tokenPrecisionMultipliers[exchange.reserveAsset] = 10 ** (18 - uint256(reserveAssetDecimals)); tokenPrecisionMultipliers[exchange.tokenAddress] = 10 ** (18 - uint256(tokenDecimals)); @@ -247,7 +238,13 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B emit ExchangeCreated(exchangeId, exchange.reserveAsset, exchange.tokenAddress); } - function _destroyExchange(bytes32 exchangeId, uint256 exchangeIdIndex) internal returns (bool destroyed) { + /** + * @notice Destroys a exchange with the given parameters if it exists. + * @param exchangeId the id of the exchange to destroy + * @param exchangeIdIndex The index of the exchangeId in the ids array + * @return destroyed A boolean indicating whether or not the exchange was successfully destroyed. + */ + function destroyExchange(bytes32 exchangeId, uint256 exchangeIdIndex) external onlyOwner returns (bool destroyed) { require(exchangeIdIndex < exchangeIds.length, "exchangeIdIndex not in range"); require(exchangeIds[exchangeIdIndex] == exchangeId, "exchangeId at index doesn't match"); PoolExchange memory exchange = exchanges[exchangeId]; @@ -260,21 +257,61 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B emit ExchangeDestroyed(exchangeId, exchange.reserveAsset, exchange.tokenAddress); } - 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"); + /** + * @notice Execute a token swap with fixed amountIn + * @param exchangeId The id of exchange, i.e. PoolExchange to use + * @param tokenIn The token to be sold + * @param tokenOut The token to be bought + * @param amountIn The amount of tokenIn to be sold + * @return amountOut The amount of tokenOut to be bought + */ + function swapIn( + bytes32 exchangeId, + address tokenIn, + address tokenOut, + uint256 amountIn + ) public virtual onlyBroker returns (uint256 amountOut) { + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledAmountIn = amountIn * tokenPrecisionMultipliers[tokenIn]; + uint256 scaledAmountOut = _getAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); + executeSwap(exchangeId, tokenIn, scaledAmountIn, scaledAmountOut); - PoolExchange storage exchange = exchanges[exchangeId]; - exchange.exitContribution = exitContribution; - emit ExitContributionSet(exchangeId, exitContribution); + amountOut = scaledAmountOut / tokenPrecisionMultipliers[tokenOut]; + return amountOut; } /** - * @notice Execute a swap against the in-memory exchange and write the new exchange state to storage. - * @param exchangeId The ID of the pool + * @notice Execute a token swap with fixed amountOut + * @param exchangeId The id of exchange, i.e. PoolExchange to use + * @param tokenIn The token to be sold + * @param tokenOut The token to be bought + * @param amountOut The amount of tokenOut to be bought + * @return amountIn The amount of tokenIn to be sold + */ + function swapOut( + bytes32 exchangeId, + address tokenIn, + address tokenOut, + uint256 amountOut + ) public virtual onlyBroker returns (uint256 amountIn) { + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledAmountOut = amountOut * tokenPrecisionMultipliers[tokenOut]; + uint256 scaledAmountIn = _getAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); + executeSwap(exchangeId, tokenIn, scaledAmountIn, scaledAmountOut); + + amountIn = scaledAmountIn / tokenPrecisionMultipliers[tokenIn]; + return amountIn; + } + + /* ==================== Private Functions ==================== */ + + /** + * @notice Execute a swap against the in memory exchange and write + * the new exchange state to storage. + * @param exchangeId The id of the exchange * @param tokenIn The token to be sold - * @param scaledAmountIn The amount of tokenIn to be sold, scaled to 18 decimals - * @param scaledAmountOut The amount of tokenOut to be bought, scaled to 18 decimals + * @param scaledAmountIn The amount of tokenIn to be sold scaled to 18 decimals + * @param scaledAmountOut The amount of tokenOut to be bought scaled to 18 decimals */ function executeSwap(bytes32 exchangeId, address tokenIn, uint256 scaledAmountIn, uint256 scaledAmountOut) internal { PoolExchange memory exchange = getPoolExchange(exchangeId); @@ -290,14 +327,14 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B } /** - * @notice Calculate the scaledAmountIn of tokenIn for a given scaledAmountOut of tokenOut - * @param exchange The pool exchange to operate on + * @notice Calculate amountIn of tokenIn for a given amountOut of tokenOut + * @param exchange The exchange to operate on * @param tokenIn The token to be sold * @param tokenOut The token to be bought - * @param scaledAmountOut The amount of tokenOut to be bought, scaled to 18 decimals - * @return scaledAmountIn The amount of tokenIn to be sold, scaled to 18 decimals + * @param scaledAmountOut The amount of tokenOut to be bought scaled to 18 decimals + * @return scaledAmountIn The amount of tokenIn to be sold scaled to 18 decimals */ - function _getScaledAmountIn( + function _getAmountIn( PoolExchange memory exchange, address tokenIn, address tokenOut, @@ -313,14 +350,14 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B } /** - * @notice Calculate the scaledAmountOut of tokenOut received for a given scaledAmountIn of tokenIn - * @param exchange The pool exchange to operate on + * @notice Calculate amountOut of tokenOut received for a given amountIn of tokenIn + * @param exchange The exchange to operate on * @param tokenIn The token to be sold * @param tokenOut The token to be bought - * @param scaledAmountIn The amount of tokenIn to be sold, scaled to 18 decimals - * @return scaledAmountOut The amount of tokenOut to be bought, scaled to 18 decimals + * @param scaledAmountIn The amount of tokenIn to be sold scaled to 18 decimals + * @return scaledAmountOut The amount of tokenOut to be bought scaled to 18 decimals */ - function _getScaledAmountOut( + function _getAmountOut( PoolExchange memory exchange, address tokenIn, address tokenOut, @@ -346,20 +383,20 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B } /** - * @notice Validates a PoolExchange's parameters and configuration + * @notice Valitates a PoolExchange's parameters and configuration * @dev Reverts if not valid * @param exchange The PoolExchange to validate */ - function validateExchange(PoolExchange memory exchange) internal view { + function validate(PoolExchange memory exchange) internal view { require(exchange.reserveAsset != address(0), "Invalid reserve asset"); require( reserve.isCollateralAsset(exchange.reserveAsset), - "Reserve asset must be a collateral registered with the reserve" + "reserve asset must be a collateral registered with the reserve" ); require(exchange.tokenAddress != address(0), "Invalid token address"); - require(reserve.isStableAsset(exchange.tokenAddress), "Token must be a stable registered with the reserve"); - 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(reserve.isStableAsset(exchange.tokenAddress), "token must be a stable registered with the reserve"); + require(exchange.reserveRatio > 1, "Invalid reserve ratio"); + require(exchange.reserveRatio <= MAX_WEIGHT, "Invalid reserve ratio"); + require(exchange.exitContribution <= MAX_WEIGHT, "Invalid exit contribution"); } } diff --git a/contracts/goodDollar/BancorFormula.sol b/contracts/goodDollar/BancorFormula.sol index 29eaa918..e083ae12 100644 --- a/contracts/goodDollar/BancorFormula.sol +++ b/contracts/goodDollar/BancorFormula.sol @@ -559,13 +559,9 @@ contract BancorFormula { * - The natural logarithm of the input is calculated by summing up the intermediate results above * - For example: log(250) = log(e^4 * e^1 * e^0.5 * 1.021692859) = 4 + 1 + 0.5 + log(1 + 0.021692859) */ - // We're choosing to trust Bancor's audited Math - // slither-disable-start divide-before-multiply function optimalLog(uint256 x) internal pure returns (uint256) { uint256 res = 0; - // slither false positive, y is initialized as z = y = ... - // slither-disable-next-line uninitialized-local uint256 y; uint256 z; uint256 w; @@ -638,8 +634,6 @@ contract BancorFormula { function optimalExp(uint256 x) internal pure returns (uint256) { uint256 res = 0; - // slither false positive, y is initialized as z = y = ... - // slither-disable-next-line uninitialized-local uint256 y; uint256 z; @@ -702,4 +696,3 @@ contract BancorFormula { return res; } } -// slither-disable-end divide-before-multiply diff --git a/contracts/goodDollar/GoodDollarExchangeProvider.sol b/contracts/goodDollar/GoodDollarExchangeProvider.sol index e52cc103..1c19c1c6 100644 --- a/contracts/goodDollar/GoodDollarExchangeProvider.sol +++ b/contracts/goodDollar/GoodDollarExchangeProvider.sol @@ -14,9 +14,7 @@ import { UD60x18, unwrap, wrap } from "prb/math/UD60x18.sol"; * @notice Provides exchange functionality for the GoodDollar system. */ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchangeProvider, PausableUpgradeable { - /* ========================================================= */ /* ==================== State Variables ==================== */ - /* ========================================================= */ // Address of the Expansion Controller contract. IGoodDollarExpansionController public expansionController; @@ -25,19 +23,25 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan // solhint-disable-next-line var-name-mixedcase address public AVATAR; - /* ===================================================== */ /* ==================== Constructor ==================== */ - /* ===================================================== */ /** - * @dev Should be called with disable=true in deployments when it's accessed through a Proxy. - * Call this with disable=false during testing, when used without a proxy. + * @dev Should be called with disable=true in deployments when + * it's accessed through a Proxy. + * Call this with disable=false during testing, when used + * without a proxy. * @param disable Set to true to run `_disableInitializers()` inherited from * openzeppelin-contracts-upgradeable/Initializable.sol */ constructor(bool disable) BancorExchangeProvider(disable) {} - /// @inheritdoc IGoodDollarExchangeProvider + /** + * @notice Initializes the contract with the given parameters. + * @param _broker The address of the Broker contract. + * @param _reserve The address of the Reserve contract. + * @param _expansionController The address of the ExpansionController contract. + * @param _avatar The address of the GoodDollar DAO contract. + */ function initialize( address _broker, address _reserve, @@ -51,9 +55,7 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan setAvatar(_avatar); } - /* =================================================== */ /* ==================== Modifiers ==================== */ - /* =================================================== */ modifier onlyAvatar() { require(msg.sender == AVATAR, "Only Avatar can call this function"); @@ -65,18 +67,22 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan _; } - /* ============================================================ */ /* ==================== Mutative Functions ==================== */ - /* ============================================================ */ - /// @inheritdoc IGoodDollarExchangeProvider + /** + * @notice Sets the address of the GoodDollar DAO contract. + * @param _avatar The address of the DAO contract. + */ function setAvatar(address _avatar) public onlyOwner { require(_avatar != address(0), "Avatar address must be set"); AVATAR = _avatar; emit AvatarUpdated(_avatar); } - /// @inheritdoc IGoodDollarExchangeProvider + /** + * @notice Sets the address of the Expansion Controller contract. + * @param _expansionController The address of the Expansion Controller contract. + */ function setExpansionController(address _expansionController) public onlyOwner { require(_expansionController != address(0), "ExpansionController address must be set"); expansionController = IGoodDollarExpansionController(_expansionController); @@ -84,33 +90,13 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan } /** - * @inheritdoc BancorExchangeProvider - * @dev Only callable by the GoodDollar DAO contract. - */ - function setExitContribution(bytes32 exchangeId, uint32 exitContribution) external override onlyAvatar { - return _setExitContribution(exchangeId, exitContribution); - } - - /** - * @inheritdoc BancorExchangeProvider - * @dev Only callable by the GoodDollar DAO contract. + * @notice Execute a token swap with fixed amountIn + * @param exchangeId The id of exchange, i.e. PoolExchange to use + * @param tokenIn The token to be sold + * @param tokenOut The token to be bought + * @param amountIn The amount of tokenIn to be sold + * @return amountOut The amount of tokenOut to be bought */ - function createExchange(PoolExchange calldata _exchange) external override onlyAvatar returns (bytes32 exchangeId) { - return _createExchange(_exchange); - } - - /** - * @inheritdoc BancorExchangeProvider - * @dev Only callable by the GoodDollar DAO contract. - */ - function destroyExchange( - bytes32 exchangeId, - uint256 exchangeIdIndex - ) external override onlyAvatar returns (bool destroyed) { - return _destroyExchange(exchangeId, exchangeIdIndex); - } - - /// @inheritdoc BancorExchangeProvider function swapIn( bytes32 exchangeId, address tokenIn, @@ -120,7 +106,14 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan amountOut = BancorExchangeProvider.swapIn(exchangeId, tokenIn, tokenOut, amountIn); } - /// @inheritdoc BancorExchangeProvider + /** + * @notice Execute a token swap with fixed amountOut + * @param exchangeId The id of exchange, i.e. PoolExchange to use + * @param tokenIn The token to be sold + * @param tokenOut The token to be bought + * @param amountOut The amount of tokenOut to be bought + * @return amountIn The amount of tokenIn to be sold + */ function swapOut( bytes32 exchangeId, address tokenIn, @@ -131,10 +124,13 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan } /** - * @inheritdoc IGoodDollarExchangeProvider - * @dev Calculates the amount of G$ tokens that need to be minted as a result of the expansion + * @notice Calculates the amount of tokens to be minted as a result of expansion. + * @dev Calculates the amount of tokens that need to be minted as a result of the expansion * while keeping the current price the same. * calculation: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio + * @param exchangeId The id of the pool to calculate expansion for. + * @param expansionScaler Scaler for calculating the new reserve ratio. + * @return amountToMint amount of tokens to be minted as a result of the expansion. */ function mintFromExpansion( bytes32 exchangeId, @@ -157,15 +153,17 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan amountToMint = scaledAmountToMint / tokenPrecisionMultipliers[exchange.tokenAddress]; emit ReserveRatioUpdated(exchangeId, newRatioUint); - return amountToMint; } /** - * @inheritdoc IGoodDollarExchangeProvider - * @dev Calculates the amount of G$ tokens that need to be minted as a result of the reserve interest + * @notice Calculates the amount of tokens to be minted as a result of collecting the reserve interest. + * @dev Calculates the amount of tokens that need to be minted as a result of the reserve interest * flowing into the reserve while keeping the current price the same. * calculation: amountToMint = reserveInterest * tokenSupply / reserveBalance + * @param exchangeId The id of the pool the reserve interest is added to. + * @param reserveInterest The amount of reserve tokens collected from interest. + * @return amountToMint amount of tokens to be minted as a result of the reserve interest. */ function mintFromInterest( bytes32 exchangeId, @@ -186,9 +184,11 @@ 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. + * @notice Calculates the reserve ratio needed to mint the reward. + * @dev Calculates the new reserve ratio needed to mint the reward while keeping the current price the same. * calculation: newRatio = reserveBalance / (tokenSupply + reward) * currentPrice + * @param exchangeId The id of the pool the reward is minted from. + * @param reward The amount of tokens to be minted as a reward. */ function updateRatioForReward(bytes32 exchangeId, uint256 reward) external onlyExpansionController whenNotPaused { PoolExchange memory exchange = getPoolExchange(exchangeId); @@ -212,16 +212,16 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan } /** - * @inheritdoc IGoodDollarExchangeProvider - * @dev Only callable by the GoodDollar DAO contract. + * @notice Pauses the contract. + * @dev Functions is only callable by the GoodDollar DAO contract. */ function pause() external virtual onlyAvatar { _pause(); } /** - * @inheritdoc IGoodDollarExchangeProvider - * @dev Only callable by the GoodDollar DAO contract. + * @notice Unpauses the contract. + * @dev Functions is only callable by the GoodDollar DAO contract. */ function unpause() external virtual onlyAvatar { _unpause(); diff --git a/contracts/goodDollar/GoodDollarExpansionController.sol b/contracts/goodDollar/GoodDollarExpansionController.sol index 300ab958..2465e27a 100644 --- a/contracts/goodDollar/GoodDollarExpansionController.sol +++ b/contracts/goodDollar/GoodDollarExpansionController.sol @@ -17,9 +17,7 @@ import { unwrap, wrap, powu } from "prb/math/UD60x18.sol"; * @notice Provides functionality to expand the supply of GoodDollars. */ contract GoodDollarExpansionController is IGoodDollarExpansionController, PausableUpgradeable, OwnableUpgradeable { - /* ========================================================= */ /* ==================== State Variables ==================== */ - /* ========================================================= */ // MAX_WEIGHT is the max rate that can be assigned to an exchange uint256 public constant MAX_WEIGHT = 1e18; @@ -34,19 +32,19 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab IGoodDollarExchangeProvider public goodDollarExchangeProvider; // Maps exchangeId to exchangeExpansionConfig - mapping(bytes32 exchangeId => ExchangeExpansionConfig) public exchangeExpansionConfigs; + mapping(bytes32 => ExchangeExpansionConfig) public exchangeExpansionConfigs; // Address of the GoodDollar DAO contract. // solhint-disable-next-line var-name-mixedcase address public AVATAR; - /* ===================================================== */ /* ==================== Constructor ==================== */ - /* ===================================================== */ /** - * @dev Should be called with disable=true in deployments when it's accessed through a Proxy. - * Call this with disable=false during testing, when used without a proxy. + * @dev Should be called with disable=true in deployments when + * it's accessed through a Proxy. + * Call this with disable=false during testing, when used + * without a proxy. * @param disable Set to true to run `_disableInitializers()` inherited from * openzeppelin-contracts-upgradeable/Initializable.sol */ @@ -56,7 +54,13 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab } } - /// @inheritdoc IGoodDollarExpansionController + /** + * @notice Initializes the contract with the given parameters. + * @param _goodDollarExchangeProvider The address of the GoodDollarExchangeProvider contract. + * @param _distributionHelper The address of the distribution helper contract. + * @param _reserve The address of the Reserve contract. + * @param _avatar The address of the GoodDollar DAO contract. + */ function initialize( address _goodDollarExchangeProvider, address _distributionHelper, @@ -67,61 +71,78 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab __Ownable_init(); setGoodDollarExchangeProvider(_goodDollarExchangeProvider); - _setDistributionHelper(_distributionHelper); + setDistributionHelper(_distributionHelper); setReserve(_reserve); setAvatar(_avatar); } - /* =================================================== */ /* ==================== Modifiers ==================== */ - /* =================================================== */ modifier onlyAvatar() { require(msg.sender == AVATAR, "Only Avatar can call this function"); _; } - /* ======================================================== */ /* ==================== View Functions ==================== */ - /* ======================================================== */ - /// @inheritdoc IGoodDollarExpansionController + /** + * @notice Returns the expansion config for the given exchange. + * @param exchangeId The id of the exchange to get the expansion config for. + * @return config The expansion config. + */ function getExpansionConfig(bytes32 exchangeId) public view returns (ExchangeExpansionConfig memory) { require(exchangeExpansionConfigs[exchangeId].expansionRate > 0, "Expansion config not set"); return exchangeExpansionConfigs[exchangeId]; } - /* ============================================================ */ /* ==================== Mutative Functions ==================== */ - /* ============================================================ */ - /// @inheritdoc IGoodDollarExpansionController + /** + * @notice Sets the GoodDollarExchangeProvider address. + * @param _goodDollarExchangeProvider The address of the GoodDollarExchangeProvider contract. + */ function setGoodDollarExchangeProvider(address _goodDollarExchangeProvider) public onlyOwner { require(_goodDollarExchangeProvider != address(0), "GoodDollarExchangeProvider address must be set"); goodDollarExchangeProvider = IGoodDollarExchangeProvider(_goodDollarExchangeProvider); emit GoodDollarExchangeProviderUpdated(_goodDollarExchangeProvider); } - /// @inheritdoc IGoodDollarExpansionController - function setDistributionHelper(address _distributionHelper) public onlyAvatar { - return _setDistributionHelper(_distributionHelper); + /** + * @notice Sets the distribution helper address. + * @param _distributionHelper The address of the distribution helper contract. + */ + function setDistributionHelper(address _distributionHelper) public onlyOwner { + require(_distributionHelper != address(0), "DistributionHelper address must be set"); + distributionHelper = IDistributionHelper(_distributionHelper); + emit DistributionHelperUpdated(_distributionHelper); } - /// @inheritdoc IGoodDollarExpansionController + /** + * @notice Sets the reserve address. + * @param _reserve The address of the reserve contract. + */ function setReserve(address _reserve) public onlyOwner { require(_reserve != address(0), "Reserve address must be set"); reserve = _reserve; emit ReserveUpdated(_reserve); } - /// @inheritdoc IGoodDollarExpansionController + /** + * @notice Sets the AVATAR address. + * @param _avatar The address of the AVATAR contract. + */ function setAvatar(address _avatar) public onlyOwner { require(_avatar != address(0), "Avatar address must be set"); AVATAR = _avatar; emit AvatarUpdated(_avatar); } - /// @inheritdoc IGoodDollarExpansionController + /** + * @notice Sets the expansion config for the given exchange. + * @param exchangeId The id of the exchange to set the expansion config for. + * @param expansionRate The rate of expansion. + * @param expansionFrequency The frequency of expansion. + */ function setExpansionConfig(bytes32 exchangeId, uint64 expansionRate, uint32 expansionFrequency) external onlyAvatar { require(expansionRate < MAX_WEIGHT, "Expansion rate must be less than 100%"); require(expansionRate > 0, "Expansion rate must be greater than 0"); @@ -133,9 +154,13 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab emit ExpansionConfigSet(exchangeId, expansionRate, expansionFrequency); } - /// @inheritdoc IGoodDollarExpansionController + /** + * @notice Mints UBI for the given exchange from collecting reserve interest. + * @param exchangeId The id of the exchange to mint UBI for. + * @param reserveInterest The amount of reserve tokens collected from interest. + */ function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external { - require(reserveInterest > 0, "Reserve interest must be greater than 0"); + require(reserveInterest > 0, "reserveInterest must be greater than 0"); IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) .getPoolExchange(exchangeId); @@ -143,13 +168,14 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab require(IERC20(exchange.reserveAsset).transferFrom(msg.sender, reserve, reserveInterest), "Transfer failed"); IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountToMint); - - // Ignored, because contracts only interacts with trusted contracts and tokens - // slither-disable-next-line reentrancy-events emit InterestUBIMinted(exchangeId, amountToMint); } - /// @inheritdoc IGoodDollarExpansionController + /** + * @notice Mints UBI for the given exchange by comparing the reserve Balance of the contract to the virtual balance. + * @param exchangeId The id of the exchange to mint UBI for. + * @return amountMinted The amount of UBI tokens minted. + */ function mintUBIFromReserveBalance(bytes32 exchangeId) external returns (uint256 amountMinted) { IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) .getPoolExchange(exchangeId); @@ -160,13 +186,15 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab amountMinted = goodDollarExchangeProvider.mintFromInterest(exchangeId, additionalReserveBalance); 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, amountMinted); } } - /// @inheritdoc IGoodDollarExpansionController + /** + * @notice Mints UBI for the given exchange by calculating the expansion rate. + * @param exchangeId The id of the exchange to mint UBI for. + * @return amountMinted The amount of UBI tokens minted. + */ function mintUBIFromExpansion(bytes32 exchangeId) external returns (uint256 amountMinted) { ExchangeExpansionConfig memory config = getExpansionConfig(exchangeId); IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) @@ -176,7 +204,7 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab if (shouldExpand || config.lastExpansion == 0) { uint256 numberOfExpansions; - // Special case for first expansion + //special case for first expansion if (config.lastExpansion == 0) { numberOfExpansions = 1; } else { @@ -191,35 +219,24 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountMinted); distributionHelper.onDistribution(amountMinted); - - // Ignored, because contracts only interacts with trusted contracts and tokens - // slither-disable-next-line reentrancy-events emit ExpansionUBIMinted(exchangeId, amountMinted); } } - /// @inheritdoc IGoodDollarExpansionController - function mintRewardFromReserveRatio(bytes32 exchangeId, address to, uint256 amount) external onlyAvatar { - require(to != address(0), "Recipient address must be set"); + /** + * @notice Mints a reward of tokens for the given exchange. + * @param exchangeId The id of the exchange to mint reward. + * @param to The address of the recipient. + * @param amount The amount of tokens to mint. + */ + function mintRewardFromRR(bytes32 exchangeId, address to, uint256 amount) external onlyAvatar { + require(to != address(0), "Invalid to address"); require(amount > 0, "Amount must be greater than 0"); IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) .getPoolExchange(exchangeId); goodDollarExchangeProvider.updateRatioForReward(exchangeId, amount); IGoodDollar(exchange.tokenAddress).mint(to, amount); - - // Ignored, because contracts only interacts with trusted contracts and tokens - // slither-disable-next-line reentrancy-events emit RewardMinted(exchangeId, to, amount); } - - /* =========================================================== */ - /* ==================== Private Functions ==================== */ - /* =========================================================== */ - - function _setDistributionHelper(address _distributionHelper) internal { - require(_distributionHelper != address(0), "Distribution helper address must be set"); - distributionHelper = IDistributionHelper(_distributionHelper); - emit DistributionHelperUpdated(_distributionHelper); - } } diff --git a/contracts/goodDollar/interfaces/IGoodProtocol.sol b/contracts/goodDollar/interfaces/IGoodProtocol.sol index 1c774079..a7709f52 100644 --- a/contracts/goodDollar/interfaces/IGoodProtocol.sol +++ b/contracts/goodDollar/interfaces/IGoodProtocol.sol @@ -15,7 +15,6 @@ interface IGoodDollar { function balanceOf(address account) external view returns (uint256); - // slither-disable-next-line erc721-interface function approve(address spender, uint256 amount) external returns (bool); } diff --git a/contracts/interfaces/IBancorExchangeProvider.sol b/contracts/interfaces/IBancorExchangeProvider.sol index 9f491c97..ac2ab51c 100644 --- a/contracts/interfaces/IBancorExchangeProvider.sol +++ b/contracts/interfaces/IBancorExchangeProvider.sol @@ -12,9 +12,7 @@ interface IBancorExchangeProvider { uint32 exitContribution; } - /* ========================================== */ - /* ================= Events ================= */ - /* ========================================== */ + /* ------- Events ------- */ /** * @notice Emitted when the broker address is updated. @@ -29,16 +27,16 @@ interface IBancorExchangeProvider { event ReserveUpdated(address indexed newReserve); /** - * @notice Emitted when a new pool has been created. - * @param exchangeId The id of the new pool + * @notice Emitted when a new PoolExchange has been created. + * @param exchangeId The id of the new PoolExchange * @param reserveAsset The address of the reserve asset * @param tokenAddress The address of the token */ event ExchangeCreated(bytes32 indexed exchangeId, address indexed reserveAsset, address indexed tokenAddress); /** - * @notice Emitted when a pool has been destroyed. - * @param exchangeId The id of the pool to destroy + * @notice Emitted when a PoolExchange has been destroyed. + * @param exchangeId The id of the PoolExchange * @param reserveAsset The address of the reserve asset * @param tokenAddress The address of the token */ @@ -51,71 +49,47 @@ interface IBancorExchangeProvider { */ event ExitContributionSet(bytes32 indexed exchangeId, uint256 exitContribution); - /* ======================================================== */ - /* ==================== View Functions ==================== */ - /* ======================================================== */ - - /** - * @notice Allows the contract to be upgradable via the proxy. - * @param _broker The address of the broker contract. - * @param _reserve The address of the reserve contract. - */ - function initialize(address _broker, address _reserve) external; + /* ------- Functions ------- */ /** * @notice Retrieves the pool with the specified exchangeId. - * @param exchangeId The ID of the pool to be retrieved. - * @return exchange The pool with that ID. + * @param exchangeId The id of the pool to be retrieved. + * @return exchange The PoolExchange with that ID. */ function getPoolExchange(bytes32 exchangeId) external view returns (PoolExchange memory exchange); /** - * @notice Gets all pool IDs. - * @return exchangeIds List of the pool IDs. + * @notice Get all exchange IDs. + * @return exchangeIds List of the exchangeIds. */ function getExchangeIds() external view returns (bytes32[] memory exchangeIds); /** - * @notice Gets the current price based of the Bancor formula - * @param exchangeId The ID of the pool to get the price for - * @return price The current continuous price of the pool + * @notice Create a PoolExchange with the provided data. + * @param exchange The PoolExchange to be created. + * @return exchangeId The id of the exchange. */ - function currentPrice(bytes32 exchangeId) external view returns (uint256 price); - - /* ============================================================ */ - /* ==================== Mutative Functions ==================== */ - /* ============================================================ */ - /** - * @notice Sets the address of the broker contract. - * @param _broker The new address of the broker contract. - */ - function setBroker(address _broker) external; + function createExchange(PoolExchange calldata exchange) external returns (bytes32 exchangeId); /** - * @notice Sets the address of the reserve contract. - * @param _reserve The new address of the reserve contract. + * @notice Delete a PoolExchange. + * @param exchangeId The PoolExchange to be created. + * @param exchangeIdIndex The index of the exchangeId in the exchangeIds array. + * @return destroyed - true on successful delition. */ - function setReserve(address _reserve) external; + function destroyExchange(bytes32 exchangeId, uint256 exchangeIdIndex) external returns (bool destroyed); /** - * @notice Sets the exit contribution for a given pool - * @param exchangeId The ID of the pool + * @notice Set the exit contribution for a given exchange + * @param exchangeId The id of the exchange * @param exitContribution The exit contribution to be set */ function setExitContribution(bytes32 exchangeId, uint32 exitContribution) external; /** - * @notice Creates a new pool with the given parameters. - * @param exchange The pool to be created. - * @return exchangeId The ID of the new pool. - */ - function createExchange(PoolExchange calldata exchange) external returns (bytes32 exchangeId); - - /** - * @notice Destroys a pool with the given parameters if it exists. - * @param exchangeId The ID of the pool to be destroyed. - * @param exchangeIdIndex The index of the pool in the exchangeIds array. - * @return destroyed A boolean indicating whether or not the exchange was successfully destroyed. + * @notice gets the current price based of the bancor formula + * @param exchangeId The id of the exchange to get the price for + * @return price the current continious price */ - function destroyExchange(bytes32 exchangeId, uint256 exchangeIdIndex) external returns (bool destroyed); + function currentPrice(bytes32 exchangeId) external returns (uint256 price); } diff --git a/contracts/interfaces/IBiPoolManager.sol b/contracts/interfaces/IBiPoolManager.sol index 56194d55..0d17b9f8 100644 --- a/contracts/interfaces/IBiPoolManager.sol +++ b/contracts/interfaces/IBiPoolManager.sol @@ -13,7 +13,8 @@ import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; /** * @title BiPool Manager interface - * @notice An exchange provider implementation managing the state of all two-asset virtual pools. + * @notice The two asset pool manager is responsible for + * managing the state of all two-asset virtual pools. */ interface IBiPoolManager { /** diff --git a/contracts/interfaces/IBroker.sol b/contracts/interfaces/IBroker.sol index 00d8f913..1df81930 100644 --- a/contracts/interfaces/IBroker.sol +++ b/contracts/interfaces/IBroker.sol @@ -38,125 +38,76 @@ interface IBroker { event TradingLimitConfigured(bytes32 exchangeId, address token, ITradingLimits.Config config); /** - * @notice Allows the contract to be upgradable via the proxy. - * @param _exchangeProviders The addresses of the ExchangeProvider contracts. - * @param _reserves The address of the Reserve contract. - */ - function initialize(address[] calldata _exchangeProviders, address[] calldata _reserves) external; - - /** - * @notice Set the reserves for the exchange providers. - * @param _exchangeProviders The addresses of the ExchangeProvider contracts. - * @param _reserves The addresses of the Reserve contracts. - */ - function setReserves(address[] calldata _exchangeProviders, address[] calldata _reserves) external; - - /** - * @notice Add an exchange provider to the list of providers. - * @param exchangeProvider The address of the exchange provider to add. - * @param reserve The address of the reserve used by the exchange provider. - * @return index The index of the newly added specified exchange provider. - */ - function addExchangeProvider(address exchangeProvider, address reserve) external returns (uint256 index); - - /** - * @notice Remove an exchange provider from the list of providers. - * @param exchangeProvider The address of the exchange provider to remove. - * @param index The index of the exchange provider being removed. - */ - function removeExchangeProvider(address exchangeProvider, uint256 index) external; - - /** - * @notice Calculate amountIn of tokenIn needed for a given amountOut of tokenOut. + * @notice Execute a token swap with fixed amountIn. * @param exchangeProvider the address of the exchange provider for the pair. * @param exchangeId The id of the exchange to use. * @param tokenIn The token to be sold. * @param tokenOut The token to be bought. - * @param amountOut The amount of tokenOut to be bought. - * @return amountIn The amount of tokenIn to be sold. + * @param amountIn The amount of tokenIn to be sold. + * @param amountOutMin Minimum amountOut to be received - controls slippage. + * @return amountOut The amount of tokenOut to be bought. */ - function getAmountIn( + function swapIn( address exchangeProvider, bytes32 exchangeId, address tokenIn, address tokenOut, - uint256 amountOut - ) external view returns (uint256 amountIn); + uint256 amountIn, + uint256 amountOutMin + ) external returns (uint256 amountOut); /** - * @notice Calculate amountOut of tokenOut received for a given amountIn of tokenIn. + * @notice Execute a token swap with fixed amountOut. * @param exchangeProvider the address of the exchange provider for the pair. * @param exchangeId The id of the exchange to use. * @param tokenIn The token to be sold. * @param tokenOut The token to be bought. - * @param amountIn The amount of tokenIn to be sold. - * @return amountOut The amount of tokenOut to be bought. + * @param amountOut The amount of tokenOut to be bought. + * @param amountInMax Maximum amount of tokenIn that can be traded. + * @return amountIn The amount of tokenIn to be sold. */ - function getAmountOut( + function swapOut( address exchangeProvider, bytes32 exchangeId, address tokenIn, address tokenOut, - uint256 amountIn - ) external view returns (uint256 amountOut); + uint256 amountOut, + uint256 amountInMax + ) external returns (uint256 amountIn); /** - * @notice Execute a token swap with fixed amountIn. + * @notice Calculate amountOut of tokenOut received for a given amountIn of tokenIn. * @param exchangeProvider the address of the exchange provider for the pair. * @param exchangeId The id of the exchange to use. * @param tokenIn The token to be sold. * @param tokenOut The token to be bought. * @param amountIn The amount of tokenIn to be sold. - * @param amountOutMin Minimum amountOut to be received - controls slippage. * @return amountOut The amount of tokenOut to be bought. */ - function swapIn( + function getAmountOut( address exchangeProvider, bytes32 exchangeId, address tokenIn, address tokenOut, - uint256 amountIn, - uint256 amountOutMin - ) external returns (uint256 amountOut); + uint256 amountIn + ) external view returns (uint256 amountOut); /** - * @notice Execute a token swap with fixed amountOut. + * @notice Calculate amountIn of tokenIn needed for a given amountOut of tokenOut. * @param exchangeProvider the address of the exchange provider for the pair. * @param exchangeId The id of the exchange to use. * @param tokenIn The token to be sold. * @param tokenOut The token to be bought. * @param amountOut The amount of tokenOut to be bought. - * @param amountInMax Maximum amount of tokenIn that can be traded. * @return amountIn The amount of tokenIn to be sold. */ - function swapOut( + function getAmountIn( address exchangeProvider, bytes32 exchangeId, address tokenIn, address tokenOut, - uint256 amountOut, - uint256 amountInMax - ) external returns (uint256 amountIn); - - /** - * @notice Permissionless way to burn stables from msg.sender directly. - * @param token The token getting burned. - * @param amount The amount of the token getting burned. - * @return True if transaction succeeds. - */ - function burnStableTokens(address token, uint256 amount) external returns (bool); - - /** - * @notice Configure trading limits for an (exchangeId, token) touple. - * @dev Will revert if the configuration is not valid according to the - * TradingLimits library. - * Resets existing state according to the TradingLimits library logic. - * Can only be called by owner. - * @param exchangeId the exchangeId to target. - * @param token the token to target. - * @param config the new trading limits config. - */ - function configureTradingLimit(bytes32 exchangeId, address token, ITradingLimits.Config calldata config) external; + uint256 amountOut + ) external view returns (uint256 amountIn); /** * @notice Get the list of registered exchange providers. @@ -165,19 +116,44 @@ interface IBroker { */ function getExchangeProviders() external view returns (address[] memory); - /** - * @notice Get the address of the exchange provider at a given index. - * @dev Auto-generated getter for the exchangeProviders array. - * @param index The index of the exchange provider. - * @return exchangeProvider The address of the exchange provider. - */ - function exchangeProviders(uint256 index) external view returns (address exchangeProvider); + function burnStableTokens(address token, uint256 amount) external returns (bool); /** - * @notice Check if a given address is an exchange provider. - * @dev Auto-generated getter for the isExchangeProvider mapping. - * @param exchangeProvider The address to check. - * @return isExchangeProvider True if the address is an exchange provider, false otherwise. + * @notice Allows the contract to be upgradable via the proxy. + * @param _exchangeProviders The addresses of the ExchangeProvider contracts. + * @param _reserves The address of the Reserve contract. */ + function initialize(address[] calldata _exchangeProviders, address[] calldata _reserves) external; + + //TODO: Bogdan added these to the interface but when upgrading to 0.8.18, + // This is causing a compilation error. because they are defined in Ownable.sol as well. + // only way of fixing this is to remove them from the interface. or have the explicit + // implementation in the contract and add a override(IBroker, Ownable) to the functions. + + //function renounceOwnership() external; + + //function owner() external view returns (address); + + /// @notice IOwnable: + //function transferOwnership(address newOwner) external; + + /// @notice Getters: + //function reserve() external view returns (address); + function isExchangeProvider(address exchangeProvider) external view returns (bool); + + /// @notice Setters: + function addExchangeProvider(address exchangeProvider, address reserve) external returns (uint256 index); + + function removeExchangeProvider(address exchangeProvider, uint256 index) external; + + function setReserves(address[] calldata _exchangeProviders, address[] calldata _reserves) external; + + function configureTradingLimit(bytes32 exchangeId, address token, ITradingLimits.Config calldata config) external; + + // function tradingLimitsConfig(bytes32 id) external view returns (ITradingLimits.Config memory); + + // function tradingLimitsState(bytes32 id) external view returns (ITradingLimits.State memory); + + function exchangeProviders(uint256 i) external view returns (address); } diff --git a/contracts/interfaces/IExchangeProvider.sol b/contracts/interfaces/IExchangeProvider.sol index 79c9353f..b6f956fa 100644 --- a/contracts/interfaces/IExchangeProvider.sol +++ b/contracts/interfaces/IExchangeProvider.sol @@ -76,7 +76,7 @@ interface IExchangeProvider { /** * @notice Calculate amountIn of tokenIn needed for a given amountOut of tokenOut - * @param exchangeId The ID of the pool to use + * @param exchangeId The id of the exchange to use * @param tokenIn The token to be sold * @param tokenOut The token to be bought * @param amountOut The amount of tokenOut to be bought diff --git a/contracts/interfaces/IGoodDollarExchangeProvider.sol b/contracts/interfaces/IGoodDollarExchangeProvider.sol index a4e92a40..8b7b85eb 100644 --- a/contracts/interfaces/IGoodDollarExchangeProvider.sol +++ b/contracts/interfaces/IGoodDollarExchangeProvider.sol @@ -3,9 +3,7 @@ pragma solidity >=0.5.17 <0.8.19; pragma experimental ABIEncoderV2; interface IGoodDollarExchangeProvider { - /* ========================================== */ - /* ================= Events ================= */ - /* ========================================== */ + /* ------- Events ------- */ /** * @notice Emitted when the ExpansionController address is updated. @@ -14,22 +12,20 @@ interface IGoodDollarExchangeProvider { event ExpansionControllerUpdated(address indexed expansionController); /** - * @notice Emitted when the GoodDollar DAO address is updated. - * @param AVATAR The address of the GoodDollar DAO contract. + * @notice Emitted when the AVATAR address is updated. + * @param AVATAR The address of the AVATAR contract. */ // solhint-disable-next-line var-name-mixedcase event AvatarUpdated(address indexed AVATAR); /** - * @notice Emitted when the reserve ratio for a pool is updated. - * @param exchangeId The id of the pool. + * @notice Emitted when reserve ratio for exchange is updated. + * @param exchangeId The id of the exchange. * @param reserveRatio The new reserve ratio. */ event ReserveRatioUpdated(bytes32 indexed exchangeId, uint32 reserveRatio); - /* =========================================== */ - /* ================ Functions ================ */ - /* =========================================== */ + /* ------- Functions ------- */ /** * @notice Initializes the contract with the given parameters. @@ -41,47 +37,35 @@ interface IGoodDollarExchangeProvider { function initialize(address _broker, address _reserve, address _expansionController, address _avatar) external; /** - * @notice Sets the address of the GoodDollar DAO contract. - * @param _avatar The address of the DAO contract. - */ - function setAvatar(address _avatar) external; - - /** - * @notice Sets the address of the Expansion Controller contract. - * @param _expansionController The address of the Expansion Controller contract. - */ - function setExpansionController(address _expansionController) external; - - /** - * @notice Calculates the amount of G$ tokens to be minted as a result of the expansion. - * @param exchangeId The ID of the pool to calculate the expansion for. + * @notice calculates the amount of tokens to be minted as a result of expansion. + * @param exchangeId The id of the pool to calculate expansion for. * @param expansionScaler Scaler for calculating the new reserve ratio. - * @return amountToMint Amount of G$ tokens to be minted as a result of the expansion. + * @return amountToMint amount of tokens to be minted as a result of the expansion. */ function mintFromExpansion(bytes32 exchangeId, uint256 expansionScaler) external returns (uint256 amountToMint); /** - * @notice Calculates the amount of G$ tokens to be minted as a result of the collected reserve interest. - * @param exchangeId The ID of the pool the collected reserve interest is added to. - * @param reserveInterest The amount of reserve asset tokens collected from interest. - * @return amountToMint The amount of G$ tokens to be minted as a result of the collected reserve interest. + * @notice calculates the amount of tokens to be minted as a result of the reserve interest. + * @param exchangeId The id of the pool the reserve interest is added to. + * @param reserveInterest The amount of reserve tokens collected from interest. + * @return amount of tokens to be minted as a result of the reserve interest. */ - function mintFromInterest(bytes32 exchangeId, uint256 reserveInterest) external returns (uint256 amountToMint); + function mintFromInterest(bytes32 exchangeId, uint256 reserveInterest) external returns (uint256); /** - * @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. + * @notice calculates the reserve ratio needed to mint the reward. + * @param exchangeId The id of the pool the reward is minted from. + * @param reward The amount of tokens to be minted as a reward. */ function updateRatioForReward(bytes32 exchangeId, uint256 reward) external; /** - * @notice Pauses the Exchange, disabling minting. + * @notice pauses the Exchange disables minting. */ function pause() external; /** - * @notice Unpauses the Exchange, enabling minting again. + * @notice unpauses the Exchange enables minting again. */ function unpause() external; } diff --git a/contracts/interfaces/IGoodDollarExpansionController.sol b/contracts/interfaces/IGoodDollarExpansionController.sol index 2268ef97..b43c7315 100644 --- a/contracts/interfaces/IGoodDollarExpansionController.sol +++ b/contracts/interfaces/IGoodDollarExpansionController.sol @@ -6,8 +6,8 @@ interface IGoodDollarExpansionController { /** * @notice Struct holding the configuration for the expansion of an exchange. * @param expansionRate The rate of expansion in percentage with 1e18 being 100%. - * @param expansionFrequency The frequency of expansion in seconds. - * @param lastExpansion The timestamp of the last prior expansion. + * @param expansionfrequency The frequency of expansion in seconds. + * @param lastExpansion The last timestamp an expansion was done. */ struct ExchangeExpansionConfig { uint64 expansionRate; @@ -36,38 +36,38 @@ interface IGoodDollarExpansionController { event ReserveUpdated(address indexed reserve); /** - * @notice Emitted when the GoodDollar DAO address is updated. - * @param avatar The new address of the GoodDollar DAO. + * @notice Emitted when the AVATAR address is updated. + * @param avatar The address of the new AVATAR. */ event AvatarUpdated(address indexed avatar); /** - * @notice Emitted when the expansion config is set for an pool. - * @param exchangeId The ID of the pool. + * @notice Emitted when the expansion config is set for an exchange. + * @param exchangeId The id of the exchange. * @param expansionRate The rate of expansion. - * @param expansionFrequency The frequency of expansion. + * @param expansionfrequency The frequency of expansion. */ - event ExpansionConfigSet(bytes32 indexed exchangeId, uint64 expansionRate, uint32 expansionFrequency); + event ExpansionConfigSet(bytes32 indexed exchangeId, uint64 expansionRate, uint32 expansionfrequency); /** - * @notice Emitted when a G$ reward is minted. - * @param exchangeId The ID of the pool. + * @notice Emitted when a reward is minted. + * @param exchangeId The id of the exchange. * @param to The address of the recipient. - * @param amount The amount of G$ tokens minted. + * @param amount The amount of tokens minted. */ event RewardMinted(bytes32 indexed exchangeId, address indexed to, uint256 amount); /** * @notice Emitted when UBI is minted through collecting reserve interest. - * @param exchangeId The ID of the pool. - * @param amount The amount of G$ tokens minted. + * @param exchangeId The id of the exchange. + * @param amount Amount of tokens minted. */ event InterestUBIMinted(bytes32 indexed exchangeId, uint256 amount); /** * @notice Emitted when UBI is minted through expansion. - * @param exchangeId The ID of the pool. - * @param amount The amount of G$ tokens minted. + * @param exchangeId The id of the exchange. + * @param amount Amount of tokens minted. */ event ExpansionUBIMinted(bytes32 indexed exchangeId, uint256 amount); @@ -87,13 +87,6 @@ interface IGoodDollarExpansionController { address _avatar ) external; - /** - * @notice Returns the expansion config for the given exchange. - * @param exchangeId The id of the exchange to get the expansion config for. - * @return config The expansion config. - */ - function getExpansionConfig(bytes32 exchangeId) external returns (ExchangeExpansionConfig memory); - /** * @notice Sets the GoodDollarExchangeProvider address. * @param _goodDollarExchangeProvider The address of the GoodDollarExchangeProvider contract. @@ -119,39 +112,39 @@ interface IGoodDollarExpansionController { function setAvatar(address _avatar) external; /** - * @notice Sets the expansion config for the given pool. - * @param exchangeId The ID of the pool to set the expansion config for. + * @notice Sets the expansion config for the given exchange. + * @param exchangeId The id of the exchange to set the expansion config for. * @param expansionRate The rate of expansion. * @param expansionFrequency The frequency of expansion. */ function setExpansionConfig(bytes32 exchangeId, uint64 expansionRate, uint32 expansionFrequency) external; /** - * @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. + * @notice Mints UBI for the given exchange from collecting reserve interest. + * @param exchangeId The id of the exchange to mint UBI for. * @param reserveInterest The amount of reserve tokens collected from interest. */ function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external; /** - * @notice Mints UBI as G$ tokens for a given pool by comparing the contract's reserve balance to the virtual balance. - * @param exchangeId The ID of the pool to mint UBI for. - * @return amountMinted The amount of G$ tokens minted. + * @notice Mints UBI for the given exchange by comparing the reserve Balance of the contract to the virtual balance. + * @param exchangeId The id of the exchange to mint UBI for. + * @return amountMinted The amount of UBI tokens minted. */ function mintUBIFromReserveBalance(bytes32 exchangeId) external returns (uint256 amountMinted); /** - * @notice Mints UBI as G$ tokens for a given pool by calculating the expansion rate. - * @param exchangeId The ID of the pool to mint UBI for. - * @return amountMinted The amount of G$ tokens minted. + * @notice Mints UBI for the given exchange by calculating the expansion rate. + * @param exchangeId The id of the exchange to mint UBI for. + * @return amountMinted The amount of UBI tokens minted. */ function mintUBIFromExpansion(bytes32 exchangeId) external returns (uint256 amountMinted); /** - * @notice Mints a reward of G$ tokens for a given pool. - * @param exchangeId The ID of the pool to mint a G$ reward for. + * @notice Mints a reward of tokens for the given exchange. + * @param exchangeId The id of the exchange to mint reward. * @param to The address of the recipient. - * @param amount The amount of G$ tokens to mint. + * @param amount The amount of tokens to mint. */ - function mintRewardFromReserveRatio(bytes32 exchangeId, address to, uint256 amount) external; + function mintRewardFromRR(bytes32 exchangeId, address to, uint256 amount) external; } diff --git a/contracts/swap/Broker.sol b/contracts/swap/Broker.sol index 24cdd43e..b6365c8d 100644 --- a/contracts/swap/Broker.sol +++ b/contracts/swap/Broker.sol @@ -14,6 +14,7 @@ import { ITradingLimits } from "../interfaces/ITradingLimits.sol"; import { TradingLimits } from "../libraries/TradingLimits.sol"; import { Initializable } from "celo/contracts/common/Initializable.sol"; +// TODO: ensure we can use latest impl of openzeppelin import { ReentrancyGuard } from "openzeppelin-contracts-next/contracts/security/ReentrancyGuard.sol"; interface IERC20Metadata { @@ -34,16 +35,12 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar /* ==================== State Variables ==================== */ - /// @inheritdoc IBroker address[] public exchangeProviders; - - /// @inheritdoc IBroker mapping(address => bool) public isExchangeProvider; mapping(bytes32 => ITradingLimits.State) public tradingLimitsState; mapping(bytes32 => ITradingLimits.Config) public tradingLimitsConfig; - // Deprecated address of the reserve. Kept to keep storage layout consistent with previous versions. - // slither-disable-next-line constable-states + // Address of the reserve. uint256 public __deprecated0; // prev: IReserve public reserve; uint256 private constant MAX_INT256 = uint256(type(int256).max); @@ -57,7 +54,11 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar */ constructor(bool test) Initializable(test) {} - /// @inheritdoc IBroker + /** + * @notice Allows the contract to be upgradable via the proxy. + * @param _exchangeProviders The addresses of the ExchangeProvider contracts. + * @param _reserves The addresses of the Reserve contracts. + */ function initialize(address[] calldata _exchangeProviders, address[] calldata _reserves) external initializer { _transferOwnership(msg.sender); for (uint256 i = 0; i < _exchangeProviders.length; i++) { @@ -65,7 +66,11 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar } } - /// @inheritdoc IBroker + /** + * @notice Set the reserves for the exchange providers. + * @param _exchangeProviders The addresses of the ExchangeProvider contracts. + * @param _reserves The addresses of the Reserve contracts. + */ function setReserves( address[] calldata _exchangeProviders, address[] calldata _reserves @@ -80,7 +85,12 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar /* ==================== Mutative Functions ==================== */ - /// @inheritdoc IBroker + /** + * @notice Add an exchange provider to the list of providers. + * @param exchangeProvider The address of the exchange provider to add. + * @param reserve The address of the reserve used by the exchange provider. + * @return index The index of the newly added specified exchange provider. + */ function addExchangeProvider( address exchangeProvider, address reserve @@ -96,7 +106,11 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar index = exchangeProviders.length - 1; } - /// @inheritdoc IBroker + /** + * @notice Remove an exchange provider from the list of providers. + * @param exchangeProvider The address of the exchange provider to remove. + * @param index The index of the exchange provider being removed. + */ function removeExchangeProvider( address exchangeProvider, uint256 index @@ -109,7 +123,15 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar emit ExchangeProviderRemoved(exchangeProvider); } - /// @inheritdoc IBroker + /** + * @notice Calculate the amount of tokenIn to be sold for a given amountOut of tokenOut + * @param exchangeProvider the address of the exchange manager for the pair + * @param exchangeId The id of the exchange to use + * @param tokenIn The token to be sold + * @param tokenOut The token to be bought + * @param amountOut The amount of tokenOut to be bought + * @return amountIn The amount of tokenIn to be sold + */ function getAmountIn( address exchangeProvider, bytes32 exchangeId, @@ -118,14 +140,18 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar uint256 amountOut ) external view returns (uint256 amountIn) { require(isExchangeProvider[exchangeProvider], "ExchangeProvider does not exist"); - address reserve = exchangeReserve[exchangeProvider]; - if (IReserve(reserve).isCollateralAsset(tokenOut)) { - require(IERC20(tokenOut).balanceOf(reserve) >= amountOut, "Insufficient balance in reserve"); - } amountIn = IExchangeProvider(exchangeProvider).getAmountIn(exchangeId, tokenIn, tokenOut, amountOut); } - /// @inheritdoc IBroker + /** + * @notice Calculate the amount of tokenOut to be bought for a given amount of tokenIn to be sold + * @param exchangeProvider the address of the exchange manager for the pair + * @param exchangeId The id of the exchange to use + * @param tokenIn The token to be sold + * @param tokenOut The token to be bought + * @param amountIn The amount of tokenIn to be sold + * @return amountOut The amount of tokenOut to be bought + */ function getAmountOut( address exchangeProvider, bytes32 exchangeId, @@ -135,13 +161,18 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar ) external view returns (uint256 amountOut) { require(isExchangeProvider[exchangeProvider], "ExchangeProvider does not exist"); amountOut = IExchangeProvider(exchangeProvider).getAmountOut(exchangeId, tokenIn, tokenOut, amountIn); - address reserve = exchangeReserve[exchangeProvider]; - if (IReserve(reserve).isCollateralAsset(tokenOut)) { - require(IERC20(tokenOut).balanceOf(reserve) >= amountOut, "Insufficient balance in reserve"); - } } - /// @inheritdoc IBroker + /** + * @notice Execute a token swap with fixed amountIn. + * @param exchangeProvider the address of the exchange provider for the pair. + * @param exchangeId The id of the exchange to use. + * @param tokenIn The token to be sold. + * @param tokenOut The token to be bought. + * @param amountIn The amount of tokenIn to be sold. + * @param amountOutMin Minimum amountOut to be received - controls slippage. + * @return amountOut The amount of tokenOut to be bought. + */ function swapIn( address exchangeProvider, bytes32 exchangeId, @@ -162,7 +193,16 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar emit Swap(exchangeProvider, exchangeId, msg.sender, tokenIn, tokenOut, amountIn, amountOut); } - /// @inheritdoc IBroker + /** + * @notice Execute a token swap with fixed amountOut. + * @param exchangeProvider the address of the exchange provider for the pair. + * @param exchangeId The id of the exchange to use. + * @param tokenIn The token to be sold. + * @param tokenOut The token to be bought. + * @param amountOut The amount of tokenOut to be bought. + * @param amountInMax Maximum amount of tokenIn that can be traded. + * @return amountIn The amount of tokenIn to be sold. + */ function swapOut( address exchangeProvider, bytes32 exchangeId, @@ -183,19 +223,35 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar emit Swap(exchangeProvider, exchangeId, msg.sender, tokenIn, tokenOut, amountIn, amountOut); } - /// @inheritdoc IBroker + /** + * @notice Permissionless way to burn stables from msg.sender directly. + * @param token The token getting burned. + * @param amount The amount of the token getting burned. + * @return True if transaction succeeds. + */ function burnStableTokens(address token, uint256 amount) public returns (bool) { IERC20(token).safeTransferFrom(msg.sender, address(this), amount); IERC20(token).safeBurn(amount); return true; } - /// @inheritdoc IBroker + /** + * @notice Configure trading limits for an (exchangeId, token) touple. + * @dev Will revert if the configuration is not valid according to the + * TradingLimits library. + * Resets existing state according to the TradingLimits library logic. + * Can only be called by owner. + * @param exchangeId the exchangeId to target. + * @param token the token to target. + * @param config the new trading limits config. + */ + // TODO: Make this external with next update. + // slither-disable-next-line external-function function configureTradingLimit( bytes32 exchangeId, address token, ITradingLimits.Config memory config - ) external onlyOwner { + ) public onlyOwner { config.validate(); bytes32 limitId = exchangeId ^ bytes32(uint256(uint160(token))); diff --git a/package.json b/package.json index 6406d575..502ac44d 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "fork-test:celo-mainnet": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract Celo", "check-no-ir": "./bin/check-contracts.sh", "check-contract-sizes": "env FOUNDRY_PROFILE=optimized forge build --sizes --skip \"test/**/*\"", - "slither": "slither .", "todo": "git ls-files -c --exclude-standard | grep -v \"package.json\" | xargs -I {} sh -c 'grep -H -n -i --color \"TODO:\\|FIXME:\" \"{}\" 2>/dev/null || true'" }, "dependencies": { diff --git a/test/fork/BaseForkTest.sol b/test/fork/BaseForkTest.sol index 4a2a5eaf..ab4afbb8 100644 --- a/test/fork/BaseForkTest.sol +++ b/test/fork/BaseForkTest.sol @@ -9,6 +9,7 @@ import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; import { IRegistry } from "celo/contracts/common/interfaces/IRegistry.sol"; import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; +import { IBroker } from "contracts/interfaces/IBroker.sol"; import { Broker } from "contracts/swap/Broker.sol"; import { IReserve } from "contracts/interfaces/IReserve.sol"; import { ISortedOracles } from "contracts/interfaces/ISortedOracles.sol"; diff --git a/test/integration/protocol/GoodDollarIntegration.t.sol b/test/integration/protocol/GoodDollarIntegration.t.sol index b5f1315f..072840b0 100644 --- a/test/integration/protocol/GoodDollarIntegration.t.sol +++ b/test/integration/protocol/GoodDollarIntegration.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.18; pragma experimental ABIEncoderV2; import { Test } from "mento-std/Test.sol"; +import { console } from "forge-std/console.sol"; import { GoodDollarExchangeProvider } from "contracts/goodDollar/GoodDollarExchangeProvider.sol"; import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarExpansionController.sol"; @@ -140,7 +141,6 @@ contract GoodDollarIntegrationTest is Test { exitContribution: 0.01 * 1e8 }); - vm.prank(avatar); exchangeId = IBancorExchangeProvider(address(exchangeProvider)).createExchange(poolExchange1); } @@ -155,6 +155,7 @@ contract GoodDollarIntegrationTest is Test { // @notice manual minting of GD through avatar because foundry deal crashes on GoodDollar contract function mintGoodDollar(uint256 amount, address to) public { + console.log("mintGoodDollar"); vm.prank(GoodDollarAvatar); gdToken.mint(to, amount); } @@ -193,6 +194,7 @@ contract GoodDollarIntegrationTest is Test { } function test_SwapIn_gDollarToReserveToken() public { + console.log("test_SwapIn_gDollarToReserveToken"); uint256 amountIn = 1000 * 1e18; uint256 reserveBalanceBefore = reserveToken.balanceOf(address(reserve)); diff --git a/test/unit/goodDollar/BancorExchangeProvider.t.sol b/test/unit/goodDollar/BancorExchangeProvider.t.sol index 24c1e0d2..0ffd6ebc 100644 --- a/test/unit/goodDollar/BancorExchangeProvider.t.sol +++ b/test/unit/goodDollar/BancorExchangeProvider.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, max-line-length // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -import { Test } from "forge-std/Test.sol"; +import { Test, console } from "forge-std/Test.sol"; import { ERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/ERC20.sol"; import { BancorExchangeProvider } from "contracts/goodDollar/BancorExchangeProvider.sol"; import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; @@ -169,7 +169,7 @@ contract BancorExchangeProviderTest_initilizerSettersGetters is BancorExchangePr bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint32 maxWeight = bancorExchangeProvider.MAX_WEIGHT(); - vm.expectRevert("Exit contribution is too high"); + vm.expectRevert("Invalid exit contribution"); bancorExchangeProvider.setExitContribution(exchangeId, maxWeight + 1); } @@ -189,7 +189,7 @@ contract BancorExchangeProviderTest_initilizerSettersGetters is BancorExchangePr function test_getPoolExchange_whenExchangeDoesNotExist_shouldRevert() public { bytes32 exchangeId = "0xexchangeId"; - vm.expectRevert("Exchange does not exist"); + vm.expectRevert("An exchange with the specified id does not exist"); bancorExchangeProvider.getPoolExchange(exchangeId); } @@ -260,7 +260,7 @@ contract BancorExchangeProviderTest_createExchange is BancorExchangeProviderTest abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken)), abi.encode(false) ); - vm.expectRevert("Reserve asset must be a collateral registered with the reserve"); + vm.expectRevert("reserve asset must be a collateral registered with the reserve"); bancorExchangeProvider.createExchange(poolExchange1); } @@ -276,28 +276,28 @@ contract BancorExchangeProviderTest_createExchange is BancorExchangeProviderTest abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token)), abi.encode(false) ); - vm.expectRevert("Token must be a stable registered with the reserve"); + vm.expectRevert("token must be a stable registered with the reserve"); bancorExchangeProvider.createExchange(poolExchange1); } function test_createExchange_whenReserveRatioIsSmaller2_shouldRevert() public { poolExchange1.reserveRatio = 0; - vm.expectRevert("Reserve ratio is too low"); + vm.expectRevert("Invalid reserve ratio"); bancorExchangeProvider.createExchange(poolExchange1); poolExchange1.reserveRatio = 1; - vm.expectRevert("Reserve ratio is too low"); + vm.expectRevert("Invalid reserve ratio"); bancorExchangeProvider.createExchange(poolExchange1); } function test_createExchange_whenReserveRatioAbove100Percent_shouldRevert() public { poolExchange1.reserveRatio = bancorExchangeProvider.MAX_WEIGHT() + 1; - vm.expectRevert("Reserve ratio is too high"); + vm.expectRevert("Invalid reserve ratio"); bancorExchangeProvider.createExchange(poolExchange1); } function test_createExchange_whenExitContributionAbove100Percent_shouldRevert() public { poolExchange1.exitContribution = bancorExchangeProvider.MAX_WEIGHT() + 1; - vm.expectRevert("Exit contribution is too high"); + vm.expectRevert("Invalid exit contribution"); bancorExchangeProvider.createExchange(poolExchange1); } @@ -309,13 +309,13 @@ contract BancorExchangeProviderTest_createExchange is BancorExchangeProviderTest function test_createExchanges_whenReserveTokenHasMoreDecimalsThan18_shouldRevert() public { vm.mockCall(address(reserveToken), abi.encodeWithSelector(reserveToken.decimals.selector), abi.encode(19)); - vm.expectRevert("Reserve asset decimals must be <= 18"); + vm.expectRevert("reserve token decimals must be <= 18"); bancorExchangeProvider.createExchange(poolExchange1); } function test_createExchange_whenTokenHasMoreDecimalsThan18_shouldRevert() public { vm.mockCall(address(token), abi.encodeWithSelector(token.decimals.selector), abi.encode(19)); - vm.expectRevert("Token decimals must be <= 18"); + vm.expectRevert("token decimals must be <= 18"); bancorExchangeProvider.createExchange(poolExchange1); } @@ -398,8 +398,13 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { function test_getAmountIn_whenExchangeDoesNotExist_shouldRevert() public { bytes32 exchangeId = "0xexchangeId"; - vm.expectRevert("Exchange does not exist"); - bancorExchangeProvider.getAmountIn(exchangeId, address(reserveToken), address(token), 1e18); + vm.expectRevert("An exchange with the specified id does not exist"); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: 1e18 + }); } function test_getAmountIn_whenTokenInNotInExchange_shouldRevert() public { @@ -940,8 +945,13 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { function test_getAmountOut_whenExchangeDoesNotExist_shouldRevert() public { bytes32 exchangeId = "0xexchangeId"; - vm.expectRevert("Exchange does not exist"); - bancorExchangeProvider.getAmountOut(exchangeId, address(reserveToken), address(token), 1e18); + vm.expectRevert("An exchange with the specified id does not exist"); + bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: 1e18 + }); } function test_getAmountOut_whenTokenInNotInExchange_shouldRevert() public { @@ -1517,7 +1527,7 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { function test_swapIn_whenExchangeDoesNotExist_shouldRevert() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); vm.prank(brokerAddress); - vm.expectRevert("Exchange does not exist"); + vm.expectRevert("An exchange with the specified id does not exist"); bancorExchangeProvider.swapIn("0xexchangeId", address(reserveToken), address(token), 1e18); } @@ -1606,7 +1616,7 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { function test_swapOut_whenExchangeDoesNotExist_shouldRevert() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); vm.prank(brokerAddress); - vm.expectRevert("Exchange does not exist"); + vm.expectRevert("An exchange with the specified id does not exist"); bancorExchangeProvider.swapOut("0xexchangeId", address(reserveToken), address(token), 1e18); } diff --git a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol index 9a6dae10..af6226d4 100644 --- a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol +++ b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -import { Test } from "forge-std/Test.sol"; +import { Test, console } from "forge-std/Test.sol"; import { GoodDollarExchangeProvider } from "contracts/goodDollar/GoodDollarExchangeProvider.sol"; import { ERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/ERC20.sol"; @@ -14,18 +14,14 @@ import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangePro contract GoodDollarExchangeProviderTest is Test { /* ------- Events from IGoodDollarExchangeProvider ------- */ + event ExchangeCreated(bytes32 indexed exchangeId, address indexed reserveAsset, address indexed tokenAddress); + event ExpansionControllerUpdated(address indexed expansionController); event AvatarUpdated(address indexed AVATAR); event ReserveRatioUpdated(bytes32 indexed exchangeId, uint32 reserveRatio); - event ExchangeCreated(bytes32 indexed exchangeId, address indexed reserveAsset, address indexed tokenAddress); - - event ExchangeDestroyed(bytes32 indexed exchangeId, address indexed reserveAsset, address indexed tokenAddress); - - event ExitContributionSet(bytes32 indexed exchangeId, uint256 exitContribution); - /* ------------------------------------------- */ ERC20 public reserveToken; @@ -38,7 +34,6 @@ contract GoodDollarExchangeProviderTest is Test { address public expansionControllerAddress; IBancorExchangeProvider.PoolExchange public poolExchange1; - IBancorExchangeProvider.PoolExchange public poolExchange2; function setUp() public virtual { reserveToken = new ERC20("cUSD", "cUSD"); @@ -59,15 +54,6 @@ contract GoodDollarExchangeProviderTest is Test { exitContribution: 0.01 * 1e8 }); - poolExchange2 = IBancorExchangeProvider.PoolExchange({ - reserveAsset: address(reserveToken), - tokenAddress: address(token2), - tokenSupply: 300_000 * 1e18, - reserveBalance: 60_000 * 1e18, - reserveRatio: 1e8 * 0.2, - exitContribution: 1e8 * 0.01 - }); - vm.mockCall( reserveAddress, abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token)), @@ -152,139 +138,6 @@ contract GoodDollarExchangeProviderTest_initializerSettersGetters is GoodDollarE assertEq(address(exchangeProvider.expansionController()), newExpansionController); } - - /* ---------- setExitContribution ---------- */ - /* Focuses only on access control, implementation details are covered in BancorExchangeProvider tests */ - function test_setExitContribution_whenSenderIsOwner_shouldRevert() public { - vm.expectRevert("Only Avatar can call this function"); - bytes32 exchangeId = "0xexchangeId"; - exchangeProvider.setExitContribution(exchangeId, 1e5); - } - - function test_setExitContribution_whenSenderIsNotAvatar_shouldRevert() public { - vm.startPrank(makeAddr("NotAvatarAndNotOwner")); - vm.expectRevert("Only Avatar can call this function"); - bytes32 exchangeId = "0xexchangeId"; - exchangeProvider.setExitContribution(exchangeId, 1e5); - vm.stopPrank(); - } - - function test_setExitContribution_whenSenderIsAvatar_shouldUpdateAndEmit() public { - vm.startPrank(avatarAddress); - bytes32 exchangeId = exchangeProvider.createExchange(poolExchange1); - uint32 newExitContribution = 1e3; - vm.expectEmit(true, true, true, true); - emit ExitContributionSet(exchangeId, newExitContribution); - exchangeProvider.setExitContribution(exchangeId, newExitContribution); - - IBancorExchangeProvider.PoolExchange memory poolExchange = exchangeProvider.getPoolExchange(exchangeId); - assertEq(poolExchange.exitContribution, newExitContribution); - vm.stopPrank(); - } - /* ---------- setExitContribution end ---------- */ -} - -/** - * @notice createExchange tests - * @dev These tests focus only on access control. The implementation details - * are covered in the BancorExchangeProvider tests. - */ -contract GoodDollarExchangeProviderTest_createExchange is GoodDollarExchangeProviderTest { - GoodDollarExchangeProvider exchangeProvider; - - function setUp() public override { - super.setUp(); - exchangeProvider = initializeGoodDollarExchangeProvider(); - } - - function test_createExchange_whenSenderIsNotAvatar_shouldRevert() public { - vm.prank(makeAddr("NotAvatar")); - vm.expectRevert("Only Avatar can call this function"); - exchangeProvider.createExchange(poolExchange1); - } - - function test_createExchange_whenSenderIsOwner_shouldRevert() public { - vm.expectRevert("Only Avatar can call this function"); - exchangeProvider.createExchange(poolExchange1); - } - - function test_createExchange_whenSenderIsAvatar_shouldCreateExchangeAndEmit() public { - vm.startPrank(avatarAddress); - vm.expectEmit(true, true, true, true); - bytes32 expectedExchangeId = keccak256(abi.encodePacked(reserveToken.symbol(), token.symbol())); - emit ExchangeCreated(expectedExchangeId, address(reserveToken), address(token)); - bytes32 exchangeId = exchangeProvider.createExchange(poolExchange1); - assertEq(exchangeId, expectedExchangeId); - - IBancorExchangeProvider.PoolExchange memory poolExchange = exchangeProvider.getPoolExchange(exchangeId); - assertEq(poolExchange.reserveAsset, poolExchange1.reserveAsset); - assertEq(poolExchange.tokenAddress, poolExchange1.tokenAddress); - assertEq(poolExchange.tokenSupply, poolExchange1.tokenSupply); - assertEq(poolExchange.reserveBalance, poolExchange1.reserveBalance); - assertEq(poolExchange.reserveRatio, poolExchange1.reserveRatio); - assertEq(poolExchange.exitContribution, poolExchange1.exitContribution); - - IExchangeProvider.Exchange[] memory exchanges = exchangeProvider.getExchanges(); - assertEq(exchanges.length, 1); - assertEq(exchanges[0].exchangeId, exchangeId); - - assertEq(exchangeProvider.tokenPrecisionMultipliers(address(reserveToken)), 1); - assertEq(exchangeProvider.tokenPrecisionMultipliers(address(token)), 1); - vm.stopPrank(); - } -} - -/** - * @notice destroyExchange tests - * @dev These tests focus only on access control. The implementation details - * are covered in the BancorExchangeProvider tests. - */ -contract GoodDollarExchangeProviderTest_destroyExchange is GoodDollarExchangeProviderTest { - GoodDollarExchangeProvider exchangeProvider; - - function setUp() public override { - super.setUp(); - exchangeProvider = initializeGoodDollarExchangeProvider(); - } - - function test_destroyExchange_whenSenderIsOwner_shouldRevert() public { - vm.startPrank(avatarAddress); - bytes32 exchangeId = exchangeProvider.createExchange(poolExchange1); - vm.stopPrank(); - vm.expectRevert("Only Avatar can call this function"); - exchangeProvider.destroyExchange(exchangeId, 0); - } - - function test_destroyExchange_whenSenderIsNotAvatar_shouldRevert() public { - vm.startPrank(avatarAddress); - bytes32 exchangeId = exchangeProvider.createExchange(poolExchange1); - vm.stopPrank(); - - vm.startPrank(makeAddr("NotAvatar")); - vm.expectRevert("Only Avatar can call this function"); - exchangeProvider.destroyExchange(exchangeId, 0); - vm.stopPrank(); - } - - function test_destroyExchange_whenExchangeExists_shouldDestroyExchangeAndEmit() public { - vm.startPrank(avatarAddress); - bytes32 exchangeId = exchangeProvider.createExchange(poolExchange1); - bytes32 exchangeId2 = exchangeProvider.createExchange(poolExchange2); - vm.stopPrank(); - - vm.startPrank(avatarAddress); - vm.expectEmit(true, true, true, true); - emit ExchangeDestroyed(exchangeId, poolExchange1.reserveAsset, poolExchange1.tokenAddress); - exchangeProvider.destroyExchange(exchangeId, 0); - - bytes32[] memory exchangeIds = exchangeProvider.getExchangeIds(); - assertEq(exchangeIds.length, 1); - - IExchangeProvider.Exchange[] memory exchanges = exchangeProvider.getExchanges(); - assertEq(exchanges.length, 1); - assertEq(exchanges[0].exchangeId, exchangeId2); - vm.stopPrank(); - } } contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeProviderTest { @@ -296,7 +149,6 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP super.setUp(); expansionRate = 1e18 * 0.99; exchangeProvider = initializeGoodDollarExchangeProvider(); - vm.prank(avatarAddress); exchangeId = exchangeProvider.createExchange(poolExchange1); } @@ -314,7 +166,7 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP function test_mintFromExpansion_whenExchangeIdIsInvalid_shouldRevert() public { vm.prank(expansionControllerAddress); - vm.expectRevert("Exchange does not exist"); + vm.expectRevert("An exchange with the specified id does not exist"); exchangeProvider.mintFromExpansion(bytes32(0), expansionRate); } @@ -362,7 +214,6 @@ contract GoodDollarExchangeProviderTest_mintFromInterest is GoodDollarExchangePr super.setUp(); reserveInterest = 1000 * 1e18; exchangeProvider = initializeGoodDollarExchangeProvider(); - vm.prank(avatarAddress); exchangeId = exchangeProvider.createExchange(poolExchange1); } @@ -374,7 +225,7 @@ contract GoodDollarExchangeProviderTest_mintFromInterest is GoodDollarExchangePr function test_mintFromInterest_whenExchangeIdIsInvalid_shouldRevert() public { vm.prank(expansionControllerAddress); - vm.expectRevert("Exchange does not exist"); + vm.expectRevert("An exchange with the specified id does not exist"); exchangeProvider.mintFromInterest(bytes32(0), reserveInterest); } @@ -419,7 +270,6 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan super.setUp(); reward = 1000 * 1e18; exchangeProvider = initializeGoodDollarExchangeProvider(); - vm.prank(avatarAddress); exchangeId = exchangeProvider.createExchange(poolExchange1); } @@ -431,7 +281,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan function test_updateRatioForReward_whenExchangeIdIsInvalid_shouldRevert() public { vm.prank(expansionControllerAddress); - vm.expectRevert("Exchange does not exist"); + vm.expectRevert("An exchange with the specified id does not exist"); exchangeProvider.updateRatioForReward(bytes32(0), reward); } @@ -458,7 +308,6 @@ contract GoodDollarExchangeProviderTest_pausable is GoodDollarExchangeProviderTe function setUp() public override { super.setUp(); exchangeProvider = initializeGoodDollarExchangeProvider(); - vm.prank(avatarAddress); exchangeId = exchangeProvider.createExchange(poolExchange1); } diff --git a/test/unit/goodDollar/GoodDollarExpansionController.t.sol b/test/unit/goodDollar/GoodDollarExpansionController.t.sol index 739c8961..ae055d3b 100644 --- a/test/unit/goodDollar/GoodDollarExpansionController.t.sol +++ b/test/unit/goodDollar/GoodDollarExpansionController.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -import { Test } from "forge-std/Test.sol"; +import { Test, console } from "forge-std/Test.sol"; import { ERC20Mock } from "openzeppelin-contracts-next/contracts/mocks/ERC20Mock.sol"; import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarExpansionController.sol"; @@ -23,7 +23,7 @@ contract GoodDollarExpansionControllerTest is Test { event AvatarUpdated(address indexed avatar); - event ExpansionConfigSet(bytes32 indexed exchangeId, uint64 expansionRate, uint32 expansionFrequency); + event ExpansionConfigSet(bytes32 indexed exchangeId, uint64 expansionRate, uint32 expansionfrequency); event RewardMinted(bytes32 indexed exchangeId, address indexed to, uint256 amount); @@ -124,33 +124,24 @@ contract GoodDollarExpansionControllerTest_initializerSettersGetters is GoodDoll assertEq(address(expansionController.goodDollarExchangeProvider()), newExchangeProvider); } - function test_setDistributionHelper_whenCallerIsNotAvatar_shouldRevert() public { - vm.prank(makeAddr("NotAvatar")); - vm.expectRevert("Only Avatar can call this function"); - expansionController.setDistributionHelper(makeAddr("NewDistributionHelper")); - } - - function test_setDistributionHelper_whenCallerIsOwner_shouldRevert() public { - vm.expectRevert("Only Avatar can call this function"); + function test_setDistributionHelper_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); expansionController.setDistributionHelper(makeAddr("NewDistributionHelper")); } - function test_setDistributionHelper_whenAddressIsZero_shouldRevert() public { - vm.startPrank(avatarAddress); - vm.expectRevert("Distribution helper address must be set"); + function test_setDistributiomHelper_whenAddressIsZero_shouldRevert() public { + vm.expectRevert("DistributionHelper address must be set"); expansionController.setDistributionHelper(address(0)); - vm.stopPrank(); } - function test_setDistributionHelper_whenCallerIsAvatar_shouldUpdateAndEmit() public { - vm.startPrank(avatarAddress); + function test_setDistributionHelper_whenCallerIsOwner_shouldUpdateAndEmit() public { address newDistributionHelper = makeAddr("NewDistributionHelper"); vm.expectEmit(true, true, true, true); emit DistributionHelperUpdated(newDistributionHelper); expansionController.setDistributionHelper(newDistributionHelper); assertEq(address(expansionController.distributionHelper()), newDistributionHelper); - vm.stopPrank(); } function test_setReserve_whenSenderIsNotOwner_shouldRevert() public { @@ -271,7 +262,7 @@ contract GoodDollarExpansionControllerTest_mintUBIFromInterest is GoodDollarExpa } function test_mintUBIFromInterest_whenReserveInterestIs0_shouldRevert() public { - vm.expectRevert("Reserve interest must be greater than 0"); + vm.expectRevert("reserveInterest must be greater than 0"); expansionController.mintUBIFromInterest(exchangeId, 0); } @@ -564,7 +555,7 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp } } -contract GoodDollarExpansionControllerTest_mintRewardFromReserveRatio is GoodDollarExpansionControllerTest { +contract GoodDollarExpansionControllerTest_mintRewardFromRR is GoodDollarExpansionControllerTest { GoodDollarExpansionController expansionController; function setUp() public override { @@ -595,25 +586,25 @@ contract GoodDollarExpansionControllerTest_mintRewardFromReserveRatio is GoodDol ); } - function test_mintRewardFromReserveRatio_whenCallerIsNotAvatar_shouldRevert() public { + function test_mintRewardFromRR_whenCallerIsNotAvatar_shouldRevert() public { vm.prank(makeAddr("NotAvatar")); vm.expectRevert("Only Avatar can call this function"); - expansionController.mintRewardFromReserveRatio(exchangeId, makeAddr("To"), 1000e18); + expansionController.mintRewardFromRR(exchangeId, makeAddr("To"), 1000e18); } - function test_mintRewardFromReserveRatio_whenToIsZero_shouldRevert() public { + function test_mintRewardFromRR_whenToIsZero_shouldRevert() public { vm.prank(avatarAddress); - vm.expectRevert("Recipient address must be set"); - expansionController.mintRewardFromReserveRatio(exchangeId, address(0), 1000e18); + vm.expectRevert("Invalid to address"); + expansionController.mintRewardFromRR(exchangeId, address(0), 1000e18); } - function test_mintRewardFromReserveRatio_whenAmountIs0_shouldRevert() public { + function test_mintRewardFromRR_whenAmountIs0_shouldRevert() public { vm.prank(avatarAddress); vm.expectRevert("Amount must be greater than 0"); - expansionController.mintRewardFromReserveRatio(exchangeId, makeAddr("To"), 0); + expansionController.mintRewardFromRR(exchangeId, makeAddr("To"), 0); } - function test_mintRewardFromReserveRatio_whenCallerIsAvatar_shouldMintAndEmit() public { + function test_mintRewardFromRR_whenCallerIsAvatar_shouldMintAndEmit() public { uint256 amountToMint = 1000e18; address to = makeAddr("To"); uint256 toBalanceBefore = token.balanceOf(to); @@ -622,7 +613,7 @@ contract GoodDollarExpansionControllerTest_mintRewardFromReserveRatio is GoodDol emit RewardMinted(exchangeId, to, amountToMint); vm.prank(avatarAddress); - expansionController.mintRewardFromReserveRatio(exchangeId, to, amountToMint); + expansionController.mintRewardFromRR(exchangeId, to, amountToMint); assertEq(token.balanceOf(to), toBalanceBefore + amountToMint); } diff --git a/test/unit/swap/Broker.t.sol b/test/unit/swap/Broker.t.sol index 98a57893..91ca31b8 100644 --- a/test/unit/swap/Broker.t.sol +++ b/test/unit/swap/Broker.t.sol @@ -225,135 +225,27 @@ contract BrokerTest_getAmounts is BrokerTest { function test_getAmountIn_whenExchangeProviderWasNotSet_shouldRevert() public { vm.expectRevert("ExchangeProvider does not exist"); - broker.getAmountIn({ - exchangeProvider: randomExchangeProvider, - exchangeId: exchangeId, - tokenIn: address(stableAsset), - tokenOut: address(collateralAsset), - amountOut: 1e24 - }); - } - - function test_getAmountIn_whenReserveBalanceIsLessThanAmountOut_shouldRevert() public { - assertEq(collateralAsset.balanceOf(address(reserve)), 0); - vm.expectRevert("Insufficient balance in reserve"); - broker.getAmountIn({ - exchangeProvider: address(exchangeProvider), - exchangeId: exchangeId, - tokenIn: address(stableAsset), - tokenOut: address(collateralAsset), - amountOut: 1e24 - }); - } - - function test_getAmountIn_whenReserveBalanceIsEqualToAmountOut_shouldReturnAmountIn() public { - uint256 amountOut = 1e18; - collateralAsset.mint(address(reserve), amountOut); - - uint256 amountIn = broker.getAmountIn({ - exchangeProvider: address(exchangeProvider), - exchangeId: exchangeId, - tokenIn: address(stableAsset), - tokenOut: address(collateralAsset), - amountOut: amountOut - }); - - assertEq(amountIn, 25e17); - } - - function test_getAmountIn_whenReserveBalanceIsLargerThanAmountOut_shouldReturnAmountIn() public { - uint256 amountOut = 1e18; - collateralAsset.mint(address(reserve), 1000e18); - - uint256 amountIn = broker.getAmountIn({ - exchangeProvider: address(exchangeProvider), - exchangeId: exchangeId, - tokenIn: address(stableAsset), - tokenOut: address(collateralAsset), - amountOut: amountOut - }); - - assertEq(amountIn, 25e17); + broker.getAmountIn(randomExchangeProvider, exchangeId, address(stableAsset), address(collateralAsset), 1e24); } - function test_getAmountIn_whenExchangeProviderIsSet_shouldReceiveCall() public { - collateralAsset.mint(address(reserve), 1000e18); - vm.expectCall( + function test_getAmountIn_whenExchangeProviderIsSet_shouldReceiveCall() public view { + uint256 amountIn = broker.getAmountIn( address(exchangeProvider), - abi.encodeWithSelector( - exchangeProvider.getAmountIn.selector, - exchangeId, - address(stableAsset), - address(collateralAsset), - 1e18 - ) + exchangeId, + address(stableAsset), + address(collateralAsset), + 1e18 ); - uint256 amountIn = broker.getAmountIn({ - exchangeProvider: address(exchangeProvider), - exchangeId: exchangeId, - tokenIn: address(stableAsset), - tokenOut: address(collateralAsset), - amountOut: 1e18 - }); assertEq(amountIn, 25e17); } function test_getAmountOut_whenExchangeProviderWasNotSet_shouldRevert() public { vm.expectRevert("ExchangeProvider does not exist"); - broker.getAmountOut({ - exchangeProvider: randomExchangeProvider, - exchangeId: exchangeId, - tokenIn: randomAsset, - tokenOut: randomAsset, - amountIn: 1e24 - }); - } - - function test_getAmountOut_whenReserveBalanceIsLessThanAmountOut_shouldRevert() public { - assertEq(collateralAsset.balanceOf(address(reserve)), 0); - vm.expectRevert("Insufficient balance in reserve"); - broker.getAmountOut({ - exchangeProvider: address(exchangeProvider), - exchangeId: exchangeId, - tokenIn: address(stableAsset), - tokenOut: address(collateralAsset), - amountIn: 1e24 - }); - } - - function test_getAmountOut_whenReserveBalanceIsEqualAmountOut_shouldReturnAmountIn() public { - uint256 amountIn = 1e18; - collateralAsset.mint(address(reserve), amountIn); - - uint256 amountOut = broker.getAmountOut({ - exchangeProvider: address(exchangeProvider), - exchangeId: exchangeId, - tokenIn: address(stableAsset), - tokenOut: address(collateralAsset), - amountIn: amountIn - }); - - assertEq(amountOut, 4e17); - } - - function test_getAmountOut_whenReserveBalanceIsLargerThanAmountOut_shouldReturnAmountIn() public { - uint256 amountIn = 1e18; - collateralAsset.mint(address(reserve), 1000e18); - - uint256 amountOut = broker.getAmountOut({ - exchangeProvider: address(exchangeProvider), - exchangeId: exchangeId, - tokenIn: address(stableAsset), - tokenOut: address(collateralAsset), - amountIn: amountIn - }); - - assertEq(amountOut, 4e17); + broker.getAmountOut(randomExchangeProvider, exchangeId, randomAsset, randomAsset, 1e24); } function test_getAmountOut_whenExchangeProviderIsSet_shouldReceiveCall() public { - collateralAsset.mint(address(reserve), 1000e18); vm.expectCall( address(exchangeProvider), abi.encodeWithSelector( @@ -365,13 +257,13 @@ contract BrokerTest_getAmounts is BrokerTest { ) ); - uint256 amountOut = broker.getAmountOut({ - exchangeProvider: address(exchangeProvider), - exchangeId: exchangeId, - tokenIn: address(stableAsset), - tokenOut: address(collateralAsset), - amountIn: 1e18 - }); + uint256 amountOut = broker.getAmountOut( + address(exchangeProvider), + exchangeId, + address(stableAsset), + address(collateralAsset), + 1e18 + ); assertEq(amountOut, 4e17); } } From 5022124ffc1e2ee655bc6a832d775da0623fb6b4 Mon Sep 17 00:00:00 2001 From: Ryan Noble Date: Mon, 21 Oct 2024 11:50:20 +0200 Subject: [PATCH 4/7] feat: update revert for 0 reward --- contracts/goodDollar/GoodDollarExchangeProvider.sol | 4 +--- test/unit/goodDollar/GoodDollarExchangeProvider.t.sol | 6 ++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/contracts/goodDollar/GoodDollarExchangeProvider.sol b/contracts/goodDollar/GoodDollarExchangeProvider.sol index e52cc103..6fcb8cba 100644 --- a/contracts/goodDollar/GoodDollarExchangeProvider.sol +++ b/contracts/goodDollar/GoodDollarExchangeProvider.sol @@ -193,9 +193,7 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan function updateRatioForReward(bytes32 exchangeId, uint256 reward) external onlyExpansionController whenNotPaused { PoolExchange memory exchange = getPoolExchange(exchangeId); - if (reward == 0) { - return; - } + require(reward > 0, "Reward must be greater than 0"); uint256 currentPriceScaled = currentPrice(exchangeId) * tokenPrecisionMultipliers[exchange.reserveAsset]; uint256 rewardScaled = reward * tokenPrecisionMultipliers[exchange.tokenAddress]; diff --git a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol index 9a6dae10..333d3125 100644 --- a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol +++ b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol @@ -449,6 +449,12 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio); assertEq(poolExchangeAfter.tokenSupply, poolExchange1.tokenSupply + reward); } + + function test_updateRatioForReward_whenRewardIs0_shouldRevert() public { + vm.prank(expansionControllerAddress); + vm.expectRevert("Reward must be greater than 0"); + exchangeProvider.updateRatioForReward(exchangeId, 0); + } } contract GoodDollarExchangeProviderTest_pausable is GoodDollarExchangeProviderTest { From c30d05defb2f181d98310f7104968394de73a25c Mon Sep 17 00:00:00 2001 From: philbow61 <80156619+philbow61@users.noreply.github.com> Date: Tue, 22 Oct 2024 03:05:20 +0200 Subject: [PATCH 5/7] Apply suggestions from code review Co-authored-by: baroooo --- test/unit/goodDollar/BancorExchangeProvider.t.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/goodDollar/BancorExchangeProvider.t.sol b/test/unit/goodDollar/BancorExchangeProvider.t.sol index 24c1e0d2..0366a48d 100644 --- a/test/unit/goodDollar/BancorExchangeProvider.t.sol +++ b/test/unit/goodDollar/BancorExchangeProvider.t.sol @@ -637,7 +637,7 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { function test_getAmountIn_whenTokenInIsReserveAssetAndAmountOutIsLarge_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - uint256 amountOut = 1000000e18; // 1,000,000 tokens + uint256 amountOut = 1_000_000e18; // formula: amountIn = reserveBalance * ((amountOut/tokenSupply + 1)^(1/reserveRatio) - 1) // calculation: 60_000 * ((1_000_000/300_000 + 1)^(1/0.2) - 1) ≈ 91617283.9506172839506172839 // 1 wei difference due to precision loss @@ -789,7 +789,7 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { amountIn: amountIn }); - // we allow up to 1% difference due to precision loss + // we allow up to 0.01% difference due to precision loss assertApproxEqRel(reversedAmountOut, amountOut, 1e18 * 0.0001); } @@ -1170,7 +1170,7 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { function test_getAmountOut_whenTokenInIsReserveAssetAndAmountInIsLarge_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - uint256 amountIn = 1000000e18; // 1,000,000 reserve tokens + uint256 amountIn = 1_000_000e18; // formula: amountOut = tokenSupply * ((1 + amountIn / reserveBalance) ^ reserveRatio - 1) // calculation: 300_000 * ((1 + 1_000_000 / 60_000) ^ 0.2 - 1) ≈ 232785.231205449318288038 uint256 expectedAmountOut = 232785231205449318288038; From 05c82e536b490e649ce9aa096657b4b334a36bd4 Mon Sep 17 00:00:00 2001 From: philbow61 Date: Tue, 22 Oct 2024 03:20:17 +0200 Subject: [PATCH 6/7] chore: incorperate review feedback --- .../goodDollar/BancorExchangeProvider.t.sol | 78 +++++++++++-------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/test/unit/goodDollar/BancorExchangeProvider.t.sol b/test/unit/goodDollar/BancorExchangeProvider.t.sol index 0366a48d..863849ee 100644 --- a/test/unit/goodDollar/BancorExchangeProvider.t.sol +++ b/test/unit/goodDollar/BancorExchangeProvider.t.sol @@ -399,7 +399,12 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { function test_getAmountIn_whenExchangeDoesNotExist_shouldRevert() public { bytes32 exchangeId = "0xexchangeId"; vm.expectRevert("Exchange does not exist"); - bancorExchangeProvider.getAmountIn(exchangeId, address(reserveToken), address(token), 1e18); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: 1e18 + }); } function test_getAmountIn_whenTokenInNotInExchange_shouldRevert() public { @@ -557,8 +562,8 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { poolExchange1.reserveRatio = 1e8; bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountOut = 12e18; - // formula: amountIn = (amountOut * reserveBalance - 1/1e18 ) / supply + 1/1e18 - // calculation: (12 * 60_000 - 1/1e18) / 300_000 + 1/1e18 = 2.4 + // formula: amountIn = (amountOut * reserveBalance) / supply + // calculation: (12 * 60_000) / 300_000 = 2.4 uint256 expectedAmountIn = 1e18 * 2.4; uint256 amountIn = bancorExchangeProvider.getAmountIn({ @@ -941,7 +946,12 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { function test_getAmountOut_whenExchangeDoesNotExist_shouldRevert() public { bytes32 exchangeId = "0xexchangeId"; vm.expectRevert("Exchange does not exist"); - bancorExchangeProvider.getAmountOut(exchangeId, address(reserveToken), address(token), 1e18); + bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: 1e18 + }); } function test_getAmountOut_whenTokenInNotInExchange_shouldRevert() public { @@ -1300,12 +1310,12 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { // amountIn range between 1 and 10_000_000 tokens amountIn = bound(amountIn, 1e18, 10_000_000 * 1e18); - uint256 amountOut = bancorExchangeProvider.getAmountOut( - exchangeId, - address(reserveToken), - address(token), - amountIn - ); + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); // Basic sanity checks assertTrue(0 < amountOut, "Amount out should be positive"); @@ -1553,12 +1563,12 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { uint256 reserveBalanceBefore = poolExchange1.reserveBalance; uint256 tokenSupplyBefore = poolExchange1.tokenSupply; - uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut( - exchangeId, - address(reserveToken), - address(token), - amountIn - ); + uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); vm.prank(brokerAddress); uint256 amountOut = bancorExchangeProvider.swapIn(exchangeId, address(reserveToken), address(token), amountIn); assertEq(amountOut, expectedAmountOut); @@ -1577,12 +1587,12 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { uint256 reserveBalanceBefore = poolExchange1.reserveBalance; uint256 tokenSupplyBefore = poolExchange1.tokenSupply; - uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut( - exchangeId, - address(token), - address(reserveToken), - amountIn - ); + uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); vm.prank(brokerAddress); uint256 amountOut = bancorExchangeProvider.swapIn(exchangeId, address(token), address(reserveToken), amountIn); assertEq(amountOut, expectedAmountOut); @@ -1642,12 +1652,12 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { uint256 reserveBalanceBefore = poolExchange1.reserveBalance; uint256 tokenSupplyBefore = poolExchange1.tokenSupply; - uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn( - exchangeId, - address(reserveToken), - address(token), - amountOut - ); + uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); vm.prank(brokerAddress); uint256 amountIn = bancorExchangeProvider.swapOut(exchangeId, address(reserveToken), address(token), amountOut); assertEq(amountIn, expectedAmountIn); @@ -1666,12 +1676,12 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { uint256 reserveBalanceBefore = poolExchange1.reserveBalance; uint256 tokenSupplyBefore = poolExchange1.tokenSupply; - uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn( - exchangeId, - address(token), - address(reserveToken), - amountOut - ); + uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); vm.prank(brokerAddress); uint256 amountIn = bancorExchangeProvider.swapOut(exchangeId, address(token), address(reserveToken), amountOut); assertEq(amountIn, expectedAmountIn); From 0eed5609cfd136b8fdfd6497d5eeda348ccc4fd5 Mon Sep 17 00:00:00 2001 From: Ryan Noble Date: Tue, 22 Oct 2024 12:27:11 +0200 Subject: [PATCH 7/7] fix: reverted stray commit --- contracts/goodDollar/GoodDollarExchangeProvider.sol | 4 +++- test/unit/goodDollar/GoodDollarExchangeProvider.t.sol | 6 ------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/contracts/goodDollar/GoodDollarExchangeProvider.sol b/contracts/goodDollar/GoodDollarExchangeProvider.sol index 6fcb8cba..e52cc103 100644 --- a/contracts/goodDollar/GoodDollarExchangeProvider.sol +++ b/contracts/goodDollar/GoodDollarExchangeProvider.sol @@ -193,7 +193,9 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan function updateRatioForReward(bytes32 exchangeId, uint256 reward) external onlyExpansionController whenNotPaused { PoolExchange memory exchange = getPoolExchange(exchangeId); - require(reward > 0, "Reward must be greater than 0"); + if (reward == 0) { + return; + } uint256 currentPriceScaled = currentPrice(exchangeId) * tokenPrecisionMultipliers[exchange.reserveAsset]; uint256 rewardScaled = reward * tokenPrecisionMultipliers[exchange.tokenAddress]; diff --git a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol index 333d3125..9a6dae10 100644 --- a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol +++ b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol @@ -449,12 +449,6 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio); assertEq(poolExchangeAfter.tokenSupply, poolExchange1.tokenSupply + reward); } - - function test_updateRatioForReward_whenRewardIs0_shouldRevert() public { - vm.prank(expansionControllerAddress); - vm.expectRevert("Reward must be greater than 0"); - exchangeProvider.updateRatioForReward(exchangeId, 0); - } } contract GoodDollarExchangeProviderTest_pausable is GoodDollarExchangeProviderTest {