diff --git a/.env.example b/.env.example index 45facb28..506a0579 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,3 @@ CELOSCAN_API_KEY= CELO_MAINNET_RPC_URL=https://forno.celo.org -BAKLAVA_RPC_URL=https://baklava-forno.celo-testnet.org -ALFAJORES_RPC_URL=https://alfajores-forno.celo-testnet.org +ALFAJORES_RPC_URL=https://alfajores-forno.celo-testnet.org \ No newline at end of file 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/.husky/pre-push b/.husky/pre-push index 6cdaab7b..0b8daea1 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -2,3 +2,4 @@ . "$(dirname -- "$0")/_/husky.sh" yarn lint +yarn todo 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/common/IERC20MintableBurnable.sol b/contracts/common/IERC20MintableBurnable.sol new file mode 100644 index 00000000..4eecc696 --- /dev/null +++ b/contracts/common/IERC20MintableBurnable.sol @@ -0,0 +1,12 @@ +pragma solidity ^0.8.0; + +import "openzeppelin-contracts-next/contracts/token/ERC20/IERC20.sol"; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. Does not include + * the optional functions; to access them see {ERC20Detailed}. + */ +interface IERC20MintableBurnable is IERC20 { + function mint(address account, uint256 amount) external; + function burn(uint256 amount) external; +} diff --git a/contracts/common/SafeERC20MintableBurnable.sol b/contracts/common/SafeERC20MintableBurnable.sol new file mode 100644 index 00000000..eb8c311a --- /dev/null +++ b/contracts/common/SafeERC20MintableBurnable.sol @@ -0,0 +1,47 @@ +pragma solidity ^0.8.0; + +import { IERC20MintableBurnable as IERC20 } from "contracts/common/IERC20MintableBurnable.sol"; +import { Address } from "openzeppelin-contracts-next/contracts/utils/Address.sol"; + +/** + * @title SafeERC20MintableBurnable + * @dev Wrappers around ERC20 operations that throw on failure (when the token + * contract returns false). Tokens that return no value (and instead revert or + * throw on failure) are also supported, non-reverting calls are assumed to be + * successful. + * To use this library you can add a `using SafeERC20MintableBurnable for IERC20;` statement to your contract, + * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. + */ +library SafeERC20MintableBurnable { + using Address for address; + + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { + _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); + } + + function safeMint(IERC20 token, address account, uint256 amount) internal { + _callOptionalReturn(token, abi.encodeWithSelector(token.mint.selector, account, amount)); + } + + function safeBurn(IERC20 token, uint256 amount) internal { + _callOptionalReturn(token, abi.encodeWithSelector(token.burn.selector, amount)); + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function _callOptionalReturn(IERC20 token, bytes memory data) private { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that + // the target address contains contract code and also asserts for success in the low-level call. + + bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed"); + if (returndata.length > 0) { + // Return data is optional + require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); + } + } +} diff --git a/contracts/goodDollar/BancorExchangeProvider.sol b/contracts/goodDollar/BancorExchangeProvider.sol new file mode 100644 index 00000000..95391152 --- /dev/null +++ b/contracts/goodDollar/BancorExchangeProvider.sol @@ -0,0 +1,373 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; +import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; +import { IReserve } from "contracts/interfaces/IReserve.sol"; + +import { OwnableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; + +import { BancorFormula } from "contracts/goodDollar/BancorFormula.sol"; +import { UD60x18, unwrap, wrap } from "prb/math/UD60x18.sol"; + +/** + * @title BancorExchangeProvider + * @notice Provides exchange functionality for Bancor pools. + */ +contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, BancorFormula, OwnableUpgradeable { + /* ========================================================= */ + /* ==================== State Variables ==================== */ + /* ========================================================= */ + + // Address of the broker contract. + address public broker; + + // Address of the reserve contract. + IReserve public reserve; + + // Maps an exchange id to the corresponding PoolExchange struct. + // exchangeId is in the format "asset0Symbol:asset1Symbol" + mapping(bytes32 => PoolExchange) public exchanges; + bytes32[] public exchangeIds; + + // 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. + * @param disable Set to true to run `_disableInitializers()` inherited from + * openzeppelin-contracts-upgradeable/Initializable.sol + */ + constructor(bool disable) { + if (disable) { + _disableInitializers(); + } + } + + /// @inheritdoc IBancorExchangeProvider + function initialize(address _broker, address _reserve) public initializer { + _initialize(_broker, _reserve); + } + + function _initialize(address _broker, address _reserve) internal onlyInitializing { + __Ownable_init(); + + BancorFormula.init(); + setBroker(_broker); + setReserve(_reserve); + } + + /* =================================================== */ + /* ==================== Modifiers ==================== */ + /* =================================================== */ + + modifier onlyBroker() { + require(msg.sender == broker, "Caller is not the Broker"); + _; + } + + modifier verifyExchangeTokens(address tokenIn, address tokenOut, PoolExchange memory exchange) { + require( + (tokenIn == exchange.reserveAsset && tokenOut == exchange.tokenAddress) || + (tokenIn == exchange.tokenAddress && tokenOut == exchange.reserveAsset), + "tokenIn and tokenOut must match exchange" + ); + _; + } + + /* ======================================================== */ + /* ==================== View Functions ==================== */ + /* ======================================================== */ + + /// @inheritdoc IBancorExchangeProvider + function getPoolExchange(bytes32 exchangeId) public view returns (PoolExchange memory exchange) { + exchange = exchanges[exchangeId]; + require(exchange.tokenAddress != address(0), "Exchange does not exist"); + return exchange; + } + + /// @inheritdoc IBancorExchangeProvider + function getExchangeIds() external view returns (bytes32[] memory) { + return exchangeIds; + } + + /** + * @inheritdoc IExchangeProvider + * @dev We don't expect the number of exchanges to grow to + * astronomical values so this is safe gas-wise as is. + */ + function getExchanges() public view returns (Exchange[] memory _exchanges) { + uint256 numExchanges = exchangeIds.length; + _exchanges = new Exchange[](numExchanges); + for (uint256 i = 0; i < numExchanges; i++) { + _exchanges[i].exchangeId = exchangeIds[i]; + _exchanges[i].assets = new address[](2); + _exchanges[i].assets[0] = exchanges[exchangeIds[i]].reserveAsset; + _exchanges[i].assets[1] = exchanges[exchangeIds[i]].tokenAddress; + } + } + + /// @inheritdoc IExchangeProvider + function getAmountOut( + bytes32 exchangeId, + address tokenIn, + address tokenOut, + uint256 amountIn + ) external view virtual returns (uint256 amountOut) { + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledAmountIn = amountIn * tokenPrecisionMultipliers[tokenIn]; + uint256 scaledAmountOut = _getScaledAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); + amountOut = scaledAmountOut / tokenPrecisionMultipliers[tokenOut]; + return amountOut; + } + + /// @inheritdoc IExchangeProvider + function getAmountIn( + bytes32 exchangeId, + address tokenIn, + address tokenOut, + uint256 amountOut + ) external view virtual returns (uint256 amountIn) { + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledAmountOut = amountOut * tokenPrecisionMultipliers[tokenOut]; + uint256 scaledAmountIn = _getScaledAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); + amountIn = scaledAmountIn / tokenPrecisionMultipliers[tokenIn]; + return amountIn; + } + + /// @inheritdoc IBancorExchangeProvider + function currentPrice(bytes32 exchangeId) public view returns (uint256 price) { + // calculates: reserveBalance / (tokenSupply * reserveRatio) + require(exchanges[exchangeId].reserveAsset != address(0), "Exchange does not exist"); + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledReserveRatio = uint256(exchange.reserveRatio) * 1e10; + UD60x18 denominator = wrap(exchange.tokenSupply).mul(wrap(scaledReserveRatio)); + price = unwrap(wrap(exchange.reserveBalance).div(denominator)); + return price; + } + + /* ============================================================ */ + /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ + + /// @inheritdoc IBancorExchangeProvider + function setBroker(address _broker) public onlyOwner { + require(_broker != address(0), "Broker address must be set"); + broker = _broker; + emit BrokerUpdated(_broker); + } + + /// @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)); + } + + /// @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); + + 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 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 + */ + function executeSwap(bytes32 exchangeId, address tokenIn, uint256 scaledAmountIn, uint256 scaledAmountOut) internal { + PoolExchange memory exchange = getPoolExchange(exchangeId); + if (tokenIn == exchange.reserveAsset) { + exchange.reserveBalance += scaledAmountIn; + exchange.tokenSupply += scaledAmountOut; + } else { + require(exchange.reserveBalance >= scaledAmountOut, "Insufficient reserve balance for swap"); + exchange.reserveBalance -= scaledAmountOut; + exchange.tokenSupply -= scaledAmountIn; + } + exchanges[exchangeId].reserveBalance = exchange.reserveBalance; + exchanges[exchangeId].tokenSupply = exchange.tokenSupply; + } + + /** + * @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 + */ + function _getScaledAmountIn( + PoolExchange memory exchange, + address tokenIn, + address tokenOut, + uint256 scaledAmountOut + ) internal view verifyExchangeTokens(tokenIn, tokenOut, exchange) returns (uint256 scaledAmountIn) { + if (tokenIn == exchange.reserveAsset) { + scaledAmountIn = fundCost(exchange.tokenSupply, exchange.reserveBalance, exchange.reserveRatio, scaledAmountOut); + } else { + // apply exit contribution + scaledAmountOut = (scaledAmountOut * MAX_WEIGHT) / (MAX_WEIGHT - exchange.exitContribution); + scaledAmountIn = saleCost(exchange.tokenSupply, exchange.reserveBalance, exchange.reserveRatio, scaledAmountOut); + } + } + + /** + * @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 + */ + function _getScaledAmountOut( + PoolExchange memory exchange, + address tokenIn, + address tokenOut, + uint256 scaledAmountIn + ) internal view verifyExchangeTokens(tokenIn, tokenOut, exchange) returns (uint256 scaledAmountOut) { + if (tokenIn == exchange.reserveAsset) { + scaledAmountOut = purchaseTargetAmount( + exchange.tokenSupply, + exchange.reserveBalance, + exchange.reserveRatio, + scaledAmountIn + ); + } else { + scaledAmountOut = saleTargetAmount( + exchange.tokenSupply, + exchange.reserveBalance, + exchange.reserveRatio, + scaledAmountIn + ); + // apply exit contribution + scaledAmountOut = (scaledAmountOut * (MAX_WEIGHT - exchange.exitContribution)) / MAX_WEIGHT; + } + } + + /** + * @notice Validates a PoolExchange's parameters and configuration + * @dev Reverts if not valid + * @param exchange The PoolExchange to validate + */ + 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" + ); + 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"); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} diff --git a/contracts/goodDollar/BancorFormula.sol b/contracts/goodDollar/BancorFormula.sol new file mode 100644 index 00000000..8ac8e06f --- /dev/null +++ b/contracts/goodDollar/BancorFormula.sol @@ -0,0 +1,712 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +// solhint-disable function-max-lines, max-line-length, code-complexity, reason-string +pragma solidity 0.8.18; + +/** + * @title BancorFormula contract by Bancor + * @dev https://github.com/bancorprotocol/contracts-solidity/blob/v0.6.39/solidity/contracts/converter/BancorFormula.sol + * + * Modified from the original by MentoLabs Team + * - 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 + * + */ + +contract BancorFormula { + uint256 private constant ONE = 1; + + uint32 public constant MAX_WEIGHT = 100000000; + uint8 private constant MIN_PRECISION = 32; + uint8 private constant MAX_PRECISION = 127; + + // Auto-generated via 'PrintIntScalingFactors.py' + uint256 private constant FIXED_1 = 0x080000000000000000000000000000000; + uint256 private constant FIXED_2 = 0x100000000000000000000000000000000; + uint256 private constant MAX_NUM = 0x200000000000000000000000000000000; + + // Auto-generated via 'PrintLn2ScalingFactors.py' + uint256 private constant LN2_NUMERATOR = 0x3f80fe03f80fe03f80fe03f80fe03f8; + uint256 private constant LN2_DENOMINATOR = 0x5b9de1d10bf4103d647b0955897ba80; + + // Auto-generated via 'PrintFunctionOptimalLog.py' and 'PrintFunctionOptimalExp.py' + uint256 private constant OPT_LOG_MAX_VAL = 0x15bf0a8b1457695355fb8ac404e7a79e3; + uint256 private constant OPT_EXP_MAX_VAL = 0x800000000000000000000000000000000; + + // Auto-generated via 'PrintMaxExpArray.py' + uint256[128] private maxExpArray; + + function initMaxExpArray() private { + // maxExpArray[ 0] = 0x6bffffffffffffffffffffffffffffffff; + // maxExpArray[ 1] = 0x67ffffffffffffffffffffffffffffffff; + // maxExpArray[ 2] = 0x637fffffffffffffffffffffffffffffff; + // maxExpArray[ 3] = 0x5f6fffffffffffffffffffffffffffffff; + // maxExpArray[ 4] = 0x5b77ffffffffffffffffffffffffffffff; + // maxExpArray[ 5] = 0x57b3ffffffffffffffffffffffffffffff; + // maxExpArray[ 6] = 0x5419ffffffffffffffffffffffffffffff; + // maxExpArray[ 7] = 0x50a2ffffffffffffffffffffffffffffff; + // maxExpArray[ 8] = 0x4d517fffffffffffffffffffffffffffff; + // maxExpArray[ 9] = 0x4a233fffffffffffffffffffffffffffff; + // maxExpArray[ 10] = 0x47165fffffffffffffffffffffffffffff; + // maxExpArray[ 11] = 0x4429afffffffffffffffffffffffffffff; + // maxExpArray[ 12] = 0x415bc7ffffffffffffffffffffffffffff; + // maxExpArray[ 13] = 0x3eab73ffffffffffffffffffffffffffff; + // maxExpArray[ 14] = 0x3c1771ffffffffffffffffffffffffffff; + // maxExpArray[ 15] = 0x399e96ffffffffffffffffffffffffffff; + // maxExpArray[ 16] = 0x373fc47fffffffffffffffffffffffffff; + // maxExpArray[ 17] = 0x34f9e8ffffffffffffffffffffffffffff; + // maxExpArray[ 18] = 0x32cbfd5fffffffffffffffffffffffffff; + // maxExpArray[ 19] = 0x30b5057fffffffffffffffffffffffffff; + // maxExpArray[ 20] = 0x2eb40f9fffffffffffffffffffffffffff; + // maxExpArray[ 21] = 0x2cc8340fffffffffffffffffffffffffff; + // maxExpArray[ 22] = 0x2af09481ffffffffffffffffffffffffff; + // maxExpArray[ 23] = 0x292c5bddffffffffffffffffffffffffff; + // maxExpArray[ 24] = 0x277abdcdffffffffffffffffffffffffff; + // maxExpArray[ 25] = 0x25daf6657fffffffffffffffffffffffff; + // maxExpArray[ 26] = 0x244c49c65fffffffffffffffffffffffff; + // maxExpArray[ 27] = 0x22ce03cd5fffffffffffffffffffffffff; + // maxExpArray[ 28] = 0x215f77c047ffffffffffffffffffffffff; + // maxExpArray[ 29] = 0x1fffffffffffffffffffffffffffffffff; + // maxExpArray[ 30] = 0x1eaefdbdabffffffffffffffffffffffff; + // maxExpArray[ 31] = 0x1d6bd8b2ebffffffffffffffffffffffff; + maxExpArray[32] = 0x1c35fedd14ffffffffffffffffffffffff; + maxExpArray[33] = 0x1b0ce43b323fffffffffffffffffffffff; + maxExpArray[34] = 0x19f0028ec1ffffffffffffffffffffffff; + maxExpArray[35] = 0x18ded91f0e7fffffffffffffffffffffff; + maxExpArray[36] = 0x17d8ec7f0417ffffffffffffffffffffff; + maxExpArray[37] = 0x16ddc6556cdbffffffffffffffffffffff; + maxExpArray[38] = 0x15ecf52776a1ffffffffffffffffffffff; + maxExpArray[39] = 0x15060c256cb2ffffffffffffffffffffff; + maxExpArray[40] = 0x1428a2f98d72ffffffffffffffffffffff; + maxExpArray[41] = 0x13545598e5c23fffffffffffffffffffff; + maxExpArray[42] = 0x1288c4161ce1dfffffffffffffffffffff; + maxExpArray[43] = 0x11c592761c666fffffffffffffffffffff; + maxExpArray[44] = 0x110a688680a757ffffffffffffffffffff; + maxExpArray[45] = 0x1056f1b5bedf77ffffffffffffffffffff; + maxExpArray[46] = 0x0faadceceeff8bffffffffffffffffffff; + maxExpArray[47] = 0x0f05dc6b27edadffffffffffffffffffff; + maxExpArray[48] = 0x0e67a5a25da4107fffffffffffffffffff; + maxExpArray[49] = 0x0dcff115b14eedffffffffffffffffffff; + maxExpArray[50] = 0x0d3e7a392431239fffffffffffffffffff; + maxExpArray[51] = 0x0cb2ff529eb71e4fffffffffffffffffff; + maxExpArray[52] = 0x0c2d415c3db974afffffffffffffffffff; + maxExpArray[53] = 0x0bad03e7d883f69bffffffffffffffffff; + maxExpArray[54] = 0x0b320d03b2c343d5ffffffffffffffffff; + maxExpArray[55] = 0x0abc25204e02828dffffffffffffffffff; + maxExpArray[56] = 0x0a4b16f74ee4bb207fffffffffffffffff; + maxExpArray[57] = 0x09deaf736ac1f569ffffffffffffffffff; + maxExpArray[58] = 0x0976bd9952c7aa957fffffffffffffffff; + maxExpArray[59] = 0x09131271922eaa606fffffffffffffffff; + maxExpArray[60] = 0x08b380f3558668c46fffffffffffffffff; + maxExpArray[61] = 0x0857ddf0117efa215bffffffffffffffff; + maxExpArray[62] = 0x07ffffffffffffffffffffffffffffffff; + maxExpArray[63] = 0x07abbf6f6abb9d087fffffffffffffffff; + maxExpArray[64] = 0x075af62cbac95f7dfa7fffffffffffffff; + maxExpArray[65] = 0x070d7fb7452e187ac13fffffffffffffff; + maxExpArray[66] = 0x06c3390ecc8af379295fffffffffffffff; + maxExpArray[67] = 0x067c00a3b07ffc01fd6fffffffffffffff; + maxExpArray[68] = 0x0637b647c39cbb9d3d27ffffffffffffff; + maxExpArray[69] = 0x05f63b1fc104dbd39587ffffffffffffff; + maxExpArray[70] = 0x05b771955b36e12f7235ffffffffffffff; + maxExpArray[71] = 0x057b3d49dda84556d6f6ffffffffffffff; + maxExpArray[72] = 0x054183095b2c8ececf30ffffffffffffff; + maxExpArray[73] = 0x050a28be635ca2b888f77fffffffffffff; + maxExpArray[74] = 0x04d5156639708c9db33c3fffffffffffff; + maxExpArray[75] = 0x04a23105873875bd52dfdfffffffffffff; + maxExpArray[76] = 0x0471649d87199aa990756fffffffffffff; + maxExpArray[77] = 0x04429a21a029d4c1457cfbffffffffffff; + maxExpArray[78] = 0x0415bc6d6fb7dd71af2cb3ffffffffffff; + maxExpArray[79] = 0x03eab73b3bbfe282243ce1ffffffffffff; + maxExpArray[80] = 0x03c1771ac9fb6b4c18e229ffffffffffff; + maxExpArray[81] = 0x0399e96897690418f785257fffffffffff; + maxExpArray[82] = 0x0373fc456c53bb779bf0ea9fffffffffff; + maxExpArray[83] = 0x034f9e8e490c48e67e6ab8bfffffffffff; + maxExpArray[84] = 0x032cbfd4a7adc790560b3337ffffffffff; + maxExpArray[85] = 0x030b50570f6e5d2acca94613ffffffffff; + maxExpArray[86] = 0x02eb40f9f620fda6b56c2861ffffffffff; + maxExpArray[87] = 0x02cc8340ecb0d0f520a6af58ffffffffff; + maxExpArray[88] = 0x02af09481380a0a35cf1ba02ffffffffff; + maxExpArray[89] = 0x0292c5bdd3b92ec810287b1b3fffffffff; + maxExpArray[90] = 0x0277abdcdab07d5a77ac6d6b9fffffffff; + maxExpArray[91] = 0x025daf6654b1eaa55fd64df5efffffffff; + maxExpArray[92] = 0x0244c49c648baa98192dce88b7ffffffff; + maxExpArray[93] = 0x022ce03cd5619a311b2471268bffffffff; + maxExpArray[94] = 0x0215f77c045fbe885654a44a0fffffffff; + maxExpArray[95] = 0x01ffffffffffffffffffffffffffffffff; + maxExpArray[96] = 0x01eaefdbdaaee7421fc4d3ede5ffffffff; + maxExpArray[97] = 0x01d6bd8b2eb257df7e8ca57b09bfffffff; + maxExpArray[98] = 0x01c35fedd14b861eb0443f7f133fffffff; + maxExpArray[99] = 0x01b0ce43b322bcde4a56e8ada5afffffff; + maxExpArray[100] = 0x019f0028ec1fff007f5a195a39dfffffff; + maxExpArray[101] = 0x018ded91f0e72ee74f49b15ba527ffffff; + maxExpArray[102] = 0x017d8ec7f04136f4e5615fd41a63ffffff; + maxExpArray[103] = 0x016ddc6556cdb84bdc8d12d22e6fffffff; + maxExpArray[104] = 0x015ecf52776a1155b5bd8395814f7fffff; + maxExpArray[105] = 0x015060c256cb23b3b3cc3754cf40ffffff; + maxExpArray[106] = 0x01428a2f98d728ae223ddab715be3fffff; + maxExpArray[107] = 0x013545598e5c23276ccf0ede68034fffff; + maxExpArray[108] = 0x01288c4161ce1d6f54b7f61081194fffff; + maxExpArray[109] = 0x011c592761c666aa641d5a01a40f17ffff; + maxExpArray[110] = 0x0110a688680a7530515f3e6e6cfdcdffff; + maxExpArray[111] = 0x01056f1b5bedf75c6bcb2ce8aed428ffff; + maxExpArray[112] = 0x00faadceceeff8a0890f3875f008277fff; + maxExpArray[113] = 0x00f05dc6b27edad306388a600f6ba0bfff; + maxExpArray[114] = 0x00e67a5a25da41063de1495d5b18cdbfff; + maxExpArray[115] = 0x00dcff115b14eedde6fc3aa5353f2e4fff; + maxExpArray[116] = 0x00d3e7a3924312399f9aae2e0f868f8fff; + maxExpArray[117] = 0x00cb2ff529eb71e41582cccd5a1ee26fff; + maxExpArray[118] = 0x00c2d415c3db974ab32a51840c0b67edff; + maxExpArray[119] = 0x00bad03e7d883f69ad5b0a186184e06bff; + maxExpArray[120] = 0x00b320d03b2c343d4829abd6075f0cc5ff; + maxExpArray[121] = 0x00abc25204e02828d73c6e80bcdb1a95bf; + maxExpArray[122] = 0x00a4b16f74ee4bb2040a1ec6c15fbbf2df; + maxExpArray[123] = 0x009deaf736ac1f569deb1b5ae3f36c130f; + maxExpArray[124] = 0x00976bd9952c7aa957f5937d790ef65037; + maxExpArray[125] = 0x009131271922eaa6064b73a22d0bd4f2bf; + maxExpArray[126] = 0x008b380f3558668c46c91c49a2f8e967b9; + maxExpArray[127] = 0x00857ddf0117efa215952912839f6473e6; + } + + /** + * @dev should be executed after construction (too large for the constructor) + */ + function init() public { + initMaxExpArray(); + } + + /** + * @dev given a token supply, reserve balance, weight and a deposit amount (in the reserve token), + * calculates the target amount for a given conversion (in the main token) + * + * Formula: + * return = _supply * ((1 + _amount / _reserveBalance) ^ (_reserveWeight / 1000000) - 1) + * + * @param _supply liquid token supply + * @param _reserveBalance reserve balance + * @param _reserveWeight reserve weight, represented in ppm (1-1000000) + * @param _amount amount of reserve tokens to get the target amount for + * + * @return target + */ + function purchaseTargetAmount( + uint256 _supply, + uint256 _reserveBalance, + uint32 _reserveWeight, + uint256 _amount + ) internal view returns (uint256) { + // validate input + require(_supply > 0, "ERR_INVALID_SUPPLY"); + require(_reserveBalance > 0, "ERR_INVALID_RESERVE_BALANCE"); + require(_reserveWeight > 0 && _reserveWeight <= MAX_WEIGHT, "ERR_INVALID_RESERVE_WEIGHT"); + + // special case for 0 deposit amount + if (_amount == 0) return 0; + + // special case if the weight = 100% + if (_reserveWeight == MAX_WEIGHT) return (_supply * _amount) / _reserveBalance; + + uint256 result; + uint8 precision; + uint256 baseN = _amount + _reserveBalance; + (result, precision) = power(baseN, _reserveBalance, _reserveWeight, MAX_WEIGHT); + uint256 temp = (_supply * result) >> precision; + return temp - _supply; + } + + /** + * @dev given a token supply, reserve balance, weight and a sell amount (in the main token), + * calculates the target amount for a given conversion (in the reserve token) + * + * Formula: + * 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 + * @param _reserveWeight reserve weight, represented in ppm (1-1000000) + * @param _amount amount of liquid tokens to get the target amount for + * + * @return reserve token amount + */ + function saleTargetAmount( + uint256 _supply, + uint256 _reserveBalance, + uint32 _reserveWeight, + uint256 _amount + ) internal view returns (uint256) { + // validate input + require(_supply > 0, "ERR_INVALID_SUPPLY"); + require(_reserveBalance > 0, "ERR_INVALID_RESERVE_BALANCE"); + require(_reserveWeight > 0 && _reserveWeight <= MAX_WEIGHT, "ERR_INVALID_RESERVE_WEIGHT"); + require(_amount <= _supply, "ERR_INVALID_AMOUNT"); + + // special case for 0 sell amount + if (_amount == 0) return 0; + + // special case for selling the entire supply + if (_amount == _supply) return _reserveBalance; + + // special case if the weight = 100% + if (_reserveWeight == MAX_WEIGHT) return (_reserveBalance * _amount) / _supply; + + uint256 result; + uint8 precision; + uint256 baseD = _supply - _amount; + (result, precision) = power(_supply, baseD, MAX_WEIGHT, _reserveWeight); + uint256 temp1 = _reserveBalance * result; + uint256 temp2 = _reserveBalance << precision; + return (temp1 - temp2) / result; + } + + /** + * @dev given a pool token supply, reserve balance, reserve ratio and an amount of requested pool tokens, + * calculates the amount of reserve tokens required for purchasing the given amount of pool tokens + * + * Formula: + * return = _reserveBalance * (((_supply + _amount) / _supply) ^ (MAX_WEIGHT / _reserveRatio) - 1) + * + * @param _supply pool token supply + * @param _reserveBalance reserve balance + * @param _reserveRatio reserve ratio, represented in ppm (2-2000000) + * @param _amount requested amount of pool tokens + * + * @return reserve token amount + */ + function fundCost( + uint256 _supply, + uint256 _reserveBalance, + uint32 _reserveRatio, + 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"); + + // special case for 0 amount + if (_amount == 0) return 0; + + // special case if the reserve ratio = 100% + if (_reserveRatio == MAX_WEIGHT) return (_amount * _reserveBalance - 1) / _supply + 1; + + uint256 result; + uint8 precision; + uint256 baseN = _supply + _amount; + (result, precision) = power(baseN, _supply, MAX_WEIGHT, _reserveRatio); + uint256 temp = ((_reserveBalance * result - 1) >> precision) + 1; + return temp - _reserveBalance; + } + + /** + * 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) + * + * + * @param _supply pool token supply + * @param _reserveBalance reserve balance + * @param _reserveWeight reserve weight, represented in ppm + * @param _amount amount of reserve tokens to get the target amount for + * + * @return reserve token amount + */ + function saleCost( + uint256 _supply, + uint256 _reserveBalance, + uint32 _reserveWeight, + uint256 _amount + ) internal view returns (uint256) { + // validate input + require(_supply > 0, "ERR_INVALID_SUPPLY"); + require(_reserveBalance > 0, "ERR_INVALID_RESERVE_BALANCE"); + require(_reserveWeight > 0 && _reserveWeight <= MAX_WEIGHT, "ERR_INVALID_RESERVE_WEIGHT"); + + require(_amount <= _reserveBalance, "ERR_INVALID_AMOUNT"); + + // special case for 0 sell amount + if (_amount == 0) return 0; + + // 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 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; + } + + /** + * @dev General Description: + * Determine a value of precision. + * Calculate an integer approximation of (_baseN / _baseD) ^ (_expN / _expD) * 2 ^ precision. + * Return the result along with the precision used. + * + * Detailed Description: + * Instead of calculating "base ^ exp", we calculate "e ^ (log(base) * exp)". + * The value of "log(base)" is represented with an integer slightly smaller than "log(base) * 2 ^ precision". + * The larger "precision" is, the more accurately this value represents the real value. + * However, the larger "precision" is, the more bits are required in order to store this value. + * And the exponentiation function, which takes "x" and calculates "e ^ x", is limited to a maximum exponent (maximum value of "x"). + * This maximum exponent depends on the "precision" used, and it is given by "maxExpArray[precision] >> (MAX_PRECISION - precision)". + * Hence we need to determine the highest precision which can be used for the given input, before calling the exponentiation function. + * This allows us to compute "base ^ exp" with maximum accuracy and without exceeding 256 bits in any of the intermediate computations. + * This functions assumes that "_expN < 2 ^ 256 / log(MAX_NUM - 1)", otherwise the multiplication should be replaced with a "safeMul". + * Since we rely on unsigned-integer arithmetic and "base < 1" ==> "log(base) < 0", this function does not support "_baseN < _baseD". + */ + function power(uint256 _baseN, uint256 _baseD, uint32 _expN, uint32 _expD) internal view returns (uint256, uint8) { + require(_baseN < MAX_NUM); + + uint256 baseLog; + uint256 base = (_baseN * FIXED_1) / _baseD; + if (base < OPT_LOG_MAX_VAL) { + baseLog = optimalLog(base); + } else { + baseLog = generalLog(base); + } + + uint256 baseLogTimesExp = (baseLog * _expN) / _expD; + if (baseLogTimesExp < OPT_EXP_MAX_VAL) { + return (optimalExp(baseLogTimesExp), MAX_PRECISION); + } else { + uint8 precision = findPositionInMaxExpArray(baseLogTimesExp); + return (generalExp(baseLogTimesExp >> (MAX_PRECISION - precision), precision), precision); + } + } + + /** + * @dev computes log(x / FIXED_1) * FIXED_1. + * This functions assumes that "x >= FIXED_1", because the output would be negative otherwise. + */ + function generalLog(uint256 x) internal pure returns (uint256) { + uint256 res = 0; + + // If x >= 2, then we compute the integer part of log2(x), which is larger than 0. + if (x >= FIXED_2) { + uint8 count = floorLog2(x / FIXED_1); + x >>= count; // now x < 2 + res = count * FIXED_1; + } + + // If x > 1, then we compute the fraction part of log2(x), which is larger than 0. + if (x > FIXED_1) { + for (uint8 i = MAX_PRECISION; i > 0; --i) { + x = (x * x) / FIXED_1; // now 1 < x < 4 + if (x >= FIXED_2) { + x >>= 1; // now 1 < x < 2 + res += ONE << (i - 1); + } + } + } + + return (res * LN2_NUMERATOR) / LN2_DENOMINATOR; + } + + /** + * @dev computes the largest integer smaller than or equal to the binary logarithm of the input. + */ + function floorLog2(uint256 _n) internal pure returns (uint8) { + uint8 res = 0; + + if (_n < 256) { + // At most 8 iterations + while (_n > 1) { + _n >>= 1; + res += 1; + } + } else { + // Exactly 8 iterations + for (uint8 s = 128; s > 0; s >>= 1) { + if (_n >= (ONE << s)) { + _n >>= s; + res |= s; + } + } + } + + return res; + } + + /** + * @dev the global "maxExpArray" is sorted in descending order, and therefore the following statements are equivalent: + * - This function finds the position of [the smallest value in "maxExpArray" larger than or equal to "x"] + * - This function finds the highest position of [a value in "maxExpArray" larger than or equal to "x"] + */ + function findPositionInMaxExpArray(uint256 _x) internal view returns (uint8 position) { + uint8 lo = MIN_PRECISION; + uint8 hi = MAX_PRECISION; + + while (lo + 1 < hi) { + uint8 mid = (lo + hi) / 2; + if (maxExpArray[mid] >= _x) lo = mid; + else hi = mid; + } + + if (maxExpArray[hi] >= _x) return hi; + if (maxExpArray[lo] >= _x) return lo; + + require(false); + } + + /** + * @dev this function can be auto-generated by the script 'PrintFunctionGeneralExp.py'. + * it approximates "e ^ x" via maclaurin summation: "(x^0)/0! + (x^1)/1! + ... + (x^n)/n!". + * it returns "e ^ (x / 2 ^ precision) * 2 ^ precision", that is, the result is upshifted for accuracy. + * the global "maxExpArray" maps each "precision" to "((maximumExponent + 1) << (MAX_PRECISION - precision)) - 1". + * the maximum permitted value for "x" is therefore given by "maxExpArray[precision] >> (MAX_PRECISION - precision)". + */ + function generalExp(uint256 _x, uint8 _precision) internal pure returns (uint256) { + uint256 xi = _x; + uint256 res = 0; + + xi = (xi * _x) >> _precision; + res += xi * 0x3442c4e6074a82f1797f72ac0000000; // add x^02 * (33! / 02!) + xi = (xi * _x) >> _precision; + res += xi * 0x116b96f757c380fb287fd0e40000000; // add x^03 * (33! / 03!) + xi = (xi * _x) >> _precision; + res += xi * 0x045ae5bdd5f0e03eca1ff4390000000; // add x^04 * (33! / 04!) + xi = (xi * _x) >> _precision; + res += xi * 0x00defabf91302cd95b9ffda50000000; // add x^05 * (33! / 05!) + xi = (xi * _x) >> _precision; + res += xi * 0x002529ca9832b22439efff9b8000000; // add x^06 * (33! / 06!) + xi = (xi * _x) >> _precision; + res += xi * 0x00054f1cf12bd04e516b6da88000000; // add x^07 * (33! / 07!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000a9e39e257a09ca2d6db51000000; // add x^08 * (33! / 08!) + xi = (xi * _x) >> _precision; + res += xi * 0x000012e066e7b839fa050c309000000; // add x^09 * (33! / 09!) + xi = (xi * _x) >> _precision; + res += xi * 0x000001e33d7d926c329a1ad1a800000; // add x^10 * (33! / 10!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000002bee513bdb4a6b19b5f800000; // add x^11 * (33! / 11!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000003a9316fa79b88eccf2a00000; // add x^12 * (33! / 12!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000048177ebe1fa812375200000; // add x^13 * (33! / 13!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000005263fe90242dcbacf00000; // add x^14 * (33! / 14!) + xi = (xi * _x) >> _precision; + res += xi * 0x000000000057e22099c030d94100000; // add x^15 * (33! / 15!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000057e22099c030d9410000; // add x^16 * (33! / 16!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000052b6b54569976310000; // add x^17 * (33! / 17!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000004985f67696bf748000; // add x^18 * (33! / 18!) + xi = (xi * _x) >> _precision; + res += xi * 0x000000000000003dea12ea99e498000; // add x^19 * (33! / 19!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000000031880f2214b6e000; // add x^20 * (33! / 20!) + xi = (xi * _x) >> _precision; + res += xi * 0x000000000000000025bcff56eb36000; // add x^21 * (33! / 21!) + xi = (xi * _x) >> _precision; + res += xi * 0x000000000000000001b722e10ab1000; // add x^22 * (33! / 22!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000001317c70077000; // add x^23 * (33! / 23!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000000000000cba84aafa00; // add x^24 * (33! / 24!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000000000000082573a0a00; // add x^25 * (33! / 25!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000000000000005035ad900; // add x^26 * (33! / 26!) + xi = (xi * _x) >> _precision; + res += xi * 0x000000000000000000000002f881b00; // add x^27 * (33! / 27!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000000000001b29340; // add x^28 * (33! / 28!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000000000000000000efc40; // add x^29 * (33! / 29!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000000000000007fe0; // add x^30 * (33! / 30!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000000000000000420; // add x^31 * (33! / 31!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000000000000000021; // add x^32 * (33! / 32!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000000000000000001; // add x^33 * (33! / 33!) + + return res / 0x688589cc0e9505e2f2fee5580000000 + _x + (ONE << _precision); // divide by 33! and then add x^1 / 1! + x^0 / 0! + } + + /** + * @dev computes log(x / FIXED_1) * FIXED_1 + * Input range: FIXED_1 <= x <= OPT_LOG_MAX_VAL - 1 + * Auto-generated via 'PrintFunctionOptimalLog.py' + * Detailed description: + * - Rewrite the input as a product of natural exponents and a single residual r, such that 1 < r < 2 + * - The natural logarithm of each (pre-calculated) exponent is the degree of the exponent + * - The natural logarithm of r is calculated via Taylor series for log(1 + x), where x = r - 1 + * - 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; + + if (x >= 0xd3094c70f034de4b96ff7d5b6f99fcd8) { + res += 0x40000000000000000000000000000000; + x = (x * FIXED_1) / 0xd3094c70f034de4b96ff7d5b6f99fcd8; + } // add 1 / 2^1 + if (x >= 0xa45af1e1f40c333b3de1db4dd55f29a7) { + res += 0x20000000000000000000000000000000; + x = (x * FIXED_1) / 0xa45af1e1f40c333b3de1db4dd55f29a7; + } // add 1 / 2^2 + if (x >= 0x910b022db7ae67ce76b441c27035c6a1) { + res += 0x10000000000000000000000000000000; + x = (x * FIXED_1) / 0x910b022db7ae67ce76b441c27035c6a1; + } // add 1 / 2^3 + if (x >= 0x88415abbe9a76bead8d00cf112e4d4a8) { + res += 0x08000000000000000000000000000000; + x = (x * FIXED_1) / 0x88415abbe9a76bead8d00cf112e4d4a8; + } // add 1 / 2^4 + if (x >= 0x84102b00893f64c705e841d5d4064bd3) { + res += 0x04000000000000000000000000000000; + x = (x * FIXED_1) / 0x84102b00893f64c705e841d5d4064bd3; + } // add 1 / 2^5 + if (x >= 0x8204055aaef1c8bd5c3259f4822735a2) { + res += 0x02000000000000000000000000000000; + x = (x * FIXED_1) / 0x8204055aaef1c8bd5c3259f4822735a2; + } // add 1 / 2^6 + if (x >= 0x810100ab00222d861931c15e39b44e99) { + res += 0x01000000000000000000000000000000; + x = (x * FIXED_1) / 0x810100ab00222d861931c15e39b44e99; + } // add 1 / 2^7 + if (x >= 0x808040155aabbbe9451521693554f733) { + res += 0x00800000000000000000000000000000; + x = (x * FIXED_1) / 0x808040155aabbbe9451521693554f733; + } // add 1 / 2^8 + + z = y = x - FIXED_1; + w = (y * y) / FIXED_1; + res += (z * (0x100000000000000000000000000000000 - y)) / 0x100000000000000000000000000000000; + z = (z * w) / FIXED_1; // add y^01 / 01 - y^02 / 02 + res += (z * (0x0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - y)) / 0x200000000000000000000000000000000; + z = (z * w) / FIXED_1; // add y^03 / 03 - y^04 / 04 + res += (z * (0x099999999999999999999999999999999 - y)) / 0x300000000000000000000000000000000; + z = (z * w) / FIXED_1; // add y^05 / 05 - y^06 / 06 + res += (z * (0x092492492492492492492492492492492 - y)) / 0x400000000000000000000000000000000; + z = (z * w) / FIXED_1; // add y^07 / 07 - y^08 / 08 + res += (z * (0x08e38e38e38e38e38e38e38e38e38e38e - y)) / 0x500000000000000000000000000000000; + z = (z * w) / FIXED_1; // add y^09 / 09 - y^10 / 10 + res += (z * (0x08ba2e8ba2e8ba2e8ba2e8ba2e8ba2e8b - y)) / 0x600000000000000000000000000000000; + z = (z * w) / FIXED_1; // add y^11 / 11 - y^12 / 12 + res += (z * (0x089d89d89d89d89d89d89d89d89d89d89 - y)) / 0x700000000000000000000000000000000; + z = (z * w) / FIXED_1; // add y^13 / 13 - y^14 / 14 + res += (z * (0x088888888888888888888888888888888 - y)) / 0x800000000000000000000000000000000; // add y^15 / 15 - y^16 / 16 + + return res; + } + + /** + * @dev computes e ^ (x / FIXED_1) * FIXED_1 + * input range: 0 <= x <= OPT_EXP_MAX_VAL - 1 + * auto-generated via 'PrintFunctionOptimalExp.py' + * Detailed description: + * - Rewrite the input as a sum of binary exponents and a single residual r, as small as possible + * - The exponentiation of each binary exponent is given (pre-calculated) + * - The exponentiation of r is calculated via Taylor series for e^x, where x = r + * - The exponentiation of the input is calculated by multiplying the intermediate results above + * - For example: e^5.521692859 = e^(4 + 1 + 0.5 + 0.021692859) = e^4 * e^1 * e^0.5 * e^0.021692859 + */ + 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; + + z = y = x % 0x10000000000000000000000000000000; // get the input modulo 2^(-3) + z = (z * y) / FIXED_1; + res += z * 0x10e1b3be415a0000; // add y^02 * (20! / 02!) + z = (z * y) / FIXED_1; + res += z * 0x05a0913f6b1e0000; // add y^03 * (20! / 03!) + z = (z * y) / FIXED_1; + res += z * 0x0168244fdac78000; // add y^04 * (20! / 04!) + z = (z * y) / FIXED_1; + res += z * 0x004807432bc18000; // add y^05 * (20! / 05!) + z = (z * y) / FIXED_1; + res += z * 0x000c0135dca04000; // add y^06 * (20! / 06!) + z = (z * y) / FIXED_1; + res += z * 0x0001b707b1cdc000; // add y^07 * (20! / 07!) + z = (z * y) / FIXED_1; + res += z * 0x000036e0f639b800; // add y^08 * (20! / 08!) + z = (z * y) / FIXED_1; + res += z * 0x00000618fee9f800; // add y^09 * (20! / 09!) + z = (z * y) / FIXED_1; + res += z * 0x0000009c197dcc00; // add y^10 * (20! / 10!) + z = (z * y) / FIXED_1; + res += z * 0x0000000e30dce400; // add y^11 * (20! / 11!) + z = (z * y) / FIXED_1; + res += z * 0x000000012ebd1300; // add y^12 * (20! / 12!) + z = (z * y) / FIXED_1; + res += z * 0x0000000017499f00; // add y^13 * (20! / 13!) + z = (z * y) / FIXED_1; + res += z * 0x0000000001a9d480; // add y^14 * (20! / 14!) + z = (z * y) / FIXED_1; + res += z * 0x00000000001c6380; // add y^15 * (20! / 15!) + z = (z * y) / FIXED_1; + res += z * 0x000000000001c638; // add y^16 * (20! / 16!) + z = (z * y) / FIXED_1; + res += z * 0x0000000000001ab8; // add y^17 * (20! / 17!) + z = (z * y) / FIXED_1; + res += z * 0x000000000000017c; // add y^18 * (20! / 18!) + z = (z * y) / FIXED_1; + res += z * 0x0000000000000014; // add y^19 * (20! / 19!) + z = (z * y) / FIXED_1; + res += z * 0x0000000000000001; // add y^20 * (20! / 20!) + res = res / 0x21c3677c82b40000 + y + FIXED_1; // divide by 20! and then add y^1 / 1! + y^0 / 0! + + if ((x & 0x010000000000000000000000000000000) != 0) + res = (res * 0x1c3d6a24ed82218787d624d3e5eba95f9) / 0x18ebef9eac820ae8682b9793ac6d1e776; // multiply by e^2^(-3) + if ((x & 0x020000000000000000000000000000000) != 0) + res = (res * 0x18ebef9eac820ae8682b9793ac6d1e778) / 0x1368b2fc6f9609fe7aceb46aa619baed4; // multiply by e^2^(-2) + if ((x & 0x040000000000000000000000000000000) != 0) + res = (res * 0x1368b2fc6f9609fe7aceb46aa619baed5) / 0x0bc5ab1b16779be3575bd8f0520a9f21f; // multiply by e^2^(-1) + if ((x & 0x080000000000000000000000000000000) != 0) + res = (res * 0x0bc5ab1b16779be3575bd8f0520a9f21e) / 0x0454aaa8efe072e7f6ddbab84b40a55c9; // multiply by e^2^(+0) + if ((x & 0x100000000000000000000000000000000) != 0) + res = (res * 0x0454aaa8efe072e7f6ddbab84b40a55c5) / 0x00960aadc109e7a3bf4578099615711ea; // multiply by e^2^(+1) + if ((x & 0x200000000000000000000000000000000) != 0) + res = (res * 0x00960aadc109e7a3bf4578099615711d7) / 0x0002bf84208204f5977f9a8cf01fdce3d; // multiply by e^2^(+2) + if ((x & 0x400000000000000000000000000000000) != 0) + res = (res * 0x0002bf84208204f5977f9a8cf01fdc307) / 0x0000003c6ab775dd0b95b4cbee7e65d11; // multiply by e^2^(+3) + + return res; + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} +// slither-disable-end divide-before-multiply diff --git a/contracts/goodDollar/GoodDollarExchangeProvider.sol b/contracts/goodDollar/GoodDollarExchangeProvider.sol new file mode 100644 index 00000000..b744fcc7 --- /dev/null +++ b/contracts/goodDollar/GoodDollarExchangeProvider.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { PausableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/security/PausableUpgradeable.sol"; + +import { IGoodDollarExchangeProvider } from "contracts/interfaces/IGoodDollarExchangeProvider.sol"; +import { IGoodDollarExpansionController } from "contracts/interfaces/IGoodDollarExpansionController.sol"; + +import { BancorExchangeProvider } from "./BancorExchangeProvider.sol"; +import { UD60x18, unwrap, wrap } from "prb/math/UD60x18.sol"; + +/** + * @title GoodDollarExchangeProvider + * @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; + + // 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. + * @param disable Set to true to run `_disableInitializers()` inherited from + * openzeppelin-contracts-upgradeable/Initializable.sol + */ + constructor(bool disable) BancorExchangeProvider(disable) {} + + /// @inheritdoc IGoodDollarExchangeProvider + function initialize( + address _broker, + address _reserve, + address _expansionController, + address _avatar + ) public initializer { + BancorExchangeProvider._initialize(_broker, _reserve); + __Pausable_init(); + + setExpansionController(_expansionController); + setAvatar(_avatar); + } + + /* =================================================== */ + /* ==================== Modifiers ==================== */ + /* =================================================== */ + + modifier onlyAvatar() { + require(msg.sender == AVATAR, "Only Avatar can call this function"); + _; + } + + modifier onlyExpansionController() { + require(msg.sender == address(expansionController), "Only ExpansionController can call this function"); + _; + } + + /* ============================================================ */ + /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ + + /// @inheritdoc IGoodDollarExchangeProvider + function setAvatar(address _avatar) public onlyOwner { + require(_avatar != address(0), "Avatar address must be set"); + AVATAR = _avatar; + emit AvatarUpdated(_avatar); + } + + /// @inheritdoc IGoodDollarExchangeProvider + function setExpansionController(address _expansionController) public onlyOwner { + require(_expansionController != address(0), "ExpansionController address must be set"); + expansionController = IGoodDollarExpansionController(_expansionController); + emit ExpansionControllerUpdated(_expansionController); + } + + /** + * @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, + address tokenOut, + uint256 amountIn + ) public override onlyBroker whenNotPaused returns (uint256 amountOut) { + amountOut = BancorExchangeProvider.swapIn(exchangeId, tokenIn, tokenOut, amountIn); + } + + /// @inheritdoc BancorExchangeProvider + function swapOut( + bytes32 exchangeId, + address tokenIn, + address tokenOut, + uint256 amountOut + ) public override onlyBroker whenNotPaused returns (uint256 amountIn) { + amountIn = BancorExchangeProvider.swapOut(exchangeId, tokenIn, tokenOut, amountOut); + } + + /** + * @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 + */ + function mintFromExpansion( + bytes32 exchangeId, + uint256 reserveRatioScalar + ) external onlyExpansionController whenNotPaused returns (uint256 amountToMint) { + require(reserveRatioScalar > 0, "Reserve ratio scalar must be greater than 0"); + PoolExchange memory exchange = getPoolExchange(exchangeId); + + UD60x18 scaledRatio = wrap(uint256(exchange.reserveRatio) * 1e10); + UD60x18 newRatio = scaledRatio.mul(wrap(reserveRatioScalar)); + + uint32 newRatioUint = uint32(unwrap(newRatio) / 1e10); + require(newRatioUint > 0, "New ratio must be greater than 0"); + + UD60x18 numerator = wrap(exchange.tokenSupply).mul(scaledRatio); + numerator = numerator.sub(wrap(exchange.tokenSupply).mul(newRatio)); + + uint256 scaledAmountToMint = unwrap(numerator.div(newRatio)); + + exchanges[exchangeId].reserveRatio = newRatioUint; + exchanges[exchangeId].tokenSupply += scaledAmountToMint; + + 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 + * flowing into the reserve while keeping the current price the same. + * calculation: amountToMint = reserveInterest * tokenSupply / reserveBalance + */ + function mintFromInterest( + bytes32 exchangeId, + uint256 reserveInterest + ) external onlyExpansionController whenNotPaused returns (uint256 amountToMint) { + PoolExchange memory exchange = getPoolExchange(exchangeId); + + uint256 reserveinterestScaled = reserveInterest * tokenPrecisionMultipliers[exchange.reserveAsset]; + uint256 amountToMintScaled = unwrap( + wrap(reserveinterestScaled).mul(wrap(exchange.tokenSupply)).div(wrap(exchange.reserveBalance)) + ); + amountToMint = amountToMintScaled / tokenPrecisionMultipliers[exchange.tokenAddress]; + + exchanges[exchangeId].tokenSupply += amountToMintScaled; + exchanges[exchangeId].reserveBalance += reserveinterestScaled; + + return amountToMint; + } + + /** + * @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 + */ + function updateRatioForReward(bytes32 exchangeId, uint256 reward) external onlyExpansionController whenNotPaused { + PoolExchange memory exchange = getPoolExchange(exchangeId); + + uint256 currentPriceScaled = currentPrice(exchangeId) * tokenPrecisionMultipliers[exchange.reserveAsset]; + uint256 rewardScaled = reward * tokenPrecisionMultipliers[exchange.tokenAddress]; + + UD60x18 numerator = wrap(exchange.reserveBalance); + UD60x18 denominator = wrap(exchange.tokenSupply + rewardScaled).mul(wrap(currentPriceScaled)); + uint256 newRatioScaled = unwrap(numerator.div(denominator)); + + uint32 newRatioUint = uint32(newRatioScaled / 1e10); + exchanges[exchangeId].reserveRatio = newRatioUint; + exchanges[exchangeId].tokenSupply += rewardScaled; + + emit ReserveRatioUpdated(exchangeId, newRatioUint); + } + + /** + * @inheritdoc IGoodDollarExchangeProvider + * @dev Only callable by the GoodDollar DAO contract. + */ + function pause() external virtual onlyAvatar { + _pause(); + } + + /** + * @inheritdoc IGoodDollarExchangeProvider + * @dev Only callable by the GoodDollar DAO contract. + */ + function unpause() external virtual onlyAvatar { + _unpause(); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} diff --git a/contracts/goodDollar/GoodDollarExpansionController.sol b/contracts/goodDollar/GoodDollarExpansionController.sol new file mode 100644 index 00000000..f753d060 --- /dev/null +++ b/contracts/goodDollar/GoodDollarExpansionController.sol @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IGoodDollarExpansionController } from "contracts/interfaces/IGoodDollarExpansionController.sol"; +import { IGoodDollarExchangeProvider } from "contracts/interfaces/IGoodDollarExchangeProvider.sol"; +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; +import { IERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/IERC20.sol"; +import { IGoodDollar } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; +import { IDistributionHelper } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; + +import { PausableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/security/PausableUpgradeable.sol"; +import { OwnableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import { unwrap, wrap, powu } from "prb/math/UD60x18.sol"; + +/** + * @title GoodDollarExpansionController + * @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; + + // Address of the distribution helper contract + IDistributionHelper public distributionHelper; + + // Address of reserve contract holding the GoodDollar reserve + address public reserve; + + // Address of the GoodDollar exchange provider + IGoodDollarExchangeProvider public goodDollarExchangeProvider; + + // Maps exchangeId to exchangeExpansionConfig + 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. + * @param disable Set to true to run `_disableInitializers()` inherited from + * openzeppelin-contracts-upgradeable/Initializable.sol + */ + constructor(bool disable) { + if (disable) { + _disableInitializers(); + } + } + + /// @inheritdoc IGoodDollarExpansionController + function initialize( + address _goodDollarExchangeProvider, + address _distributionHelper, + address _reserve, + address _avatar + ) public initializer { + __Pausable_init(); + __Ownable_init(); + + setGoodDollarExchangeProvider(_goodDollarExchangeProvider); + _setDistributionHelper(_distributionHelper); + setReserve(_reserve); + setAvatar(_avatar); + } + + /* =================================================== */ + /* ==================== Modifiers ==================== */ + /* =================================================== */ + + modifier onlyAvatar() { + require(msg.sender == AVATAR, "Only Avatar can call this function"); + _; + } + + /* ======================================================== */ + /* ==================== View Functions ==================== */ + /* ======================================================== */ + + /// @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 ==================== */ + /* ============================================================ */ + + /// @inheritdoc IGoodDollarExpansionController + 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); + } + + /// @inheritdoc IGoodDollarExpansionController + function setReserve(address _reserve) public onlyOwner { + require(_reserve != address(0), "Reserve address must be set"); + reserve = _reserve; + emit ReserveUpdated(_reserve); + } + + /// @inheritdoc IGoodDollarExpansionController + function setAvatar(address _avatar) public onlyOwner { + require(_avatar != address(0), "Avatar address must be set"); + AVATAR = _avatar; + emit AvatarUpdated(_avatar); + } + + /// @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"); + require(expansionFrequency > 0, "Expansion frequency must be greater than 0"); + + exchangeExpansionConfigs[exchangeId].expansionRate = expansionRate; + exchangeExpansionConfigs[exchangeId].expansionFrequency = expansionFrequency; + + emit ExpansionConfigSet(exchangeId, expansionRate, expansionFrequency); + } + + /// @inheritdoc IGoodDollarExpansionController + function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external { + require(reserveInterest > 0, "Reserve interest must be greater than 0"); + IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) + .getPoolExchange(exchangeId); + + uint256 amountToMint = goodDollarExchangeProvider.mintFromInterest(exchangeId, reserveInterest); + + require(IERC20(exchange.reserveAsset).transferFrom(msg.sender, reserve, reserveInterest), "Transfer failed"); + IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountToMint); + + // Ignored, because contracts only interacts with trusted contracts and tokens + // slither-disable-next-line reentrancy-events + emit InterestUBIMinted(exchangeId, amountToMint); + } + + /// @inheritdoc IGoodDollarExpansionController + function mintUBIFromReserveBalance(bytes32 exchangeId) external returns (uint256 amountMinted) { + IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) + .getPoolExchange(exchangeId); + + uint256 contractReserveBalance = IERC20(exchange.reserveAsset).balanceOf(reserve); + uint256 additionalReserveBalance = contractReserveBalance - exchange.reserveBalance; + if (additionalReserveBalance > 0) { + 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 + function mintUBIFromExpansion(bytes32 exchangeId) external returns (uint256 amountMinted) { + IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) + .getPoolExchange(exchangeId); + ExchangeExpansionConfig memory config = getExpansionConfig(exchangeId); + + bool shouldExpand = block.timestamp > config.lastExpansion + config.expansionFrequency; + if (shouldExpand || config.lastExpansion == 0) { + uint256 reserveRatioScalar = _getReserveRatioScalar(config); + + exchangeExpansionConfigs[exchangeId].lastExpansion = uint32(block.timestamp); + amountMinted = goodDollarExchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar); + + 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"); + 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 ==================== */ + /* =========================================================== */ + + /** + * @notice Sets the distribution helper address. + * @param _distributionHelper The address of the distribution helper contract. + */ + function _setDistributionHelper(address _distributionHelper) internal { + require(_distributionHelper != address(0), "Distribution helper address must be set"); + distributionHelper = IDistributionHelper(_distributionHelper); + emit DistributionHelperUpdated(_distributionHelper); + } + + /** + * @notice Calculates the reserve ratio scalar for the given expansion config. + * @param config The expansion config. + * @return reserveRatioScalar The reserve ratio scalar. + */ + function _getReserveRatioScalar(ExchangeExpansionConfig memory config) internal view returns (uint256) { + uint256 numberOfExpansions; + + // If there was no previous expansion, we expand once. + if (config.lastExpansion == 0) { + numberOfExpansions = 1; + } else { + numberOfExpansions = (block.timestamp - config.lastExpansion) / config.expansionFrequency; + } + + uint256 stepReserveRatioScalar = MAX_WEIGHT - config.expansionRate; + return unwrap(powu(wrap(stepReserveRatioScalar), numberOfExpansions)); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} diff --git a/contracts/goodDollar/interfaces/IGoodProtocol.sol b/contracts/goodDollar/interfaces/IGoodProtocol.sol new file mode 100644 index 00000000..1c774079 --- /dev/null +++ b/contracts/goodDollar/interfaces/IGoodProtocol.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity >=0.5.17 <0.8.19; +pragma experimental ABIEncoderV2; + +interface IGoodDollar { + function mint(address to, uint256 amount) external; + + function burn(uint256 amount) external; + + function safeTransferFrom(address from, address to, uint256 value) external; + + function addMinter(address _minter) external; + + function isMinter(address account) external view returns (bool); + + function balanceOf(address account) external view returns (uint256); + + // slither-disable-next-line erc721-interface + function approve(address spender, uint256 amount) external returns (bool); +} + +interface IDistributionHelper { + function onDistribution(uint256 _amount) external; +} diff --git a/contracts/import.sol b/contracts/import.sol index e74ef425..86143961 100644 --- a/contracts/import.sol +++ b/contracts/import.sol @@ -12,4 +12,3 @@ import "celo/contracts/common/Freezer.sol"; import "celo/contracts/stability/SortedOracles.sol"; import "test/utils/harnesses/WithThresholdHarness.sol"; import "test/utils/harnesses/WithCooldownHarness.sol"; -import "test/utils/harnesses/TradingLimitsHarness.sol"; diff --git a/contracts/interfaces/IBancorExchangeProvider.sol b/contracts/interfaces/IBancorExchangeProvider.sol new file mode 100644 index 00000000..9f491c97 --- /dev/null +++ b/contracts/interfaces/IBancorExchangeProvider.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.17 <0.8.19; +pragma experimental ABIEncoderV2; + +interface IBancorExchangeProvider { + struct PoolExchange { + address reserveAsset; + address tokenAddress; + uint256 tokenSupply; + uint256 reserveBalance; + uint32 reserveRatio; + uint32 exitContribution; + } + + /* ========================================== */ + /* ================= Events ================= */ + /* ========================================== */ + + /** + * @notice Emitted when the broker address is updated. + * @param newBroker The address of the new broker. + */ + event BrokerUpdated(address indexed newBroker); + + /** + * @notice Emitted when the reserve contract is set. + * @param newReserve The address of the new reserve. + */ + event ReserveUpdated(address indexed newReserve); + + /** + * @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 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 + */ + event ExchangeDestroyed(bytes32 indexed exchangeId, address indexed reserveAsset, address indexed tokenAddress); + + /** + * @notice Emitted when the exit contribution for a pool is set. + * @param exchangeId The id of the pool + * @param exitContribution The exit contribution + */ + 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; + + /** + * @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. + */ + function getPoolExchange(bytes32 exchangeId) external view returns (PoolExchange memory exchange); + + /** + * @notice Gets all pool IDs. + * @return exchangeIds List of the pool IDs. + */ + 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 + */ + 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; + + /** + * @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 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 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 675713bd..e0d3089f 100644 --- a/contracts/interfaces/IBroker.sol +++ b/contracts/interfaces/IBroker.sol @@ -38,76 +38,124 @@ 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) tuple. + * @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,39 +164,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 _reserve 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 _reserve) external; - - /// @notice IOwnable: - function transferOwnership(address newOwner) external; - - function renounceOwnership() external; - - function owner() external view returns (address); - - /// @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) external returns (uint256 index); - - function removeExchangeProvider(address exchangeProvider, uint256 index) external; - - function setReserve(address _reserve) 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/IBrokerAdmin.sol b/contracts/interfaces/IBrokerAdmin.sol index d0aba861..6cac3dd4 100644 --- a/contracts/interfaces/IBrokerAdmin.sol +++ b/contracts/interfaces/IBrokerAdmin.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity >=0.5.13 <0.8.19; /* * @title Broker Admin Interface @@ -38,11 +38,12 @@ interface IBrokerAdmin { * @param exchangeProvider The address of the ExchangeProvider to add. * @return index The index where the ExchangeProvider was inserted. */ - function addExchangeProvider(address exchangeProvider) external returns (uint256 index); + function addExchangeProvider(address exchangeProvider, address reserve) external returns (uint256 index); /** - * @notice Set the Mento reserve address. - * @param reserve The Mento reserve address. + * @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 setReserve(address reserve) external; + function setReserves(address[] calldata _exchangeProviders, address[] calldata _reserves) external; } 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 new file mode 100644 index 00000000..54328aeb --- /dev/null +++ b/contracts/interfaces/IGoodDollarExchangeProvider.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.17 <0.8.19; +pragma experimental ABIEncoderV2; + +interface IGoodDollarExchangeProvider { + /* ========================================== */ + /* ================= Events ================= */ + /* ========================================== */ + + /** + * @notice Emitted when the ExpansionController address is updated. + * @param expansionController The address of the ExpansionController contract. + */ + event ExpansionControllerUpdated(address indexed expansionController); + + /** + * @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 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 ================ */ + /* =========================================== */ + + /** + * @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, 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. + * @param reserveRatioScalar Scaler for calculating the new reserve ratio. + * @return amountToMint Amount of G$ tokens to be minted as a result of the expansion. + */ + function mintFromExpansion(bytes32 exchangeId, uint256 reserveRatioScalar) 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. + */ + function mintFromInterest(bytes32 exchangeId, uint256 reserveInterest) external returns (uint256 amountToMint); + + /** + * @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, disabling minting. + */ + function pause() external; + + /** + * @notice Unpauses the Exchange, enabling minting again. + */ + function unpause() external; +} diff --git a/contracts/interfaces/IGoodDollarExpansionController.sol b/contracts/interfaces/IGoodDollarExpansionController.sol new file mode 100644 index 00000000..2268ef97 --- /dev/null +++ b/contracts/interfaces/IGoodDollarExpansionController.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.17 <0.8.19; +pragma experimental ABIEncoderV2; + +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. + */ + struct ExchangeExpansionConfig { + uint64 expansionRate; + uint32 expansionFrequency; + uint32 lastExpansion; + } + + /* ------- Events ------- */ + + /** + * @notice Emitted when the GoodDollarExchangeProvider is updated. + * @param exchangeProvider The address of the new GoodDollarExchangeProvider. + */ + event GoodDollarExchangeProviderUpdated(address indexed exchangeProvider); + + /** + * @notice Emitted when the distribution helper is updated. + * @param distributionHelper The address of the new distribution helper. + */ + event DistributionHelperUpdated(address indexed distributionHelper); + + /** + * @notice Emitted when the Reserve address is updated. + * @param reserve The address of the new Reserve. + */ + event ReserveUpdated(address indexed reserve); + + /** + * @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 pool. + * @param exchangeId The ID of the pool. + * @param expansionRate The rate of expansion. + * @param expansionFrequency The frequency of expansion. + */ + event ExpansionConfigSet(bytes32 indexed exchangeId, uint64 expansionRate, uint32 expansionFrequency); + + /** + * @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 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 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 pool. + * @param amount The amount of G$ tokens minted. + */ + event ExpansionUBIMinted(bytes32 indexed exchangeId, uint256 amount); + + /* ------- Functions ------- */ + + /** + * @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, + address _reserve, + 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. + */ + function setGoodDollarExchangeProvider(address _goodDollarExchangeProvider) external; + + /** + * @notice Sets the distribution helper address. + * @param _distributionHelper The address of the distribution helper contract. + */ + function setDistributionHelper(address _distributionHelper) external; + + /** + * @notice Sets the reserve address. + * @param _reserve The address of the reserve contract. + */ + function setReserve(address _reserve) external; + + /** + * @notice Sets the AVATAR address. + * @param _avatar The address of the AVATAR contract. + */ + 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. + * @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. + * @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. + */ + 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. + */ + 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. + * @param to The address of the recipient. + * @param amount The amount of G$ tokens to mint. + */ + function mintRewardFromReserveRatio(bytes32 exchangeId, address to, uint256 amount) external; +} diff --git a/contracts/interfaces/IStableTokenV2.sol b/contracts/interfaces/IStableTokenV2.sol index cbf49418..10d2a077 100644 --- a/contracts/interfaces/IStableTokenV2.sol +++ b/contracts/interfaces/IStableTokenV2.sol @@ -70,9 +70,7 @@ interface IStableTokenV2 { */ function initializeV2(address _broker, address _validators, address _exchange) external; - /** - * @notice Gets the address of the Broker contract. - */ + /// @notice Gets the address of the Broker contract. function broker() external returns (address); /** diff --git a/contracts/libraries/TradingLimits.sol b/contracts/libraries/TradingLimits.sol index 66ad2cfd..14e59549 100644 --- a/contracts/libraries/TradingLimits.sol +++ b/contracts/libraries/TradingLimits.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity 0.8.18; pragma experimental ABIEncoderV2; import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; @@ -44,7 +44,7 @@ library TradingLimits { uint8 private constant L0 = 1; // 0b001 Limit0 uint8 private constant L1 = 2; // 0b010 Limit1 uint8 private constant LG = 4; // 0b100 LimitGlobal - int48 private constant MAX_INT48 = int48(uint48(-1) / 2); + int48 private constant MAX_INT48 = type(int48).max; /** * @notice Validate a trading limit configuration. @@ -129,7 +129,11 @@ library TradingLimits { ) internal view returns (ITradingLimits.State memory) { int256 _deltaFlowUnits = _deltaFlow / int256((10 ** uint256(decimals))); require(_deltaFlowUnits <= MAX_INT48, "dFlow too large"); - int48 deltaFlowUnits = _deltaFlowUnits == 0 ? 1 : int48(_deltaFlowUnits); + + int48 deltaFlowUnits = int48(_deltaFlowUnits); + if (deltaFlowUnits == 0) { + deltaFlowUnits = _deltaFlow > 0 ? int48(1) : int48(-1); + } if (config.flags & L0 > 0) { if (block.timestamp > self.lastUpdated0 + config.timestep0) { diff --git a/contracts/swap/Broker.sol b/contracts/swap/Broker.sol index 234172c0..24cdd43e 100644 --- a/contracts/swap/Broker.sol +++ b/contracts/swap/Broker.sol @@ -1,22 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity 0.8.18; pragma experimental ABIEncoderV2; -import { Ownable } from "openzeppelin-solidity/contracts/ownership/Ownable.sol"; -import { SafeERC20 } from "openzeppelin-solidity/contracts/token/ERC20/SafeERC20.sol"; -import { IERC20 } from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; -import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import { Ownable } from "openzeppelin-contracts-next/contracts/access/Ownable.sol"; +import { SafeERC20MintableBurnable } from "contracts/common/SafeERC20MintableBurnable.sol"; +import { IERC20MintableBurnable as IERC20 } from "contracts/common/IERC20MintableBurnable.sol"; import { IExchangeProvider } from "../interfaces/IExchangeProvider.sol"; import { IBroker } from "../interfaces/IBroker.sol"; import { IBrokerAdmin } from "../interfaces/IBrokerAdmin.sol"; import { IReserve } from "../interfaces/IReserve.sol"; -import { IStableTokenV2 } from "../interfaces/IStableTokenV2.sol"; import { ITradingLimits } from "../interfaces/ITradingLimits.sol"; import { TradingLimits } from "../libraries/TradingLimits.sol"; import { Initializable } from "celo/contracts/common/Initializable.sol"; -import { ReentrancyGuard } from "celo/contracts/common/libraries/ReentrancyGuard.sol"; +import { ReentrancyGuard } from "openzeppelin-contracts-next/contracts/security/ReentrancyGuard.sol"; interface IERC20Metadata { /** @@ -32,90 +30,86 @@ interface IERC20Metadata { contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuard { using TradingLimits for ITradingLimits.State; using TradingLimits for ITradingLimits.Config; - using SafeERC20 for IERC20; - using SafeMath for uint256; + using SafeERC20MintableBurnable for IERC20; /* ==================== 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. - IReserve public 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(-1) / 2; + uint256 private constant MAX_INT256 = uint256(type(int256).max); + mapping(address => address) public exchangeReserve; /* ==================== Constructor ==================== */ /** * @notice Sets initialized == true on implementation contracts. * @param test Set to true to skip implementation initialization. */ - constructor(bool test) public Initializable(test) {} + constructor(bool test) Initializable(test) {} - /** - * @notice Allows the contract to be upgradable via the proxy. - * @param _exchangeProviders The addresses of the ExchangeProvider contracts. - * @param _reserve The address of the Reserve contract. - */ - function initialize(address[] calldata _exchangeProviders, address _reserve) external initializer { + /// @inheritdoc IBroker + function initialize(address[] calldata _exchangeProviders, address[] calldata _reserves) external initializer { _transferOwnership(msg.sender); for (uint256 i = 0; i < _exchangeProviders.length; i++) { - addExchangeProvider(_exchangeProviders[i]); + addExchangeProvider(_exchangeProviders[i], _reserves[i]); + } + } + + /// @inheritdoc IBroker + function setReserves( + address[] calldata _exchangeProviders, + address[] calldata _reserves + ) external override(IBroker, IBrokerAdmin) onlyOwner { + for (uint256 i = 0; i < _exchangeProviders.length; i++) { + require(isExchangeProvider[_exchangeProviders[i]], "ExchangeProvider does not exist"); + require(_reserves[i] != address(0), "Reserve address can't be 0"); + exchangeReserve[_exchangeProviders[i]] = _reserves[i]; + emit ReserveSet(_exchangeProviders[i], _reserves[i]); } - setReserve(_reserve); } /* ==================== Mutative Functions ==================== */ - /** - * @notice Add an exchange provider to the list of providers. - * @param exchangeProvider The address of the exchange provider to add. - * @return index The index of the newly added specified exchange provider. - */ - function addExchangeProvider(address exchangeProvider) public onlyOwner returns (uint256 index) { + /// @inheritdoc IBroker + function addExchangeProvider( + address exchangeProvider, + address reserve + ) public override(IBroker, IBrokerAdmin) onlyOwner returns (uint256 index) { require(!isExchangeProvider[exchangeProvider], "ExchangeProvider already exists in the list"); require(exchangeProvider != address(0), "ExchangeProvider address can't be 0"); + require(reserve != address(0), "Reserve address can't be 0"); exchangeProviders.push(exchangeProvider); isExchangeProvider[exchangeProvider] = true; + exchangeReserve[exchangeProvider] = reserve; emit ExchangeProviderAdded(exchangeProvider); - index = exchangeProviders.length.sub(1); + emit ReserveSet(exchangeProvider, reserve); + 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. - */ - function removeExchangeProvider(address exchangeProvider, uint256 index) public onlyOwner { + /// @inheritdoc IBroker + function removeExchangeProvider( + address exchangeProvider, + uint256 index + ) public override(IBroker, IBrokerAdmin) onlyOwner { require(exchangeProviders[index] == exchangeProvider, "index doesn't match provider"); - exchangeProviders[index] = exchangeProviders[exchangeProviders.length.sub(1)]; + exchangeProviders[index] = exchangeProviders[exchangeProviders.length - 1]; exchangeProviders.pop(); delete isExchangeProvider[exchangeProvider]; + delete exchangeReserve[exchangeProvider]; emit ExchangeProviderRemoved(exchangeProvider); } - /** - * @notice Set the Mento reserve address. - * @param _reserve The Mento reserve address. - */ - function setReserve(address _reserve) public onlyOwner { - require(_reserve != address(0), "Reserve address must be set"); - emit ReserveSet(_reserve, address(reserve)); - reserve = IReserve(_reserve); - } - - /** - * @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, @@ -124,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, @@ -145,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, @@ -170,21 +155,14 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar amountOut = IExchangeProvider(exchangeProvider).swapIn(exchangeId, tokenIn, tokenOut, amountIn); require(amountOut >= amountOutMin, "amountOutMin not met"); guardTradingLimits(exchangeId, tokenIn, amountIn, tokenOut, amountOut); - transferIn(msg.sender, tokenIn, amountIn); - transferOut(msg.sender, tokenOut, amountOut); + + address reserve = exchangeReserve[exchangeProvider]; + transferIn(payable(msg.sender), tokenIn, amountIn, reserve); + transferOut(payable(msg.sender), tokenOut, amountOut, reserve); 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, @@ -198,41 +176,26 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar amountIn = IExchangeProvider(exchangeProvider).swapOut(exchangeId, tokenIn, tokenOut, amountOut); require(amountIn <= amountInMax, "amountInMax exceeded"); guardTradingLimits(exchangeId, tokenIn, amountIn, tokenOut, amountOut); - transferIn(msg.sender, tokenIn, amountIn); - transferOut(msg.sender, tokenOut, amountOut); + + address reserve = exchangeReserve[exchangeProvider]; + transferIn(payable(msg.sender), tokenIn, amountIn, reserve); + transferOut(payable(msg.sender), tokenOut, amountOut, reserve); 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) { - require(reserve.isStableAsset(token), "Token must be a reserve stable asset"); IERC20(token).safeTransferFrom(msg.sender, address(this), amount); - require(IStableTokenV2(token).burn(amount), "Burning of the stable asset failed"); + 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))); @@ -250,10 +213,12 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar * @param to The address receiving the asset. * @param token The asset to transfer. * @param amount The amount of `token` to be transferred. + * @param _reserve The address of the corresponding reserve. */ - function transferOut(address payable to, address token, uint256 amount) internal { + function transferOut(address payable to, address token, uint256 amount, address _reserve) internal { + IReserve reserve = IReserve(_reserve); if (reserve.isStableAsset(token)) { - require(IStableTokenV2(token).mint(to, amount), "Minting of the stable asset failed"); + IERC20(token).safeMint(to, amount); } else if (reserve.isCollateralAsset(token)) { require(reserve.transferExchangeCollateralAsset(token, to, amount), "Transfer of the collateral asset failed"); } else { @@ -268,11 +233,13 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar * @param from The address to transfer the asset from. * @param token The asset to transfer. * @param amount The amount of `token` to be transferred. + * @param _reserve The address of the corresponding reserve. */ - function transferIn(address payable from, address token, uint256 amount) internal { + function transferIn(address payable from, address token, uint256 amount, address _reserve) internal { + IReserve reserve = IReserve(_reserve); if (reserve.isStableAsset(token)) { IERC20(token).safeTransferFrom(from, address(this), amount); - require(IStableTokenV2(token).burn(amount), "Burning of the stable asset failed"); + IERC20(token).safeBurn(amount); } else if (reserve.isCollateralAsset(token)) { IERC20(token).safeTransferFrom(from, address(reserve), amount); } else { diff --git a/foundry.toml b/foundry.toml index d1bc4ab2..89127769 100644 --- a/foundry.toml +++ b/foundry.toml @@ -14,13 +14,9 @@ no_match_contract = "ForkTest" gas_limit = 9223372036854775807 -allow_paths = [ - "node_modules/@celo" -] +allow_paths = ["node_modules/@celo"] -fs_permissions = [ - { access = "read", path = "out" } -] +fs_permissions = [{ access = "read", path = "out" }] [profile.ci] fuzz_runs = 1_000 @@ -35,6 +31,5 @@ no_match_contract = "_random" # in order to reset the no_match_contract match_contract = "ForkTest" [rpc_endpoints] -celo="${CELO_MAINNET_RPC_URL}" -baklava="${BAKLAVA_RPC_URL}" -alfajores="${ALFAJORES_RPC_URL}" +celo = "${CELO_MAINNET_RPC_URL}" +alfajores = "${ALFAJORES_RPC_URL}" diff --git a/lib/forge-std b/lib/forge-std index 1714bee7..035de35f 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d +Subproject commit 035de35f5e366c8d6ed142aec4ccb57fe2dd87d4 diff --git a/package.json b/package.json index 01572f79..7d828e81 100644 --- a/package.json +++ b/package.json @@ -33,14 +33,15 @@ "prettier:check": "prettier --config \"./.prettierrc.yml\" --check \"**/*.{json,md,sol,yml}\"", "solhint": "yarn solhint:contracts && yarn solhint:tests", "solhint:contracts": "solhint --config \"./.solhint.json\" \"contracts/**/*.sol\" -w 0", - "solhint:tests": "solhint --config \"./.solhint.test.json\" \"test/**/*.sol\" -w 0", + "solhint:tests": "solhint --config \"./test/.solhint.json\" \"test/**/*.sol\" -w 0", "test": "forge test", "fork-test": "env FOUNDRY_PROFILE=fork-tests forge test", - "fork-test:baklava": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract Baklava", "fork-test:alfajores": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract Alfajores", "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/**/*\"" + "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": { "@celo/contracts": "^11.0.0" diff --git a/.solhint.test.json b/test/.solhint.json similarity index 66% rename from .solhint.test.json rename to test/.solhint.json index 6d67cf31..de2b87b7 100644 --- a/.solhint.test.json +++ b/test/.solhint.json @@ -2,39 +2,24 @@ "extends": "solhint:recommended", "plugins": ["prettier"], "rules": { - "one-contract-per-file": "off", - "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", + "const-name-snakecase": "off", + "contract-name-camelcase": "off", + "func-name-mixedcase": "off", + "func-visibility": ["error", { "ignoreConstructors": true }], "function-max-lines": ["error", 121], "gas-custom-errors": "off", + "max-line-length": ["error", 121], "max-states-count": "off", - "var-name-mixedcase": "off", - "func-name-mixedcase": "off", - "state-visibility": "off", - "const-name-snakecase": "off", - "contract-name-camelcase": "off", + "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", + "one-contract-per-file": "off", + "prettier/prettier": ["error", { "endOfLine": "auto" }], + "reason-string": ["warn", { "maxLength": 64 }], + "state-visibility": "off", + "var-name-mixedcase": "off" } } diff --git a/test/fork/BancorExchangeProviderForkTest.sol b/test/fork/BancorExchangeProviderForkTest.sol new file mode 100644 index 00000000..ad2c4d71 --- /dev/null +++ b/test/fork/BancorExchangeProviderForkTest.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +// Libraries +import { ERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/ERC20.sol"; + +// Contracts +import { BaseForkTest } from "./BaseForkTest.sol"; +import { BancorExchangeProvider } from "contracts/goodDollar/BancorExchangeProvider.sol"; +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; + +contract BancorExchangeProviderForkTest is BaseForkTest { + address ownerAddress; + address brokerAddress; + address reserveAddress; + ERC20 reserveToken; + ERC20 swapToken; + + BancorExchangeProvider bancorExchangeProvider; + IBancorExchangeProvider.PoolExchange poolExchange; + bytes32 exchangeId; + + constructor(uint256 _chainId) BaseForkTest(_chainId) {} + + function setUp() public override { + super.setUp(); + ownerAddress = makeAddr("owner"); + brokerAddress = address(this.broker()); + reserveAddress = address(mentoReserve); + reserveToken = ERC20(address(mentoReserve.collateralAssets(0))); // == CELO + swapToken = ERC20(this.lookup("StableToken")); // == cUSD + + // Deploy and initialize BancorExchangeProvider (includes BancorFormula as part of init) + setUpBancorExchangeProvider(); + } + + function setUpBancorExchangeProvider() public { + vm.startPrank(ownerAddress); + bancorExchangeProvider = new BancorExchangeProvider(false); + bancorExchangeProvider.initialize(brokerAddress, reserveAddress); + vm.stopPrank(); + + poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(swapToken), + tokenSupply: 300_000 * 1e18, + reserveBalance: 60_000 * 1e18, + reserveRatio: 0.2 * 1e8, + exitContribution: 0.01 * 1e8 + }); + + vm.prank(ownerAddress); + exchangeId = bancorExchangeProvider.createExchange(poolExchange); + } + + function test_init_isDeployedAndInitializedCorrectly() public view { + assertEq(bancorExchangeProvider.owner(), ownerAddress); + assertEq(bancorExchangeProvider.broker(), brokerAddress); + assertEq(address(bancorExchangeProvider.reserve()), reserveAddress); + + IBancorExchangeProvider.PoolExchange memory _poolExchange = bancorExchangeProvider.getPoolExchange(exchangeId); + assertEq(_poolExchange.reserveAsset, _poolExchange.reserveAsset); + assertEq(_poolExchange.tokenAddress, _poolExchange.tokenAddress); + assertEq(_poolExchange.tokenSupply, _poolExchange.tokenSupply); + assertEq(_poolExchange.reserveBalance, _poolExchange.reserveBalance); + assertEq(_poolExchange.reserveRatio, _poolExchange.reserveRatio); + assertEq(_poolExchange.exitContribution, _poolExchange.exitContribution); + } + + function test_swapIn_whenTokenInIsReserveToken_shouldSwapIn() public { + uint256 amountIn = 1e18; + uint256 reserveBalanceBefore = poolExchange.reserveBalance; + uint256 swapTokenSupplyBefore = poolExchange.tokenSupply; + + uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(swapToken), + amountIn: amountIn + }); + vm.prank(brokerAddress); + uint256 amountOut = bancorExchangeProvider.swapIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(swapToken), + amountIn: amountIn + }); + assertEq(amountOut, expectedAmountOut); + + (, , uint256 swapTokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore + amountIn); + assertEq(swapTokenSupplyAfter, swapTokenSupplyBefore + amountOut); + } + + function test_swapIn_whenTokenInIsSwapToken_shouldSwapIn() public { + uint256 amountIn = 1e18; + uint256 reserveBalanceBefore = poolExchange.reserveBalance; + uint256 swapTokenSupplyBefore = poolExchange.tokenSupply; + uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut( + exchangeId, + address(swapToken), + address(reserveToken), + amountIn + ); + vm.prank(brokerAddress); + uint256 amountOut = bancorExchangeProvider.swapIn({ + exchangeId: exchangeId, + tokenIn: address(swapToken), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + assertEq(amountOut, expectedAmountOut); + + (, , uint256 swapTokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); + assertEq(swapTokenSupplyAfter, swapTokenSupplyBefore - amountIn); + } + + function test_swapOut_whenTokenInIsReserveToken_shouldSwapOut() public { + uint256 amountOut = 1e18; + uint256 reserveBalanceBefore = poolExchange.reserveBalance; + uint256 swapTokenSupplyBefore = poolExchange.tokenSupply; + uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn( + exchangeId, + address(reserveToken), + address(swapToken), + amountOut + ); + vm.prank(brokerAddress); + uint256 amountIn = bancorExchangeProvider.swapOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(swapToken), + amountOut: amountOut + }); + assertEq(amountIn, expectedAmountIn); + + (, , uint256 swapTokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore + amountIn); + assertEq(swapTokenSupplyAfter, swapTokenSupplyBefore + amountOut); + } + + function test_swapOut_whenTokenInIsSwapToken_shouldSwapOut() public { + uint256 amountOut = 1e18; + uint256 reserveBalanceBefore = poolExchange.reserveBalance; + uint256 swapTokenSupplyBefore = poolExchange.tokenSupply; + uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn( + exchangeId, + address(swapToken), + address(reserveToken), + amountOut + ); + vm.prank(brokerAddress); + + uint256 amountIn = bancorExchangeProvider.swapOut({ + exchangeId: exchangeId, + tokenIn: address(swapToken), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + assertEq(amountIn, expectedAmountIn); + + (, , uint256 swapTokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); + assertEq(swapTokenSupplyAfter, swapTokenSupplyBefore - amountIn); + } +} diff --git a/test/fork/BaseForkTest.sol b/test/fork/BaseForkTest.sol index fe97e8a0..3bda5146 100644 --- a/test/fork/BaseForkTest.sol +++ b/test/fork/BaseForkTest.sol @@ -1,18 +1,25 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count pragma solidity ^0.8; +// Libraries import { Test } from "mento-std/Test.sol"; import { CELO_REGISTRY_ADDRESS } from "mento-std/Constants.sol"; - import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; -import { IRegistry } from "celo/contracts/common/interfaces/IRegistry.sol"; +// Interfaces +import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; import { IBroker } from "contracts/interfaces/IBroker.sol"; +import { ICeloProxy } from "contracts/interfaces/ICeloProxy.sol"; +import { IOwnable } from "contracts/interfaces/IOwnable.sol"; +import { IRegistry } from "celo/contracts/common/interfaces/IRegistry.sol"; import { IReserve } from "contracts/interfaces/IReserve.sol"; import { ISortedOracles } from "contracts/interfaces/ISortedOracles.sol"; import { ITradingLimitsHarness } from "test/utils/harnesses/ITradingLimitsHarness.sol"; + +// Contracts & Utils +import { Broker } from "contracts/swap/Broker.sol"; +import { TradingLimitsHarness } from "test/utils/harnesses/TradingLimitsHarness.sol"; import { toRateFeed } from "./helpers/misc.sol"; interface IMint { @@ -22,12 +29,11 @@ interface IMint { /** * @title BaseForkTest * @notice Fork tests for Mento! - * This test suite tests invariantes on a fork of a live Mento environemnts. - * The philosophy is to test in accordance with how the target fork is configured, - * therfore it doesn't make assumptions about the systems, nor tries to configure - * the system to test specific scenarios. - * However, it should be exausitve in testing invariants across all tradable pairs - * in the system, therefore each test should. + * This test suite tests invariants on a fork of a live Mento environments. + * The philosophy is to test in accordance with how the target fork is configured. + * Therefore, it doesn't make assumptions about the systems, nor tries to configure + * the system to test specific scenarios. However, it should be exhaustive in testing + * invariants across all tradable pairs in the system. */ abstract contract BaseForkTest is Test { using FixidityLib for FixidityLib.Fraction; @@ -36,9 +42,10 @@ abstract contract BaseForkTest is Test { address governance; IBroker public broker; + IBiPoolManager biPoolManager; IBreakerBox public breakerBox; ISortedOracles public sortedOracles; - IReserve public reserve; + IReserve public mentoReserve; ITradingLimitsHarness public tradingLimits; address public trader; @@ -65,30 +72,51 @@ abstract contract BaseForkTest is Test { function setUp() public virtual { fork(targetChainId); - // @dev Updaing the target fork block every 200 blocks, about ~8 min. - // This means that when running locally RPC calls will be cached. + /// @dev Updating the target fork block every 200 blocks, about ~8 min. + /// This means that, when running locally, RPC calls will be cached. fork(targetChainId, (block.number / 100) * 100); // The precompile handler needs to be reinitialized after forking. __CeloPrecompiles_init(); - tradingLimits = ITradingLimitsHarness(deployCode("TradingLimitsHarness")); + tradingLimits = new TradingLimitsHarness(); + broker = IBroker(lookup("Broker")); + biPoolManager = IBiPoolManager(broker.exchangeProviders(0)); sortedOracles = ISortedOracles(lookup("SortedOracles")); governance = lookup("Governance"); breakerBox = IBreakerBox(address(sortedOracles.breakerBox())); vm.label(address(breakerBox), "BreakerBox"); trader = makeAddr("trader"); - reserve = IReserve(broker.reserve()); + mentoReserve = IReserve(lookup("Reserve")); + + setUpBroker(); - /// @dev Hardcoded number of dependencies for each ratefeed. - /// Should be updated when they change, there is a test that will - /// validate that. + /// @dev Hardcoded number of dependencies for each rate feed. + /// Should be updated when they change, there is a test that + /// will validate that. rateFeedDependenciesCount[lookup("StableTokenXOF")] = 2; rateFeedDependenciesCount[toRateFeed("EUROCXOF")] = 2; rateFeedDependenciesCount[toRateFeed("USDCEUR")] = 1; rateFeedDependenciesCount[toRateFeed("USDCBRL")] = 1; } + // TODO: Broker setup can be removed after the Broker changes have been deployed to Mainnet + function setUpBroker() internal { + Broker newBrokerImplementation = new Broker(false); + vm.prank(IOwnable(address(broker)).owner()); + ICeloProxy(address(broker))._setImplementation(address(newBrokerImplementation)); + address brokerImplAddressAfterUpgrade = ICeloProxy(address(broker))._getImplementation(); + assert(address(newBrokerImplementation) == brokerImplAddressAfterUpgrade); + + address[] memory exchangeProviders = new address[](1); + exchangeProviders[0] = address(biPoolManager); + address[] memory reserves = new address[](1); + reserves[0] = address(mentoReserve); + + vm.prank(IOwnable(address(broker)).owner()); + broker.setReserves(exchangeProviders, reserves); + } + function mint(address asset, address to, uint256 amount, bool updateSupply) public { if (asset == lookup("GoldToken")) { if (!updateSupply) { diff --git a/test/fork/ChainForkTest.sol b/test/fork/ChainForkTest.sol index c6f4c321..b57c7479 100644 --- a/test/fork/ChainForkTest.sol +++ b/test/fork/ChainForkTest.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count pragma solidity ^0.8; import { console } from "forge-std/console.sol"; @@ -28,12 +27,12 @@ abstract contract ChainForkTest is BaseForkTest { IBiPoolManager biPoolManager = IBiPoolManager(broker.getExchangeProviders()[0]); vm.expectRevert("contract already initialized"); - biPoolManager.initialize(address(broker), reserve, sortedOracles, breakerBox); + biPoolManager.initialize(address(broker), mentoReserve, sortedOracles, breakerBox); } function test_brokerCanNotBeReinitialized() public { vm.expectRevert("contract already initialized"); - broker.initialize(new address[](0), address(reserve)); + broker.initialize(new address[](0), new address[](0)); } function test_sortedOraclesCanNotBeReinitialized() public { @@ -43,7 +42,7 @@ abstract contract ChainForkTest is BaseForkTest { function test_reserveCanNotBeReinitialized() public { vm.expectRevert("contract already initialized"); - reserve.initialize( + mentoReserve.initialize( address(10), 0, 0, @@ -83,10 +82,10 @@ abstract contract ChainForkTest is BaseForkTest { function test_numberCollateralAssetsCount() public { address collateral; for (uint256 i = 0; i < COLLATERAL_ASSETS_COUNT; i++) { - collateral = reserve.collateralAssets(i); + collateral = mentoReserve.collateralAssets(i); } vm.expectRevert(); - reserve.collateralAssets(COLLATERAL_ASSETS_COUNT); + mentoReserve.collateralAssets(COLLATERAL_ASSETS_COUNT); } function test_stableTokensCanNotBeReinitialized() public { diff --git a/test/fork/ExchangeForkTest.sol b/test/fork/ExchangeForkTest.sol index 61ec0d77..e462f045 100644 --- a/test/fork/ExchangeForkTest.sol +++ b/test/fork/ExchangeForkTest.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count pragma solidity ^0.8; import { console } from "forge-std/console.sol"; @@ -49,11 +48,16 @@ abstract contract ExchangeForkTest is SwapAssertions, CircuitBreakerAssertions, super.setUp(); loadExchange(); - console.log("%s | %s | %s", this.ticker(), exchangeProviderAddr, vm.toString(exchangeId)); + console.log( + "%s | ExchangeProvider: %s | ExchangeID: %s", + this.ticker(), + exchangeProviderAddr, + vm.toString(exchangeId) + ); for (uint256 i = 0; i < COLLATERAL_ASSETS_COUNT; i++) { - address collateralAsset = reserve.collateralAssets(i); + address collateralAsset = mentoReserve.collateralAssets(i); vm.label(collateralAsset, IERC20(collateralAsset).symbol()); - mint(collateralAsset, address(reserve), uint256(25_000_000).toSubunits(collateralAsset), true); + mint(collateralAsset, address(mentoReserve), uint256(25_000_000).toSubunits(collateralAsset), true); } } diff --git a/test/fork/ForkTests.t.sol b/test/fork/ForkTests.t.sol index 0aaf6373..d3ab0b48 100644 --- a/test/fork/ForkTests.t.sol +++ b/test/fork/ForkTests.t.sol @@ -1,22 +1,22 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, max-line-length pragma solidity ^0.8; /** @dev Fork tests for Mento! -This test suite tests invariants on a fork of a live Mento environemnts. +This test suite tests invariants on a fork of a live Mento environment. -Thare are two types of tests contracts: -- ChainForkTests: Tests that are specific to the chain, such as the number of exchanges, the number of collateral assets, contract initialization state, etc. +Thare are two types of test contracts: +- ChainForkTests: Tests that are specific to the chain, such as the number of exchanges, the number of collateral + assets, contract initialization state, etc. - ExchangeForkTests: Tests that are specific to the exchange, such as trading limits, swaps, circuit breakers, etc. To make it easier to debug and develop, we have one ChainForkTest for each chain (Alfajores, Celo) and one ExchangeForkTest for each exchange provider and exchange pair. -The ChainFork tests are instantiated with: +The ChainForkTests are instantiated with: - Chain ID. - Expected number of exchange providers. - Expected number of exchanges per exchange provider. -If any of this assertions fail then the ChainTest will fail and that's the queue to update this file +If any of these assertions fail, then the ChainForkTest will fail and that's the cue to update this file and add additional ExchangeForkTests. The ExchangeForkTests are instantiated with: @@ -24,22 +24,26 @@ The ExchangeForkTests are instantiated with: - Exchange Provider Index. - Exchange Index. -And the naming convetion for them is: -${ChainName}_P${ExchangeProviderIndex}E${ExchangeIndex}_ExchangeForkTest -e.g. Alfajores_P0E00_ExchangeForkTest (Alfajores, Exchange Provider 0, Exchange 0) -The Exchange Index is 0 padded to make them align nicely in the file, exchange provider counts shouldn't -exceed 10, if they do, then we need to update the naming convention. +And the naming convention for them is: +- ${ChainName}_P${ExchangeProviderIndex}E${ExchangeIndex}_ExchangeForkTest +- e.g. "Alfajores_P0E00_ExchangeForkTest (Alfajores, Exchange Provider 0, Exchange 0)" +The Exchange Index is 0 padded to make them align nicely in the file. +Exchange provider counts shouldn't exceed 10. If they do, then we need to update the naming convention. This makes it easy to drill into which exchange is failing and debug it like: -$ env FOUNDRY_PROFILE=fork-tests forge test --match-contract CELO_P0E12 +- `$ env FOUNDRY_PROFILE=fork-tests forge test --match-contract CELO_P0E12` or run all tests for a chain: -$ env FOUNDRY_PROFILE=fork-tests forge test --match-contract Alfajores +- `$ env FOUNDRY_PROFILE=fork-tests forge test --match-contract Alfajores` */ +import { CELO_ID, ALFAJORES_ID } from "mento-std/Constants.sol"; import { uints } from "mento-std/Array.sol"; import { ChainForkTest } from "./ChainForkTest.sol"; import { ExchangeForkTest } from "./ExchangeForkTest.sol"; -import { CELO_ID, ALFAJORES_ID } from "mento-std/Constants.sol"; +import { BancorExchangeProviderForkTest } from "./BancorExchangeProviderForkTest.sol"; +import { GoodDollarTradingLimitsForkTest } from "./GoodDollar/TradingLimitsForkTest.sol"; +import { GoodDollarSwapForkTest } from "./GoodDollar/SwapForkTest.sol"; +import { GoodDollarExpansionForkTest } from "./GoodDollar/ExpansionForkTest.sol"; contract Alfajores_ChainForkTest is ChainForkTest(ALFAJORES_ID, 1, uints(15)) {} @@ -104,3 +108,11 @@ contract Celo_P0E12_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 12) {} contract Celo_P0E13_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 13) {} contract Celo_P0E14_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 14) {} + +contract Celo_BancorExchangeProviderForkTest is BancorExchangeProviderForkTest(CELO_ID) {} + +contract Celo_GoodDollarTradingLimitsForkTest is GoodDollarTradingLimitsForkTest(CELO_ID) {} + +contract Celo_GoodDollarSwapForkTest is GoodDollarSwapForkTest(CELO_ID) {} + +contract Celo_GoodDollarExpansionForkTest is GoodDollarExpansionForkTest(CELO_ID) {} diff --git a/test/fork/GoodDollar/ExpansionForkTest.sol b/test/fork/GoodDollar/ExpansionForkTest.sol new file mode 100644 index 00000000..c1f50e8f --- /dev/null +++ b/test/fork/GoodDollar/ExpansionForkTest.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +// Libraries +import { TokenHelpers } from "../helpers/TokenHelpers.sol"; +import { TradingLimitHelpers } from "../helpers/TradingLimitHelpers.sol"; + +// Interfaces +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; +import { IDistributionHelper } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; + +// Contracts +import { GoodDollarBaseForkTest } from "./GoodDollarBaseForkTest.sol"; + +contract GoodDollarExpansionForkTest is GoodDollarBaseForkTest { + using TradingLimitHelpers for *; + using TokenHelpers for *; + + constructor(uint256 _chainId) GoodDollarBaseForkTest(_chainId) {} + + function setUp() public override { + super.setUp(); + } + + function test_mintFromExpansion() public { + uint256 priceBefore = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 distributionHelperBalanceBefore = goodDollarToken.balanceOf(distributionHelperAddress); + + vm.mockCall( + distributionHelperAddress, + abi.encodeWithSelector(IDistributionHelper(distributionHelperAddress).onDistribution.selector), + abi.encode(true) + ); + + skip(2 days + 1 seconds); + + uint256 amountMinted = expansionController.mintUBIFromExpansion(exchangeId); + uint256 priceAfter = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + assertApproxEqAbs(priceBefore, priceAfter, 1e11); + assertEq(goodDollarToken.balanceOf(distributionHelperAddress), amountMinted + distributionHelperBalanceBefore); + } + + function test_mintFromInterest() public { + uint256 priceBefore = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + address reserveInterestCollector = makeAddr("reserveInterestCollector"); + uint256 reserveInterest = 1000 * 1e18; + deal(address(reserveToken), reserveInterestCollector, reserveInterest); + + uint256 reserveBalanceBefore = reserveToken.balanceOf(address(goodDollarReserve)); + uint256 interestCollectorBalanceBefore = reserveToken.balanceOf(reserveInterestCollector); + uint256 distributionHelperBalanceBefore = goodDollarToken.balanceOf(distributionHelperAddress); + + vm.startPrank(reserveInterestCollector); + reserveToken.approve(address(expansionController), reserveInterest); + expansionController.mintUBIFromInterest(exchangeId, reserveInterest); + vm.stopPrank(); + + uint256 priceAfter = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 reserveBalanceAfter = reserveToken.balanceOf(address(goodDollarReserve)); + uint256 interestCollectorBalanceAfter = reserveToken.balanceOf(reserveInterestCollector); + uint256 distributionHelperBalanceAfter = goodDollarToken.balanceOf(distributionHelperAddress); + + assertEq(reserveBalanceAfter, reserveBalanceBefore + reserveInterest); + assertEq(interestCollectorBalanceAfter, interestCollectorBalanceBefore - reserveInterest); + assertTrue(distributionHelperBalanceBefore < distributionHelperBalanceAfter); + assertEq(priceBefore, priceAfter); + } +} diff --git a/test/fork/GoodDollar/GoodDollarBaseForkTest.sol b/test/fork/GoodDollar/GoodDollarBaseForkTest.sol new file mode 100644 index 00000000..02fc09ad --- /dev/null +++ b/test/fork/GoodDollar/GoodDollarBaseForkTest.sol @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +// Libraries / Helpers / Utils +import { console } from "forge-std/console.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; +import { TokenHelpers } from "../helpers/TokenHelpers.sol"; +import { L0, L1, LG, min } from "../helpers/misc.sol"; + +// Interfaces +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; +import { IGoodDollar } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; +import { IReserve } from "contracts/interfaces/IReserve.sol"; +import { IStableTokenV2 } from "contracts/interfaces/IStableTokenV2.sol"; +import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; + +// Contracts +import { BaseForkTest } from "../BaseForkTest.sol"; +import { Broker } from "contracts/swap/Broker.sol"; +import { GoodDollarExchangeProvider } from "contracts/goodDollar/GoodDollarExchangeProvider.sol"; +import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarExpansionController.sol"; + +contract GoodDollarBaseForkTest is BaseForkTest { + using FixidityLib for FixidityLib.Fraction; + using TokenHelpers for *; + + // Addresses + address constant AVATAR_ADDRESS = 0x495d133B938596C9984d462F007B676bDc57eCEC; + address constant CUSD_ADDRESS = 0x765DE816845861e75A25fCA122bb6898B8B1282a; + address constant GOOD_DOLLAR_ADDRESS = 0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A; + address constant REGISTRY_ADDRESS = 0x000000000000000000000000000000000000ce10; + address ownerAddress; + address distributionHelperAddress = makeAddr("distributionHelper"); + + // GoodDollar Relaunch Config + uint256 constant INITIAL_RESERVE_BALANCE = 200_000 * 1e18; + uint256 constant INITIAL_GOOD_DOLLAR_TOKEN_SUPPLY = 7_000_000_000 * 1e18; + uint32 constant INITIAL_RESERVE_RATIO = 0.28571428 * 1e8; + uint32 constant INITIAL_EXIT_CONTRIBUTION = 0.1 * 1e8; + uint64 constant INITIAL_EXPANSION_RATE = uint64(288617289022312); // == ~10% per year (assuming daily expansion) + uint32 constant INITIAL_EXPANSION_FREQUENCY = uint32(1 days); // Daily expansion + + // Tokens + IStableTokenV2 reserveToken; + IGoodDollar goodDollarToken; + + // Contracts + IReserve public goodDollarReserve; + GoodDollarExchangeProvider goodDollarExchangeProvider; + GoodDollarExpansionController expansionController; + + IBancorExchangeProvider.PoolExchange public poolExchange; + bytes32 exchangeId; + + constructor(uint256 _chainId) BaseForkTest(_chainId) {} + + /* ======================================== */ + /* ================ Set Up ================ */ + /* ======================================== */ + + function setUp() public virtual override { + super.setUp(); + // Tokens + reserveToken = IStableTokenV2(CUSD_ADDRESS); + goodDollarToken = IGoodDollar(GOOD_DOLLAR_ADDRESS); + + // Contracts + goodDollarExchangeProvider = new GoodDollarExchangeProvider(false); + expansionController = new GoodDollarExpansionController(false); + // deployCode() hack to deploy solidity v0.5 reserve contract from a v0.8 contract + goodDollarReserve = IReserve(deployCode("Reserve", abi.encode(true))); + + // Addresses + ownerAddress = makeAddr("owner"); + + // Initialize GoodDollarExchangeProvider + configureReserve(); + configureBroker(); + configureGoodDollarExchangeProvider(); + configureTokens(); + configureExpansionController(); + configureTradingLimits(); + } + + function configureReserve() public { + bytes32[] memory initialAssetAllocationSymbols = new bytes32[](2); + initialAssetAllocationSymbols[0] = bytes32("cGLD"); + initialAssetAllocationSymbols[1] = bytes32("cUSD"); + + uint256[] memory initialAssetAllocationWeights = new uint256[](2); + initialAssetAllocationWeights[0] = FixidityLib.newFixedFraction(1, 2).unwrap(); + initialAssetAllocationWeights[1] = FixidityLib.newFixedFraction(1, 2).unwrap(); + + uint256 tobinTax = FixidityLib.newFixedFraction(5, 1000).unwrap(); + uint256 tobinTaxReserveRatio = FixidityLib.newFixedFraction(2, 1).unwrap(); + + address[] memory collateralAssets = new address[](1); + collateralAssets[0] = address(reserveToken); + + uint256[] memory collateralAssetDailySpendingRatios = new uint256[](1); + collateralAssetDailySpendingRatios[0] = 1e24; + + vm.startPrank(ownerAddress); + goodDollarReserve.initialize({ + registryAddress: REGISTRY_ADDRESS, + _tobinTaxStalenessThreshold: 600, // deprecated + _spendingRatioForCelo: 1000000000000000000000000, + _frozenGold: 0, + _frozenDays: 0, + _assetAllocationSymbols: initialAssetAllocationSymbols, + _assetAllocationWeights: initialAssetAllocationWeights, + _tobinTax: tobinTax, + _tobinTaxReserveRatio: tobinTaxReserveRatio, + _collateralAssets: collateralAssets, + _collateralAssetDailySpendingRatios: collateralAssetDailySpendingRatios + }); + + goodDollarReserve.addToken(address(goodDollarToken)); + goodDollarReserve.addExchangeSpender(address(broker)); + vm.stopPrank(); + require( + goodDollarReserve.isStableAsset(address(goodDollarToken)), + "GoodDollar is not a stable token in the reserve" + ); + require( + goodDollarReserve.isCollateralAsset(address(reserveToken)), + "ReserveToken is not a collateral asset in the reserve" + ); + } + + function configureTokens() public { + vm.startPrank(AVATAR_ADDRESS); + goodDollarToken.addMinter(address(broker)); + goodDollarToken.addMinter(address(expansionController)); + vm.stopPrank(); + + deal({ token: address(reserveToken), to: address(goodDollarReserve), give: INITIAL_RESERVE_BALANCE }); + + uint256 initialReserveGoodDollarBalanceInWei = (INITIAL_RESERVE_BALANCE / + goodDollarExchangeProvider.currentPrice(exchangeId)) * 1e18; + mintGoodDollar({ amount: initialReserveGoodDollarBalanceInWei, to: address(goodDollarReserve) }); + } + + function configureBroker() public { + vm.prank(Broker(address(broker)).owner()); + broker.addExchangeProvider(address(goodDollarExchangeProvider), address(goodDollarReserve)); + + require( + broker.isExchangeProvider(address(goodDollarExchangeProvider)), + "ExchangeProvider is not registered in the broker" + ); + require( + Broker(address(broker)).exchangeReserve(address(goodDollarExchangeProvider)) == address(goodDollarReserve), + "Reserve is not registered in the broker" + ); + } + + function configureGoodDollarExchangeProvider() public { + vm.prank(ownerAddress); + goodDollarExchangeProvider.initialize( + address(broker), + address(goodDollarReserve), + address(expansionController), + AVATAR_ADDRESS + ); + + poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(goodDollarToken), + tokenSupply: INITIAL_GOOD_DOLLAR_TOKEN_SUPPLY, + reserveBalance: INITIAL_RESERVE_BALANCE, + reserveRatio: INITIAL_RESERVE_RATIO, + exitContribution: INITIAL_EXIT_CONTRIBUTION + }); + + vm.prank(AVATAR_ADDRESS); + exchangeId = IBancorExchangeProvider(address(goodDollarExchangeProvider)).createExchange(poolExchange); + } + + function configureExpansionController() public { + vm.prank(ownerAddress); + expansionController.initialize({ + _goodDollarExchangeProvider: address(goodDollarExchangeProvider), + _distributionHelper: distributionHelperAddress, + _reserve: address(goodDollarReserve), + _avatar: AVATAR_ADDRESS + }); + + vm.prank(AVATAR_ADDRESS); + expansionController.setExpansionConfig({ + exchangeId: exchangeId, + expansionRate: INITIAL_EXPANSION_RATE, + expansionFrequency: INITIAL_EXPANSION_FREQUENCY + }); + } + + function configureTradingLimits() internal { + ITradingLimits.Config memory config = ITradingLimits.Config({ + // No more than 5,000 cUSD outflow within 5 minutes + timestep0: 300, + limit0: 5_000, + // No more than 50,000 cUSD outflow within 1 day + timestep1: 86_400, + limit1: 50_000, + // No more than 100,000 cUSD outflow in total + limitGlobal: 100_000, + flags: 1 | 2 | 4 // L0 = 1, L1 = 2, LG = 4 + }); + + vm.prank(Broker(address(broker)).owner()); + broker.configureTradingLimit(exchangeId, address(reserveToken), config); + } + + /** + * @notice Manual deal helper because foundry's vm.deal() crashes + * on the GoodDollar contract with "panic: assertion failed (0x01)" + */ + function mintGoodDollar(uint256 amount, address to) public { + vm.prank(AVATAR_ADDRESS); + goodDollarToken.mint(to, amount); + } + + function logHeader() internal view { + string memory ticker = string( + abi.encodePacked(IERC20(address(reserveToken)).symbol(), "/", IERC20(address(goodDollarToken)).symbol()) + ); + console.log("========================================"); + console.log(unicode"🔦 Testing pair:", ticker); + console.log("========================================"); + } + + function getTradingLimitId(address tokenAddress) public view returns (bytes32 limitId) { + bytes32 tokenInBytes32 = bytes32(uint256(uint160(tokenAddress))); + bytes32 _limitId = exchangeId ^ tokenInBytes32; + return _limitId; + } + + function getTradingLimitsConfig(address tokenAddress) public view returns (ITradingLimits.Config memory config) { + bytes32 limitId = getTradingLimitId(tokenAddress); + ITradingLimits.Config memory _config; + (_config.timestep0, _config.timestep1, _config.limit0, _config.limit1, _config.limitGlobal, _config.flags) = Broker( + address(broker) + ).tradingLimitsConfig(limitId); + + return _config; + } + + function getTradingLimitsState(address tokenAddress) public view returns (ITradingLimits.State memory state) { + bytes32 limitId = getTradingLimitId(tokenAddress); + ITradingLimits.State memory _state; + (_state.lastUpdated0, _state.lastUpdated1, _state.netflow0, _state.netflow1, _state.netflowGlobal) = Broker( + address(broker) + ).tradingLimitsState(limitId); + + return _state; + } + + function getRefreshedTradingLimitsState( + address tokenAddress + ) public view returns (ITradingLimits.State memory state) { + ITradingLimits.Config memory config = getTradingLimitsConfig(tokenAddress); + // Netflow might be outdated because of a skip(...) call. + // By doing an update(-1) and then update(1 ) we refresh the state without changing the state. + // The reason we can't just update(0) is that 0 would be cast to -1 in the update function. + state = tradingLimits.update(getTradingLimitsState(tokenAddress), config, -2, 1); + state = tradingLimits.update(state, config, 1, 0); + } + + function maxOutflow(address tokenAddress) internal view returns (int48) { + ITradingLimits.Config memory config = getTradingLimitsConfig(tokenAddress); + ITradingLimits.State memory state = getRefreshedTradingLimitsState(tokenAddress); + int48 maxOutflowL0 = config.limit0 + state.netflow0; + int48 maxOutflowL1 = config.limit1 + state.netflow1; + int48 maxOutflowLG = config.limitGlobal + state.netflowGlobal; + + if (config.flags == L0 | L1 | LG) { + return min(maxOutflowL0, maxOutflowL1, maxOutflowLG); + } else if (config.flags == L0 | LG) { + return min(maxOutflowL0, maxOutflowLG); + } else if (config.flags == L0 | L1) { + return min(maxOutflowL0, maxOutflowL1); + } else if (config.flags == L0) { + return maxOutflowL0; + } else { + revert("Unexpected limit config"); + } + } + + function test_init_isDeployedAndInitializedCorrectly() public view { + assertEq(goodDollarExchangeProvider.owner(), ownerAddress); + assertEq(goodDollarExchangeProvider.broker(), address(broker)); + assertEq(address(goodDollarExchangeProvider.reserve()), address(goodDollarReserve)); + + IBancorExchangeProvider.PoolExchange memory _poolExchange = goodDollarExchangeProvider.getPoolExchange(exchangeId); + assertEq(_poolExchange.reserveAsset, _poolExchange.reserveAsset); + assertEq(_poolExchange.tokenAddress, _poolExchange.tokenAddress); + assertEq(_poolExchange.tokenSupply, _poolExchange.tokenSupply); + assertEq(_poolExchange.reserveBalance, _poolExchange.reserveBalance); + assertEq(_poolExchange.reserveRatio, _poolExchange.reserveRatio); + assertEq(_poolExchange.exitContribution, _poolExchange.exitContribution); + } +} diff --git a/test/fork/GoodDollar/SwapForkTest.sol b/test/fork/GoodDollar/SwapForkTest.sol new file mode 100644 index 00000000..b8f36b68 --- /dev/null +++ b/test/fork/GoodDollar/SwapForkTest.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +// Libraries +import { TokenHelpers } from "../helpers/TokenHelpers.sol"; +import { TradingLimitHelpers } from "../helpers/TradingLimitHelpers.sol"; + +// Interfaces +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; + +// Contracts +import { GoodDollarBaseForkTest } from "./GoodDollarBaseForkTest.sol"; + +contract GoodDollarSwapForkTest is GoodDollarBaseForkTest { + using TradingLimitHelpers for *; + using TokenHelpers for *; + + constructor(uint256 _chainId) GoodDollarBaseForkTest(_chainId) {} + + function setUp() public override { + super.setUp(); + } + + function test_swapIn_reserveTokenToGoodDollar() public { + uint256 amountIn = 1000 * 1e18; + + uint256 reserveBalanceBefore = reserveToken.balanceOf(address(goodDollarReserve)); + uint256 priceBefore = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + + // Calculate the expected amount of G$ to receive for `amountIn` cUSD + uint256 expectedAmountOut = broker.getAmountOut( + address(goodDollarExchangeProvider), + exchangeId, + address(reserveToken), + address(goodDollarToken), + amountIn + ); + + // Give trader required amount of cUSD to swap + deal({ token: address(reserveToken), to: trader, give: amountIn }); + + vm.startPrank(trader); + // Trader approves the broker to spend their cUSD + reserveToken.approve({ spender: address(broker), amount: amountIn }); + + // Broker swaps `amountIn` of trader's cUSD for G$ + broker.swapIn({ + exchangeProvider: address(goodDollarExchangeProvider), + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(goodDollarToken), + amountIn: amountIn, + amountOutMin: expectedAmountOut + }); + vm.stopPrank(); + + uint256 priceAfter = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 reserveBalanceAfter = reserveToken.balanceOf(address(goodDollarReserve)); + + assertEq(expectedAmountOut, goodDollarToken.balanceOf(trader)); + assertEq(reserveBalanceBefore + amountIn, reserveBalanceAfter); + assertTrue(priceBefore < priceAfter); + } + + function test_swapIn_goodDollarToReserveToken() public { + uint256 amountIn = 1000 * 1e18; + + uint256 reserveBalanceBefore = reserveToken.balanceOf(address(goodDollarReserve)); + uint256 priceBefore = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 expectedAmountOut = broker.getAmountOut( + address(goodDollarExchangeProvider), + exchangeId, + address(goodDollarToken), + address(reserveToken), + amountIn + ); + + mintGoodDollar(amountIn, trader); + + vm.startPrank(trader); + goodDollarToken.approve(address(broker), amountIn); + broker.swapIn( + address(goodDollarExchangeProvider), + exchangeId, + address(goodDollarToken), + address(reserveToken), + amountIn, + expectedAmountOut + ); + uint256 priceAfter = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 reserveBalanceAfter = reserveToken.balanceOf(address(goodDollarReserve)); + + assertEq(expectedAmountOut, reserveToken.balanceOf(trader)); + assertEq(reserveBalanceBefore - expectedAmountOut, reserveBalanceAfter); + assertTrue(priceAfter < priceBefore); + } + + function test_swapOut_reserveTokenToGoodDollar() public { + uint256 amountOut = 1000 * 1e18; + uint256 reserveBalanceBefore = reserveToken.balanceOf(address(goodDollarReserve)); + uint256 priceBefore = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 expectedAmountIn = broker.getAmountIn( + address(goodDollarExchangeProvider), + exchangeId, + address(reserveToken), + address(goodDollarToken), + amountOut + ); + + deal(address(reserveToken), trader, expectedAmountIn); + + vm.startPrank(trader); + reserveToken.approve(address(broker), expectedAmountIn); + broker.swapOut( + address(goodDollarExchangeProvider), + exchangeId, + address(reserveToken), + address(goodDollarToken), + amountOut, + expectedAmountIn + ); + uint256 priceAfter = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 reserveBalanceAfter = reserveToken.balanceOf(address(goodDollarReserve)); + + assertEq(amountOut, goodDollarToken.balanceOf(trader)); + assertEq(reserveBalanceBefore + expectedAmountIn, reserveBalanceAfter); + assertTrue(priceBefore < priceAfter); + } + + function test_swapOut_goodDollarToReserveToken() public { + uint256 amountOut = 1000 * 1e18; + + uint256 reserveBalanceBefore = reserveToken.balanceOf(address(goodDollarReserve)); + uint256 priceBefore = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 expectedAmountIn = broker.getAmountIn( + address(goodDollarExchangeProvider), + exchangeId, + address(goodDollarToken), + address(reserveToken), + amountOut + ); + + mintGoodDollar(expectedAmountIn, trader); + + vm.startPrank(trader); + goodDollarToken.approve(address(broker), expectedAmountIn); + broker.swapOut( + address(goodDollarExchangeProvider), + exchangeId, + address(goodDollarToken), + address(reserveToken), + amountOut, + expectedAmountIn + ); + uint256 priceAfter = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 reserveBalanceAfter = reserveToken.balanceOf(address(goodDollarReserve)); + + assertEq(amountOut, reserveToken.balanceOf(trader)); + assertEq(reserveBalanceBefore - amountOut, reserveBalanceAfter); + assertTrue(priceAfter < priceBefore); + } +} diff --git a/test/fork/GoodDollar/TradingLimitsForkTest.sol b/test/fork/GoodDollar/TradingLimitsForkTest.sol new file mode 100644 index 00000000..ac8865b6 --- /dev/null +++ b/test/fork/GoodDollar/TradingLimitsForkTest.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +// Libraries +import { console } from "forge-std/console.sol"; +import { L0, L1, LG } from "../helpers/misc.sol"; +import { TokenHelpers } from "../helpers/TokenHelpers.sol"; +import { TradingLimitHelpers } from "../helpers/TradingLimitHelpers.sol"; + +// Interfaces +import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; + +// Contracts +import { GoodDollarBaseForkTest } from "./GoodDollarBaseForkTest.sol"; + +contract GoodDollarTradingLimitsForkTest is GoodDollarBaseForkTest { + using TradingLimitHelpers for *; + using TokenHelpers for *; + + constructor(uint256 _chainId) GoodDollarBaseForkTest(_chainId) {} + + function setUp() public override { + super.setUp(); + } + + function test_tradingLimitsAreConfiguredForReserveToken() public view { + ITradingLimits.Config memory config = getTradingLimitsConfig(address(reserveToken)); + bool reserveAssetLimitConfigured = config.flags > uint8(0); + require(reserveAssetLimitConfigured, "Limit not configured"); + } + + function test_tradingLimitsAreEnforced_reserveTokenOutflowLimit0() public { + _swapUntilReserveTokenLimit0_onOutflow(); + _swapGoodDollarForReserveToken(bytes(L0.revertReason())); + } + + function test_tradingLimitsAreEnforced_reserveTokenOutflowLimit1() public { + _swapUntilReserveTokenLimit1_onOutflow(); + _swapGoodDollarForReserveToken(bytes(L1.revertReason())); + } + + function test_tradingLimitsAreEnforced_reserveTokenOutflowLimitGlobal() public { + _swapUntilReserveTokenGlobalLimit_onOutflow(); + _swapGoodDollarForReserveToken(bytes(LG.revertReason())); + } + + function test_tradingLimitsAreEnforced_reserveTokenInflowLimit0() public { + _swapUntilReserveTokenLimit0_onInflow(); + _swapReserveTokenForGoodDollar(bytes(L0.revertReason())); + } + + function test_tradingLimitsAreEnforced_reserveTokenInflowLimit1() public { + _swapUntilReserveTokenLimit1_onInflow(); + _swapReserveTokenForGoodDollar(bytes(L1.revertReason())); + } + + function test_tradingLimitsAreEnforced_reserveTokenInflowGlobalLimit() public { + _swapUntilReserveTokenGlobalLimit_onInflow(); + _swapReserveTokenForGoodDollar(bytes(LG.revertReason())); + } + + /** + * @notice Swaps G$ for cUSD with the maximum amount allowed per swap + * @param revertReason An optional revert reason to expect, if swap should revert. + * @dev Pass an empty string when not expecting a revert. + */ + function _swapGoodDollarForReserveToken(bytes memory revertReason) internal { + ITradingLimits.Config memory config = getTradingLimitsConfig(address(reserveToken)); + + // Get the max amount we can swap in a single transaction before we hit L0 + uint256 maxPerSwapInWei = uint256(uint48(config.limit0)) * 1e18; + uint256 inflowRequiredForAmountOut = goodDollarExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(goodDollarToken), + tokenOut: address(reserveToken), + amountOut: maxPerSwapInWei + }); + + mintGoodDollar(inflowRequiredForAmountOut, trader); + + vm.startPrank(trader); + goodDollarToken.approve(address(broker), inflowRequiredForAmountOut); + + // If a revertReason was provided, expect a revert with that reason + if (revertReason.length > 0) { + vm.expectRevert(revertReason); + } + broker.swapOut({ + exchangeProvider: address(goodDollarExchangeProvider), + exchangeId: exchangeId, + tokenIn: address(goodDollarToken), + tokenOut: address(reserveToken), + amountOut: maxPerSwapInWei, + amountInMax: type(uint256).max + }); + vm.stopPrank(); + } + + function _swapUntilReserveTokenLimit0_onOutflow() internal { + _swapGoodDollarForReserveToken({ revertReason: "" }); + } + + function _swapUntilReserveTokenLimit1_onOutflow() internal { + // Get the trading limits config and state for the reserve token + ITradingLimits.Config memory config = getTradingLimitsConfig(address(reserveToken)); + ITradingLimits.State memory state = getRefreshedTradingLimitsState(address(reserveToken)); + console.log(unicode"🏷️ [%d] Swap until L1=%d on outflow", block.timestamp, uint48(config.limit1)); + + // Get the max amount we can swap in a single transaction before we hit L0 + int48 maxPerSwap = config.limit0; + + // Swap until right before we would hit the L1 limit. + // We swap in `maxPerSwap` increments and timewarp + // by `timestep0 + 1` seconds so we avoid hitting L0. + uint256 i; + while (state.netflow1 - maxPerSwap >= -1 * config.limit1) { + skip(config.timestep0 + 1); + // Check that there's still outflow to trade as sometimes we hit LG while + // still having a bit of L1 left, which causes an infinite loop. + if (maxOutflow(address(reserveToken)) == 0) { + break; + } + + _swapUntilReserveTokenLimit0_onOutflow(); + + config = getTradingLimitsConfig(address(reserveToken)); + state = getTradingLimitsState(address(reserveToken)); + + i++; + require(i <= 10, "possible infinite loopL more than 10 iterations"); + } + skip(config.timestep0 + 1); + } + + function _swapUntilReserveTokenGlobalLimit_onOutflow() internal { + // Get the trading limits config and state for the reserve token + ITradingLimits.Config memory config = getTradingLimitsConfig(address(reserveToken)); + ITradingLimits.State memory state = getRefreshedTradingLimitsState(address(reserveToken)); + console.log(unicode"🏷️ [%d] Swap until LG=%d on outflow", block.timestamp, uint48(config.limitGlobal)); + + int48 maxPerSwap = config.limit0; + uint256 i; + while (state.netflowGlobal - maxPerSwap >= config.limitGlobal * -1) { + skip(config.timestep1 + 1); + _swapUntilReserveTokenLimit1_onOutflow(); + state = getRefreshedTradingLimitsState(address(reserveToken)); + i++; + require(i <= 50, "possible infinite loop: more than 50 iterations"); + } + skip(config.timestep1 + 1); + } + + /** + * @notice Swaps cUSD for G$ with the maximum amount allowed per swap + * @param revertReason An optional revert reason to expect, if swap should revert. + * @dev Pass an empty string when not expecting a revert. + */ + function _swapReserveTokenForGoodDollar(bytes memory revertReason) internal { + ITradingLimits.Config memory config = getTradingLimitsConfig(address(reserveToken)); + + // Get the max amount we can swap in a single transaction before we hit L0 + uint256 maxPerSwapInWei = uint256(uint48(config.limit0)) * 1e18; + deal({ token: address(reserveToken), to: trader, give: maxPerSwapInWei }); + + vm.startPrank(trader); + reserveToken.approve(address(broker), maxPerSwapInWei); + + // If a revertReason was provided, expect a revert with that reason + if (revertReason.length > 0) { + vm.expectRevert(revertReason); + } + broker.swapIn({ + exchangeProvider: address(goodDollarExchangeProvider), + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(goodDollarToken), + amountIn: maxPerSwapInWei, + amountOutMin: 0 + }); + vm.stopPrank(); + } + + function _swapUntilReserveTokenLimit0_onInflow() internal { + _swapReserveTokenForGoodDollar({ revertReason: "" }); + } + + function _swapUntilReserveTokenLimit1_onInflow() internal { + // Get the trading limits config and state for the reserve token + ITradingLimits.Config memory config = getTradingLimitsConfig(address(reserveToken)); + ITradingLimits.State memory state = getRefreshedTradingLimitsState(address(reserveToken)); + console.log(unicode"🏷️ [%d] Swap until L1=%d on inflow", block.timestamp, uint48(config.limit1)); + + // Get the max amount we can swap in a single transaction before we hit L0 + int48 maxPerSwap = config.limit0; + + // Swap until right before we would hit the L1 limit. + // We swap in `maxPerSwap` increments and timewarp + // by `timestep0 + 1` seconds so we avoid hitting L0. + while (state.netflow1 + maxPerSwap <= config.limit1) { + skip(config.timestep0 + 1); + _swapUntilReserveTokenLimit0_onInflow(); + config = getTradingLimitsConfig(address(reserveToken)); + state = getTradingLimitsState(address(reserveToken)); + + if (state.netflowGlobal == config.limitGlobal) { + console.log(unicode"🚨 LG reached during L1 inflow"); + break; + } + } + skip(config.timestep0 + 1); + } + + function _swapUntilReserveTokenGlobalLimit_onInflow() internal { + // Get the trading limits config and state for the reserve token + ITradingLimits.Config memory config = getTradingLimitsConfig(address(reserveToken)); + ITradingLimits.State memory state = getRefreshedTradingLimitsState(address(reserveToken)); + console.log(unicode"🏷️ [%d] Swap until LG=%d on inflow", block.timestamp, uint48(config.limitGlobal)); + + int48 maxPerSwap = config.limit0; + uint256 i; + while (state.netflowGlobal + maxPerSwap <= config.limitGlobal) { + skip(config.timestep1 + 1); + _swapUntilReserveTokenLimit1_onInflow(); + state = getRefreshedTradingLimitsState(address(reserveToken)); + i++; + require(i <= 50, "possible infinite loop: more than 50 iterations"); + } + skip(config.timestep1 + 1); + } +} diff --git a/test/fork/actions/SwapActions.sol b/test/fork/actions/SwapActions.sol index ce4160cb..9225249e 100644 --- a/test/fork/actions/SwapActions.sol +++ b/test/fork/actions/SwapActions.sol @@ -252,7 +252,7 @@ contract SwapActions is StdCheats { /* * from -> LG[to] * This function will do valid swaps until just before LG is hit - * during outflow on `to`, therfore we check the negative end + * during outflow on `to`, therefore we check the negative end * of the limit because `to` flows out of the reserve. */ ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); @@ -265,7 +265,7 @@ contract SwapActions is StdCheats { skip(limitConfig.timestep1 + 1); swapUntilL1_onOutflow(from, to); limitConfig = ctx.tradingLimitsConfig(to); - // Triger an update to reset netflows + // Trigger an update to reset netflows limitState = ctx.tradingLimitsState(to); } skip(limitConfig.timestep1 + 1); @@ -275,7 +275,7 @@ contract SwapActions is StdCheats { skip(limitConfig.timestep0 + 1); swapUntilL0_onOutflow(from, to); limitConfig = ctx.tradingLimitsConfig(to); - // Triger an update to reset netflows + // Trigger an update to reset netflows limitState = ctx.tradingLimitsState(to); } skip(limitConfig.timestep0 + 1); diff --git a/test/fork/assertions/SwapAssertions.sol b/test/fork/assertions/SwapAssertions.sol index 2108fbba..4e29a1e6 100644 --- a/test/fork/assertions/SwapAssertions.sol +++ b/test/fork/assertions/SwapAssertions.sol @@ -38,10 +38,10 @@ contract SwapAssertions is StdAssertions, Actions { ctx.logLimits(from); ctx.logLimits(to); if (ctx.atInflowLimit(from, LG)) { - console.log(unicode"🚨 Cannot test swap beacause the global inflow limit is reached on %s", from); + console.log(unicode"🚨 Cannot test swap because the global inflow limit is reached on %s", from); return; } else if (ctx.atOutflowLimit(to, LG)) { - console.log(unicode"🚨 Cannot test swap beacause the global outflow limit is reached on %s", to); + console.log(unicode"🚨 Cannot test swap because the global outflow limit is reached on %s", to); return; } @@ -74,10 +74,10 @@ contract SwapAssertions is StdAssertions, Actions { ctx.logLimits(from); ctx.logLimits(to); if (ctx.atInflowLimit(from, LG)) { - console.log(unicode"🚨 Cannot test swap beacause the global inflow limit is reached on %s", from); + console.log(unicode"🚨 Cannot test swap because the global inflow limit is reached on %s", from); return; } else if (ctx.atOutflowLimit(to, LG)) { - console.log(unicode"🚨 Cannot test swap beacause the global outflow limit is reached on %s", to); + console.log(unicode"🚨 Cannot test swap because the global outflow limit is reached on %s", to); return; } diff --git a/test/fork/helpers/TradingLimitHelpers.sol b/test/fork/helpers/TradingLimitHelpers.sol index 5f47adb4..cc108b55 100644 --- a/test/fork/helpers/TradingLimitHelpers.sol +++ b/test/fork/helpers/TradingLimitHelpers.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8; import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; +import { Broker } from "contracts/swap/Broker.sol"; import { ExchangeForkTest } from "../ExchangeForkTest.sol"; import { OracleHelpers } from "./OracleHelpers.sol"; @@ -14,7 +15,15 @@ library TradingLimitHelpers { using OracleHelpers for *; function isLimitConfigured(ExchangeForkTest ctx, bytes32 limitId) public view returns (bool) { - ITradingLimits.Config memory limitConfig = ctx.broker().tradingLimitsConfig(limitId); + ITradingLimits.Config memory limitConfig; + ( + limitConfig.timestep0, + limitConfig.timestep1, + limitConfig.limit0, + limitConfig.limit1, + limitConfig.limitGlobal, + limitConfig.flags + ) = Broker(address(ctx.broker())).tradingLimitsConfig(limitId); return limitConfig.flags > uint8(0); } @@ -22,21 +31,59 @@ library TradingLimitHelpers { ExchangeForkTest ctx, bytes32 limitId ) public view returns (ITradingLimits.Config memory) { - return ctx.broker().tradingLimitsConfig(limitId); + ITradingLimits.Config memory limitConfig; + ( + limitConfig.timestep0, + limitConfig.timestep1, + limitConfig.limit0, + limitConfig.limit1, + limitConfig.limitGlobal, + limitConfig.flags + ) = Broker(address(ctx.broker())).tradingLimitsConfig(limitId); + + return limitConfig; } function tradingLimitsState(ExchangeForkTest ctx, bytes32 limitId) public view returns (ITradingLimits.State memory) { - return ctx.broker().tradingLimitsState(limitId); + ITradingLimits.State memory limitState; + ( + limitState.lastUpdated0, + limitState.lastUpdated1, + limitState.netflow0, + limitState.netflow1, + limitState.netflowGlobal + ) = Broker(address(ctx.broker())).tradingLimitsState(limitId); + return limitState; } function tradingLimitsConfig(ExchangeForkTest ctx, address asset) public view returns (ITradingLimits.Config memory) { + ITradingLimits.Config memory limitConfig; bytes32 assetBytes32 = bytes32(uint256(uint160(asset))); - return ctx.broker().tradingLimitsConfig(ctx.exchangeId() ^ assetBytes32); + bytes32 limitId = ctx.exchangeId() ^ assetBytes32; + + ( + limitConfig.timestep0, + limitConfig.timestep1, + limitConfig.limit0, + limitConfig.limit1, + limitConfig.limitGlobal, + limitConfig.flags + ) = Broker(address(ctx.broker())).tradingLimitsConfig(limitId); + return limitConfig; } function tradingLimitsState(ExchangeForkTest ctx, address asset) public view returns (ITradingLimits.State memory) { + ITradingLimits.State memory limitState; bytes32 assetBytes32 = bytes32(uint256(uint160(asset))); - return ctx.broker().tradingLimitsState(ctx.exchangeId() ^ assetBytes32); + bytes32 limitId = ctx.exchangeId() ^ assetBytes32; + ( + limitState.lastUpdated0, + limitState.lastUpdated1, + limitState.netflow0, + limitState.netflow1, + limitState.netflowGlobal + ) = Broker(address(ctx.broker())).tradingLimitsState(limitId); + return limitState; } function refreshedTradingLimitsState( @@ -44,13 +91,11 @@ library TradingLimitHelpers { address asset ) public view returns (ITradingLimits.State memory state) { ITradingLimits.Config memory config = tradingLimitsConfig(ctx, asset); - // Netflow might be outdated because of a skip(...) call and doing - // an update(0) would reset the netflow if enough time has passed. - state = ctx.tradingLimits().update(tradingLimitsState(ctx, asset), config, 0, 0); - // XXX: There's a bug in our current TradingLimits library implementation where - // an update with 0 netflow will round to 1. So we do another update with -1 netflow - // to get it back to the actual value. - state = ctx.tradingLimits().update(state, config, -1, 0); + // Netflow might be outdated because of a skip(...) call. + // By doing an update(-1) and then update(1 ) we refresh the state without changing the state. + // The reason we can't just update(0) is that 0 would be cast to -1 in the update function. + state = ctx.tradingLimits().update(tradingLimitsState(ctx, asset), config, -1, 1); + state = ctx.tradingLimits().update(state, config, 1, 0); } function isLimitEnabled(ITradingLimits.Config memory config, uint8 limit) internal pure returns (bool) { diff --git a/test/integration/protocol/ProtocolTest.sol b/test/integration/protocol/ProtocolTest.sol index 04058559..668eda84 100644 --- a/test/integration/protocol/ProtocolTest.sol +++ b/test/integration/protocol/ProtocolTest.sol @@ -360,7 +360,10 @@ contract ProtocolTest is Test, WithRegistry { address[] memory exchangeProviders = new address[](1); exchangeProviders[0] = address(biPoolManager); - broker.initialize(exchangeProviders, address(reserve)); + address[] memory reserves = new address[](1); + reserves[0] = address(reserve); + + broker.initialize(exchangeProviders, reserves); registry.setAddressFor("Broker", address(broker)); reserve.addExchangeSpender(address(broker)); biPoolManager.setPricingModules(pricingModuleIdentifiers, pricingModules); diff --git a/test/unit/goodDollar/BancorExchangeProvider.t.sol b/test/unit/goodDollar/BancorExchangeProvider.t.sol new file mode 100644 index 00000000..e3d19b53 --- /dev/null +++ b/test/unit/goodDollar/BancorExchangeProvider.t.sol @@ -0,0 +1,1693 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +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 { ERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/ERC20.sol"; +import { BancorExchangeProvider } from "contracts/goodDollar/BancorExchangeProvider.sol"; +import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; +import { IReserve } from "contracts/interfaces/IReserve.sol"; + +contract BancorExchangeProviderTest is Test { + /* ------- Events from IBancorExchangeProvider ------- */ + + event BrokerUpdated(address indexed newBroker); + + event ReserveUpdated(address indexed newReserve); + + event PowerUpdated(address indexed newPower); + + 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; + ERC20 public token; + ERC20 public token2; + + address public reserveAddress; + address public brokerAddress; + IBancorExchangeProvider.PoolExchange public poolExchange1; + IBancorExchangeProvider.PoolExchange public poolExchange2; + + function setUp() public virtual { + reserveToken = new ERC20("cUSD", "cUSD"); + token = new ERC20("Good$", "G$"); + token2 = new ERC20("Good2$", "G2$"); + + brokerAddress = makeAddr("Broker"); + reserveAddress = makeAddr("Reserve"); + + poolExchange1 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: 300_000 * 1e18, + reserveBalance: 60_000 * 1e18, + reserveRatio: 1e8 * 0.2, + exitContribution: 1e8 * 0.01 + }); + + 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)), + abi.encode(true) + ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token2)), + abi.encode(true) + ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken)), + abi.encode(true) + ); + } + + function initializeBancorExchangeProvider() internal returns (BancorExchangeProvider) { + BancorExchangeProvider bancorExchangeProvider = new BancorExchangeProvider(false); + + bancorExchangeProvider.initialize(brokerAddress, reserveAddress); + return bancorExchangeProvider; + } +} + +contract BancorExchangeProviderTest_initilizerSettersGetters is BancorExchangeProviderTest { + BancorExchangeProvider bancorExchangeProvider; + + function setUp() public override { + super.setUp(); + bancorExchangeProvider = initializeBancorExchangeProvider(); + } + + /* ---------- Initializer ---------- */ + function test_initialize_shouldSetOwner() public view { + assertEq(bancorExchangeProvider.owner(), address(this)); + } + + function test_initialize_shouldSetBroker() public view { + assertEq(bancorExchangeProvider.broker(), brokerAddress); + } + + function test_initialize_shouldSetReserve() public view { + assertEq(address(bancorExchangeProvider.reserve()), reserveAddress); + } + + /* ---------- Setters ---------- */ + function test_setBroker_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + bancorExchangeProvider.setBroker(makeAddr("NewBroker")); + } + + function test_setBroker_whenAddressIsZero_shouldRevert() public { + vm.expectRevert("Broker address must be set"); + bancorExchangeProvider.setBroker(address(0)); + } + + function test_setBroker_whenSenderIsOwner_shouldUpdateAndEmit() public { + address newBroker = makeAddr("NewBroker"); + + vm.expectEmit(true, true, true, true); + emit BrokerUpdated(newBroker); + bancorExchangeProvider.setBroker(newBroker); + + assertEq(bancorExchangeProvider.broker(), newBroker); + } + + function test_setReserve_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + bancorExchangeProvider.setReserve(makeAddr("NewReserve")); + } + + function test_setReserve_whenAddressIsZero_shouldRevert() public { + vm.expectRevert("Reserve address must be set"); + bancorExchangeProvider.setReserve(address(0)); + } + + function test_setReserve_whenSenderIsOwner_shouldUpdateAndEmit() public { + address newReserve = makeAddr("NewReserve"); + + vm.expectEmit(true, true, true, true); + emit ReserveUpdated(newReserve); + bancorExchangeProvider.setReserve(newReserve); + + assertEq(address(bancorExchangeProvider.reserve()), newReserve); + } + + function test_setExitContribution_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + bytes32 exchangeId = "0xexchangeId"; + bancorExchangeProvider.setExitContribution(exchangeId, 1e5); + } + + function test_setExitContribution_whenExchangeDoesNotExist_shouldRevert() public { + bytes32 exchangeId = "0xexchangeId"; + vm.expectRevert("Exchange does not exist"); + bancorExchangeProvider.setExitContribution(exchangeId, 1e5); + } + + function test_setExitContribution_whenExitContributionAbove100Percent_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + uint32 maxWeight = bancorExchangeProvider.MAX_WEIGHT(); + vm.expectRevert("Exit contribution is too high"); + bancorExchangeProvider.setExitContribution(exchangeId, maxWeight + 1); + } + + function test_setExitContribution_whenSenderIsOwner_shouldUpdateAndEmit() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + uint32 newExitContribution = 1e3; + vm.expectEmit(true, true, true, true); + emit ExitContributionSet(exchangeId, newExitContribution); + bancorExchangeProvider.setExitContribution(exchangeId, newExitContribution); + + IBancorExchangeProvider.PoolExchange memory poolExchange = bancorExchangeProvider.getPoolExchange(exchangeId); + assertEq(poolExchange.exitContribution, newExitContribution); + } + + /* ---------- Getters ---------- */ + + function test_getPoolExchange_whenExchangeDoesNotExist_shouldRevert() public { + bytes32 exchangeId = "0xexchangeId"; + vm.expectRevert("Exchange does not exist"); + bancorExchangeProvider.getPoolExchange(exchangeId); + } + + function test_getPoolExchange_whenPoolExists_shouldReturnPool() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + IBancorExchangeProvider.PoolExchange memory poolExchange = bancorExchangeProvider.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); + } + + function test_getExchangeIds_whenNoExchanges_shouldReturnEmptyArray() public view { + bytes32[] memory exchangeIds = bancorExchangeProvider.getExchangeIds(); + assertEq(exchangeIds.length, 0); + } + + function test_getExchangeIds_whenExchangesExist_shouldReturnExchangeIds() public { + bytes32 exchangeId1 = bancorExchangeProvider.createExchange(poolExchange1); + + bytes32[] memory exchangeIds = bancorExchangeProvider.getExchangeIds(); + assertEq(exchangeIds.length, 1); + assertEq(exchangeIds[0], exchangeId1); + } + + function test_getExchanges_whenNoExchanges_shouldReturnEmptyArray() public view { + IExchangeProvider.Exchange[] memory exchanges = bancorExchangeProvider.getExchanges(); + assertEq(exchanges.length, 0); + } + + function test_getExchanges_whenExchangesExist_shouldReturnExchange() public { + bytes32 exchangeId1 = bancorExchangeProvider.createExchange(poolExchange1); + + IExchangeProvider.Exchange[] memory exchanges = bancorExchangeProvider.getExchanges(); + assertEq(exchanges.length, 1); + assertEq(exchanges[0].exchangeId, exchangeId1); + assertEq(exchanges[0].assets[0], poolExchange1.reserveAsset); + assertEq(exchanges[0].assets[1], poolExchange1.tokenAddress); + } +} + +contract BancorExchangeProviderTest_createExchange is BancorExchangeProviderTest { + BancorExchangeProvider bancorExchangeProvider; + + function setUp() public override { + super.setUp(); + bancorExchangeProvider = initializeBancorExchangeProvider(); + } + + function test_createExchange_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenReserveAssetIsZero_shouldRevert() public { + poolExchange1.reserveAsset = address(0); + vm.expectRevert("Invalid reserve asset"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenReserveAssetIsNotCollateral_shouldRevert() public { + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken)), + abi.encode(false) + ); + vm.expectRevert("Reserve asset must be a collateral registered with the reserve"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenTokenAddressIsZero_shouldRevert() public { + poolExchange1.tokenAddress = address(0); + vm.expectRevert("Invalid token address"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenTokenAddressIsNotStable_shouldRevert() public { + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token)), + abi.encode(false) + ); + 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"); + bancorExchangeProvider.createExchange(poolExchange1); + poolExchange1.reserveRatio = 1; + vm.expectRevert("Reserve ratio is too low"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenReserveRatioAbove100Percent_shouldRevert() public { + poolExchange1.reserveRatio = bancorExchangeProvider.MAX_WEIGHT() + 1; + vm.expectRevert("Reserve ratio is too high"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenExitContributionAbove100Percent_shouldRevert() public { + poolExchange1.exitContribution = bancorExchangeProvider.MAX_WEIGHT() + 1; + vm.expectRevert("Exit contribution is too high"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenExchangeAlreadyExists_shouldRevert() public { + bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("Exchange already exists"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + 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"); + 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"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenExchangeDoesNotExist_shouldCreateExchangeAndEmit() public { + vm.expectEmit(true, true, true, true); + bytes32 expectedExchangeId = keccak256(abi.encodePacked(reserveToken.symbol(), token.symbol())); + emit ExchangeCreated(expectedExchangeId, address(reserveToken), address(token)); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + assertEq(exchangeId, expectedExchangeId); + + IBancorExchangeProvider.PoolExchange memory poolExchange = bancorExchangeProvider.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 = bancorExchangeProvider.getExchanges(); + assertEq(exchanges.length, 1); + assertEq(exchanges[0].exchangeId, exchangeId); + + assertEq(bancorExchangeProvider.tokenPrecisionMultipliers(address(reserveToken)), 1); + assertEq(bancorExchangeProvider.tokenPrecisionMultipliers(address(token)), 1); + } +} + +contract BancorExchangeProviderTest_destroyExchange is BancorExchangeProviderTest { + BancorExchangeProvider bancorExchangeProvider; + + function setUp() public override { + super.setUp(); + bancorExchangeProvider = initializeBancorExchangeProvider(); + } + + function test_destroyExchange_whenSenderIsNotOwner_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + bancorExchangeProvider.destroyExchange(exchangeId, 0); + } + + function test_destroyExchange_whenIndexOutOfRange_shouldRevert() public { + bytes32 exchangeId = "0xexchangeId"; + vm.expectRevert("exchangeIdIndex not in range"); + bancorExchangeProvider.destroyExchange(exchangeId, 10); + } + + function test_destroyExchange_whenExchangeIdAndIndexDontMatch_shouldRevert() public { + bytes32 exchangeId = "0xexchangeId"; + bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("exchangeId at index doesn't match"); + bancorExchangeProvider.destroyExchange(exchangeId, 0); + } + + function test_destroyExchange_whenExchangeExists_shouldDestroyExchangeAndEmit() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + bytes32 exchangeId2 = bancorExchangeProvider.createExchange(poolExchange2); + + vm.expectEmit(true, true, true, true); + emit ExchangeDestroyed(exchangeId, poolExchange1.reserveAsset, poolExchange1.tokenAddress); + bancorExchangeProvider.destroyExchange(exchangeId, 0); + + bytes32[] memory exchangeIds = bancorExchangeProvider.getExchangeIds(); + assertEq(exchangeIds.length, 1); + + IExchangeProvider.Exchange[] memory exchanges = bancorExchangeProvider.getExchanges(); + assertEq(exchanges.length, 1); + assertEq(exchanges[0].exchangeId, exchangeId2); + } +} + +contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { + BancorExchangeProvider bancorExchangeProvider; + + function setUp() public override { + super.setUp(); + bancorExchangeProvider = initializeBancorExchangeProvider(); + } + + function test_getAmountIn_whenExchangeDoesNotExist_shouldRevert() public { + bytes32 exchangeId = "0xexchangeId"; + vm.expectRevert("Exchange does not exist"); + 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.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.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(token2), + amountOut: 1e18 + }); + } + + function test_getAmountIn_whenTokenInEqualsTokenOut_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + 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) / supply + // calculation: (12 * 60_000) / 300_000 = 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 { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + // 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: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: 1e18 + }); + assertEq(amountIn, expectedAmountIn); + } + + function test_getAmountIn_whenTokenInIsToken_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + // formula: = tokenSupply * (-1 + (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ) + // formula: amountIn = ------------------------------------------------------------------------------------------------ this is a fractional line + // formula: = (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio + + // 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 = 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 + 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 0.01% 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); + } +} + +contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { + BancorExchangeProvider bancorExchangeProvider; + + function setUp() public override { + super.setUp(); + bancorExchangeProvider = initializeBancorExchangeProvider(); + } + + function test_getAmountOut_whenExchangeDoesNotExist_shouldRevert() public { + bytes32 exchangeId = "0xexchangeId"; + vm.expectRevert("Exchange does not exist"); + 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: 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: exchangeId, + tokenIn: address(token), + tokenOut: address(token2), + amountIn: 1e18 + }); + } + + function test_getAmountOut_whenTokenInEqualTokenOut_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + 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 = 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: = 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 = 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; + 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: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: 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); + } +} + +contract BancorExchangeProviderTest_currentPrice is BancorExchangeProviderTest { + BancorExchangeProvider bancorExchangeProvider; + + function setUp() public override { + super.setUp(); + bancorExchangeProvider = initializeBancorExchangeProvider(); + } + + function test_currentPrice_whenExchangeDoesNotExist_shouldRevert() public { + bytes32 exchangeId = "0xexchangeId"; + vm.expectRevert("Exchange does not exist"); + bancorExchangeProvider.currentPrice(exchangeId); + } + + function test_currentPrice_whenExchangeExists_shouldReturnCorrectPrice() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + // formula: price = reserveBalance / tokenSupply * reserveRatio + // calculation: 60_000 / 300_000 * 0.2 = 1 + uint256 expectedPrice = 1e18; + 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 { + function test_swapIn_whenCallerIsNotBroker_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(makeAddr("NotBroker")); + vm.expectRevert("Caller is not the Broker"); + bancorExchangeProvider.swapIn(exchangeId, address(reserveToken), address(token), 1e18); + } + + function test_swapIn_whenExchangeDoesNotExist_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + vm.prank(brokerAddress); + vm.expectRevert("Exchange does not exist"); + bancorExchangeProvider.swapIn("0xexchangeId", address(reserveToken), address(token), 1e18); + } + + function test_swapIn_whenTokenInNotInexchange_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.swapIn(exchangeId, address(token2), address(token), 1e18); + } + + function test_swapIn_whenTokenOutNotInexchange_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.swapIn(exchangeId, address(token), address(token2), 1e18); + } + + function test_swapIn_whenTokenInEqualsTokenOut_itReverts() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.swapIn(exchangeId, address(token), address(token), 1e18); + } + + function test_swapIn_whenTokenInIsReserveAsset_shouldSwapIn() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountIn = 1e18; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 reserveBalanceBefore = poolExchange1.reserveBalance; + uint256 tokenSupplyBefore = poolExchange1.tokenSupply; + + 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); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore + amountIn); + assertEq(tokenSupplyAfter, tokenSupplyBefore + amountOut); + } + + function test_swapIn_whenTokenInIsToken_shouldSwapIn() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountIn = 1e18; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 reserveBalanceBefore = poolExchange1.reserveBalance; + uint256 tokenSupplyBefore = poolExchange1.tokenSupply; + + 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); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); + assertEq(tokenSupplyAfter, tokenSupplyBefore - amountIn); + } +} + +contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { + function test_swapOut_whenCallerIsNotBroker_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(makeAddr("NotBroker")); + vm.expectRevert("Caller is not the Broker"); + bancorExchangeProvider.swapOut(exchangeId, address(reserveToken), address(token), 1e18); + } + + function test_swapOut_whenExchangeDoesNotExist_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + vm.prank(brokerAddress); + vm.expectRevert("Exchange does not exist"); + bancorExchangeProvider.swapOut("0xexchangeId", address(reserveToken), address(token), 1e18); + } + + function test_swapOut_whenTokenInNotInexchange_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.swapOut(exchangeId, address(token2), address(token), 1e18); + } + + function test_swapOut_whenTokenOutNotInexchange_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.swapOut(exchangeId, address(token), address(token2), 1e18); + } + + function test_swapOut_whenTokenInEqualsTokenOut_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.swapOut(exchangeId, address(token), address(token), 1e18); + } + + function test_swapOut_whenTokenInIsReserveAsset_shouldSwapOut() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountOut = 1e18; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 reserveBalanceBefore = poolExchange1.reserveBalance; + uint256 tokenSupplyBefore = poolExchange1.tokenSupply; + + 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); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore + amountIn); + assertEq(tokenSupplyAfter, tokenSupplyBefore + amountOut); + } + + function test_swapOut_whenTokenInIsToken_shouldSwapOut() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountOut = 1e18; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 reserveBalanceBefore = poolExchange1.reserveBalance; + uint256 tokenSupplyBefore = poolExchange1.tokenSupply; + + 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); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); + assertEq(tokenSupplyAfter, tokenSupplyBefore - amountIn); + } +} diff --git a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol new file mode 100644 index 00000000..c87f5418 --- /dev/null +++ b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol @@ -0,0 +1,896 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +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 { GoodDollarExchangeProvider } from "contracts/goodDollar/GoodDollarExchangeProvider.sol"; +import { ERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/ERC20.sol"; + +import { IReserve } from "contracts/interfaces/IReserve.sol"; +import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; + +contract GoodDollarExchangeProviderTest is Test { + /* ------- Events from IGoodDollarExchangeProvider ------- */ + + 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; + ERC20 public token; + ERC20 public token2; + + address public reserveAddress; + address public brokerAddress; + address public avatarAddress; + address public expansionControllerAddress; + + IBancorExchangeProvider.PoolExchange public poolExchange1; + IBancorExchangeProvider.PoolExchange public poolExchange2; + IBancorExchangeProvider.PoolExchange public poolExchange; + + function setUp() public virtual { + reserveToken = new ERC20("cUSD", "cUSD"); + token = new ERC20("Good$", "G$"); + token2 = new ERC20("Good2$", "G2$"); + + reserveAddress = makeAddr("Reserve"); + brokerAddress = makeAddr("Broker"); + avatarAddress = makeAddr("Avatar"); + expansionControllerAddress = makeAddr("ExpansionController"); + + poolExchange1 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: 300_000 * 1e18, + reserveBalance: 60_000 * 1e18, + reserveRatio: 0.2 * 1e8, + 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 + }); + + poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: 7_000_000_000 * 1e18, + reserveBalance: 200_000 * 1e18, + reserveRatio: 1e8 * 0.28571428, + exitContribution: 1e8 * 0.1 + }); + + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token)), + abi.encode(true) + ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token2)), + abi.encode(true) + ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken)), + abi.encode(true) + ); + } + + function initializeGoodDollarExchangeProvider() internal returns (GoodDollarExchangeProvider) { + GoodDollarExchangeProvider exchangeProvider = new GoodDollarExchangeProvider(false); + + exchangeProvider.initialize(brokerAddress, reserveAddress, expansionControllerAddress, avatarAddress); + return exchangeProvider; + } +} + +contract GoodDollarExchangeProviderTest_initializerSettersGetters is GoodDollarExchangeProviderTest { + GoodDollarExchangeProvider exchangeProvider; + + function setUp() public override { + super.setUp(); + exchangeProvider = initializeGoodDollarExchangeProvider(); + } + + /* ---------- Initilizer ---------- */ + + function test_initializer() public view { + assertEq(exchangeProvider.owner(), address(this)); + assertEq(exchangeProvider.broker(), brokerAddress); + assertEq(address(exchangeProvider.reserve()), reserveAddress); + assertEq(address(exchangeProvider.expansionController()), expansionControllerAddress); + assertEq(exchangeProvider.AVATAR(), avatarAddress); + } + + /* ---------- Setters ---------- */ + + function test_setAvatar_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + exchangeProvider.setAvatar(makeAddr("NewAvatar")); + } + + function test_setAvatar_whenAddressIsZero_shouldRevert() public { + vm.expectRevert("Avatar address must be set"); + exchangeProvider.setAvatar(address(0)); + } + + function test_setAvatar_whenSenderIsOwner_shouldUpdateAndEmit() public { + address newAvatar = makeAddr("NewAvatar"); + vm.expectEmit(true, true, true, true); + emit AvatarUpdated(newAvatar); + exchangeProvider.setAvatar(newAvatar); + + assertEq(exchangeProvider.AVATAR(), newAvatar); + } + + function test_setExpansionController_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + exchangeProvider.setExpansionController(makeAddr("NewExpansionController")); + } + + function test_setExpansionController_whenAddressIsZero_shouldRevert() public { + vm.expectRevert("ExpansionController address must be set"); + exchangeProvider.setExpansionController(address(0)); + } + + function test_setExpansionController_whenSenderIsOwner_shouldUpdateAndEmit() public { + address newExpansionController = makeAddr("NewExpansionController"); + vm.expectEmit(true, true, true, true); + emit ExpansionControllerUpdated(newExpansionController); + exchangeProvider.setExpansionController(newExpansionController); + + 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 { + GoodDollarExchangeProvider exchangeProvider; + bytes32 exchangeId; + uint256 expansionRate; + uint256 reserveRatioScalar; + + function setUp() public override { + super.setUp(); + // based on a yearly expansion rate of 10% the daily rate is: + // (1-x)^365 = 0.9 -> x = 1 - 0.9^(1/365) = 0.00028861728902231263... + expansionRate = 288617289022312; + reserveRatioScalar = 1e18 - expansionRate; + exchangeProvider = initializeGoodDollarExchangeProvider(); + vm.prank(avatarAddress); + exchangeId = exchangeProvider.createExchange(poolExchange); + } + + function test_mintFromExpansion_whenCallerIsNotExpansionController_shouldRevert() public { + vm.prank(makeAddr("NotExpansionController")); + vm.expectRevert("Only ExpansionController can call this function"); + exchangeProvider.mintFromExpansion(exchangeId, expansionRate); + } + + function test_mintFromExpansionRate_whenReserveRatioScalarIs0_shouldRevert() public { + vm.prank(expansionControllerAddress); + vm.expectRevert("Reserve ratio scalar must be greater than 0"); + exchangeProvider.mintFromExpansion(exchangeId, 0); + } + + function test_mintFromExpansion_whenExchangeIdIsInvalid_shouldRevert() public { + vm.prank(expansionControllerAddress); + vm.expectRevert("Exchange does not exist"); + exchangeProvider.mintFromExpansion(bytes32(0), expansionRate); + } + + function test_mintFromExpansion_whenNewRatioIsZero_shouldRevert() public { + uint256 verySmallReserveRatioScalar = 1; + + vm.expectRevert("New ratio must be greater than 0"); + vm.prank(expansionControllerAddress); + exchangeProvider.mintFromExpansion(exchangeId, verySmallReserveRatioScalar); + } + + function test_mintFromExpansion_whenReserveRatioScalarIs100Percent_shouldReturn0() public { + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, 1e18); + assertEq(amountToMint, 0, "Minted amount should be 0"); + } + + function test_mintFromExpansion_whenValidReserveRatioScalar_shouldReturnCorrectAmountAndEmit() public { + // reserveRatioScalar is (1-0.000288617289022312) based of 10% yearly expansion rate + // Formula: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio + // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * (1-0.000288617289022312) = 0.285631817919071438 + // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.285631817919071438) / 0.285631817919071438 + // ≈ 2_020_904,291074052815139287 + uint32 expectedReserveRatio = 28563181; + uint256 expectedAmountToMint = 2020904291074052815139287; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq(amountToMint, expectedAmountToMint, "Minted amount should be correct"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); + // 0.01% relative error tolerance because of precision loss when new reserve ratio is calculated + assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + } + + function test_mintFromExpansion_withSmallReserveRatioScalar_shouldReturnCorrectAmount() public { + uint256 smallReserveRatioScalar = 1e18 * 0.00001; // 0.001% + // Formula: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio + // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * 1e13/1e18 = 0.0000028571428 + // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.0000028571428) /0.0000028571428 + // amountToMint ≈ 699993000000000 + uint32 expectedReserveRatio = 285; + uint256 expectedAmountToMint = 699993000000000 * 1e18; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, smallReserveRatioScalar); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq(amountToMint, expectedAmountToMint, "Minted amount should be correct"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); + // 1% relative error tolerance because of precision loss when new reserve ratio is calculated + assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.01, "Price should remain within 1% of initial price"); + } + + function test_mintFromExpansion_withLargeReserveRatioScalar_shouldReturnCorrectAmount() public { + uint256 largeReserveRatioScalar = 1e18 - 1; // Just below 100% + // Formula: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio + // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * (1e18 -1)/1e18 ≈ 0.285714279999999999 + // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.285714279999999999) /0.285714279999999999 + // amountToMint ≈ 0.00000002450000049000 + uint32 expectedReserveRatio = 28571427; + uint256 expectedAmountToMint = 24500000490; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, largeReserveRatioScalar); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq(amountToMint, expectedAmountToMint, "Minted amount should be correct"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); + // 0.01% relative error tolerance because of precision loss when new reserve ratio is calculated + assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + } + + function test_mintFromExpansion_withMultipleConsecutiveExpansions_shouldMintCorrectly() public { + uint256 totalMinted = 0; + uint256 initialTokenSupply = poolExchange.tokenSupply; + uint32 initialReserveRatio = poolExchange.reserveRatio; + uint256 initialReserveBalance = poolExchange.reserveBalance; + uint256 initialPrice = exchangeProvider.currentPrice(exchangeId); + + vm.startPrank(expansionControllerAddress); + for (uint256 i = 0; i < 5; i++) { + uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar); + totalMinted += amountToMint; + assertGt(amountToMint, 0, "Amount minted should be greater than 0"); + } + vm.stopPrank(); + + // Calculate expected reserve ratio + // daily Scalar is applied 5 times newRatio = initialReserveRatio * (dailyScalar ** 5) + // newRatio = 0.28571428 * (0.999711382710977688 ** 5) ≈ 0.2853022075264986 + uint256 expectedReserveRatio = 28530220; + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq( + poolExchangeAfter.tokenSupply, + initialTokenSupply + totalMinted, + "Token supply should increase by total minted amount" + ); + assertLt(poolExchangeAfter.reserveRatio, initialReserveRatio, "Reserve ratio should decrease"); + assertEq(poolExchangeAfter.reserveBalance, initialReserveBalance, "Reserve balance should remain unchanged"); + assertApproxEqRel( + poolExchangeAfter.reserveRatio, + uint32(expectedReserveRatio), + 1e18 * 0.0001, // 0.01% relative error tolerance because of precision loss when new reserve ratio is calculated + "Reserve ratio should be updated correctly within 0.01% tolerance" + ); + assertApproxEqRel(initialPrice, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + } + + function testFuzz_mintFromExpansion(uint256 reserveRatioScalar) public { + // 0.001% to 100% + reserveRatioScalar = bound(reserveRatioScalar, 1e18 * 0.00001, 1e18); + + uint256 initialTokenSupply = poolExchange.tokenSupply; + uint32 initialReserveRatio = poolExchange.reserveRatio; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + uint256 expectedReserveRatio = (uint256(initialReserveRatio) * reserveRatioScalar) / 1e18; + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, uint32(expectedReserveRatio)); + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertGe(amountToMint, 0, "Minted amount should be greater or equal than 0"); + assertGe(initialReserveRatio, poolExchangeAfter.reserveRatio, "Reserve ratio should decrease"); + assertEq( + poolExchangeAfter.tokenSupply, + initialTokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); + // 1% relative error tolerance because of precision loss when new reserve ratio is calculated + assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.01, "Price should remain within 1% of initial price"); + } +} + +contract GoodDollarExchangeProviderTest_mintFromInterest is GoodDollarExchangeProviderTest { + GoodDollarExchangeProvider exchangeProvider; + bytes32 exchangeId; + uint256 reserveInterest; + + function setUp() public override { + super.setUp(); + reserveInterest = 1000 * 1e18; + exchangeProvider = initializeGoodDollarExchangeProvider(); + vm.prank(avatarAddress); + exchangeId = exchangeProvider.createExchange(poolExchange); + } + + function test_mintFromInterest_whenCallerIsNotExpansionController_shouldRevert() public { + vm.prank(makeAddr("NotExpansionController")); + vm.expectRevert("Only ExpansionController can call this function"); + exchangeProvider.mintFromInterest(exchangeId, reserveInterest); + } + + function test_mintFromInterest_whenExchangeIdIsInvalid_shouldRevert() public { + vm.prank(expansionControllerAddress); + vm.expectRevert("Exchange does not exist"); + exchangeProvider.mintFromInterest(bytes32(0), reserveInterest); + } + + function test_mintFromInterest_whenInterestIs0_shouldReturn0() public { + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromInterest(exchangeId, 0); + assertEq(amountToMint, 0, "Minted amount should be 0"); + } + + function test_mintFromInterest_whenInterestLarger0_shouldReturnCorrectAmount() public { + uint256 interest = 1_000 * 1e18; + // formula: amountToMint = reserveInterest * tokenSupply / reserveBalance + // amountToMint = 1_000 * 7_000_000_000 / 200_000 = 35_000_000 + uint256 expectedAmountToMint = 35_000_000 * 1e18; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromInterest(exchangeId, interest); + + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + + assertEq(amountToMint, expectedAmountToMint, "Minted amount should be correct"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); + assertEq( + poolExchangeAfter.reserveBalance, + poolExchange.reserveBalance + interest, + "Reserve balance should increase by interest amount" + ); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); + } + + function test_mintFromInterest_whenInterestIsSmall_shouldReturnCorrectAmount() public { + uint256 interest = 100; // 100wei + // formula: amountToMint = reserveInterest * tokenSupply / reserveBalance + // amountToMint = (100/1e18) * 7_000_000_000 / 200_000 = 0.000000000003500000 + uint256 expectedAmountToMint = 3_500_000; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromInterest(exchangeId, interest); + + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + + assertEq(amountToMint, expectedAmountToMint, "Minted amount should be correct"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); + assertEq( + poolExchangeAfter.reserveBalance, + poolExchange.reserveBalance + interest, + "Reserve balance should increase by interest amount" + ); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); + } + + function test_mintFromInterest_whenInterestIsLarge_shouldReturnCorrectAmount() public { + // 1_000_000 reserve tokens 5 times current reserve balance + uint256 interest = 1_000_000 * 1e18; + // formula: amountToMint = reserveInterest * tokenSupply / reserveBalance + // amountToMint = 1_000_000 * 7_000_000_000 / 200_000 = 35_000_000_000 + uint256 expectedAmountToMint = 35_000_000_000 * 1e18; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromInterest(exchangeId, interest); + + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + + assertEq(amountToMint, expectedAmountToMint, "Minted amount should be correct"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); + assertEq( + poolExchangeAfter.reserveBalance, + poolExchange.reserveBalance + interest, + "Reserve balance should increase by interest amount" + ); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); + } + + function test_mintFromInterest_withMultipleConsecutiveInterests_shouldMintCorrectly() public { + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.startPrank(expansionControllerAddress); + uint256 totalMinted = 0; + for (uint256 i = 0; i < 5; i++) { + uint256 amountToMint = exchangeProvider.mintFromInterest(exchangeId, reserveInterest); + totalMinted += amountToMint; + } + vm.stopPrank(); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + totalMinted, + "Token supply should increase by total minted amount" + ); + assertEq( + poolExchangeAfter.reserveBalance, + poolExchange.reserveBalance + reserveInterest * 5, + "Reserve balance should increase by total interest" + ); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); + } + + function testFuzz_mintFromInterest(uint256 fuzzedInterest) public { + fuzzedInterest = bound(fuzzedInterest, 1, type(uint256).max / poolExchange.tokenSupply); + + uint256 initialTokenSupply = poolExchange.tokenSupply; + uint256 initialReserveBalance = poolExchange.reserveBalance; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromInterest(exchangeId, fuzzedInterest); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertGt(amountToMint, 0, "Minted amount should be greater than 0"); + assertEq( + poolExchangeAfter.tokenSupply, + initialTokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); + assertEq( + poolExchangeAfter.reserveBalance, + initialReserveBalance + fuzzedInterest, + "Reserve balance should increase by interest amount" + ); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); + } +} + +contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchangeProviderTest { + GoodDollarExchangeProvider exchangeProvider; + bytes32 exchangeId; + uint256 reward; + + function setUp() public override { + super.setUp(); + reward = 1000 * 1e18; + exchangeProvider = initializeGoodDollarExchangeProvider(); + vm.prank(avatarAddress); + exchangeId = exchangeProvider.createExchange(poolExchange); + } + + function test_updateRatioForReward_whenCallerIsNotExpansionController_shouldRevert() public { + vm.prank(makeAddr("NotExpansionController")); + vm.expectRevert("Only ExpansionController can call this function"); + exchangeProvider.updateRatioForReward(exchangeId, reward); + } + + function test_updateRatioForReward_whenExchangeIdIsInvalid_shouldRevert() public { + vm.prank(expansionControllerAddress); + vm.expectRevert("Exchange does not exist"); + exchangeProvider.updateRatioForReward(bytes32(0), reward); + } + + function test_updateRatioForReward_whenRewardLarger0_shouldReturnCorrectRatioAndEmit() public { + // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) + // reserveRatio = 200_000 / ((7_000_000_000 + 1_000) * 0.000100000002) ≈ 0.28571423... + uint32 expectedReserveRatio = 28571423; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); + exchangeProvider.updateRatioForReward(exchangeId, reward); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + reward, + "Token supply should increase by reward amount" + ); + // 1% relative error tolerance because of precision loss when new reserve ratio is calculated + assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + } + + function test_updateRatioForReward_whenRewardIsSmall_shouldReturnCorrectRatioAndEmit() public { + uint256 reward = 1e18; // 1 token + // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) + // reserveRatio = 200_000 / ((7_000_000_000 + 1) * 0.000100000002) ≈ 0.2857142799 + uint32 expectedReserveRatio = 28571427; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); + exchangeProvider.updateRatioForReward(exchangeId, reward); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + reward, + "Token supply should increase by reward amount" + ); + assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + } + + function test_updateRatioForReward_whenRewardIsLarge_shouldReturnCorrectRatioAndEmit() public { + uint256 reward = 1_000_000_000 * 1e18; // 1 billion tokens + // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) + // reserveRatio = 200_000 / ((7_000_000_000 + 1_000_000_000) * 0.000100000002) ≈ 0.2499999950000... + + uint32 expectedReserveRatio = 24999999; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); + exchangeProvider.updateRatioForReward(exchangeId, reward); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + reward, + "Token supply should increase by reward amount" + ); + assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + } + + function test_updateRatioForReward_withMultipleConsecutiveRewards() public { + uint256 totalReward = 0; + uint256 initialTokenSupply = poolExchange.tokenSupply; + uint256 initialReserveBalance = poolExchange.reserveBalance; + uint32 initialReserveRatio = poolExchange.reserveRatio; + uint256 initialPrice = exchangeProvider.currentPrice(exchangeId); + + vm.startPrank(expansionControllerAddress); + for (uint256 i = 0; i < 5; i++) { + exchangeProvider.updateRatioForReward(exchangeId, reward); + totalReward += reward; + } + vm.stopPrank(); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq( + poolExchangeAfter.tokenSupply, + initialTokenSupply + totalReward, + "Token supply should increase by total reward" + ); + assertEq(poolExchangeAfter.reserveBalance, initialReserveBalance, "Reserve balance should remain unchanged"); + assertLt(poolExchangeAfter.reserveRatio, initialReserveRatio, "Reserve ratio should decrease"); + assertApproxEqRel(initialPrice, priceAfter, 1e18 * 0.001, "Price should remain within 0.1% of initial price"); + } + + function testFuzz_updateRatioForReward(uint256 fuzzedReward) public { + // 1 to 100 trillion tokens + fuzzedReward = bound(fuzzedReward, 1, 100_000_000_000_000 * 1e18); + + uint256 initialTokenSupply = poolExchange.tokenSupply; + uint256 initialReserveBalance = poolExchange.reserveBalance; + uint32 initialReserveRatio = poolExchange.reserveRatio; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.prank(expansionControllerAddress); + exchangeProvider.updateRatioForReward(exchangeId, fuzzedReward); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq( + poolExchangeAfter.tokenSupply, + initialTokenSupply + fuzzedReward, + "Token supply should increase by reward amount" + ); + assertEq(poolExchangeAfter.reserveBalance, initialReserveBalance, "Reserve balance should remain unchanged"); + assertLe(poolExchangeAfter.reserveRatio, initialReserveRatio, "Reserve ratio should stay the same or decrease"); + assertApproxEqRel( + priceBefore, + priceAfter, + 1e18 * 0.001, + "Price should remain unchanged, with a max relative error of 0.1%" + ); + } +} + +contract GoodDollarExchangeProviderTest_pausable is GoodDollarExchangeProviderTest { + GoodDollarExchangeProvider exchangeProvider; + bytes32 exchangeId; + + function setUp() public override { + super.setUp(); + exchangeProvider = initializeGoodDollarExchangeProvider(); + vm.prank(avatarAddress); + exchangeId = exchangeProvider.createExchange(poolExchange1); + } + + function test_pause_whenCallerIsNotAvatar_shouldRevert() public { + vm.prank(makeAddr("NotAvatar")); + vm.expectRevert("Only Avatar can call this function"); + exchangeProvider.pause(); + } + + function test_unpause_whenCallerIsNotAvatar_shouldRevert() public { + vm.prank(makeAddr("NotAvatar")); + vm.expectRevert("Only Avatar can call this function"); + exchangeProvider.unpause(); + } + + function test_pause_whenCallerIsAvatar_shouldPauseAndDisableExchange() public { + vm.prank(avatarAddress); + exchangeProvider.pause(); + + assert(exchangeProvider.paused()); + + vm.startPrank(brokerAddress); + vm.expectRevert("Pausable: paused"); + exchangeProvider.swapIn(exchangeId, address(reserveToken), address(token), 1e18); + + vm.expectRevert("Pausable: paused"); + exchangeProvider.swapOut(exchangeId, address(reserveToken), address(token), 1e18); + + vm.startPrank(expansionControllerAddress); + vm.expectRevert("Pausable: paused"); + exchangeProvider.mintFromExpansion(exchangeId, 1e18); + + vm.expectRevert("Pausable: paused"); + exchangeProvider.mintFromInterest(exchangeId, 1e18); + + vm.expectRevert("Pausable: paused"); + exchangeProvider.updateRatioForReward(exchangeId, 1e18); + } + + function test_unpause_whenCallerIsAvatar_shouldUnpauseAndEnableExchange() public { + vm.prank(avatarAddress); + exchangeProvider.pause(); + + vm.prank(avatarAddress); + exchangeProvider.unpause(); + + assert(exchangeProvider.paused() == false); + + vm.startPrank(brokerAddress); + + exchangeProvider.swapIn(exchangeId, address(reserveToken), address(token), 1e18); + exchangeProvider.swapOut(exchangeId, address(reserveToken), address(token), 1e18); + + vm.startPrank(expansionControllerAddress); + + exchangeProvider.mintFromExpansion(exchangeId, 1e18); + exchangeProvider.mintFromInterest(exchangeId, 1e18); + exchangeProvider.updateRatioForReward(exchangeId, 1e18); + } +} diff --git a/test/unit/goodDollar/GoodDollarExpansionController.t.sol b/test/unit/goodDollar/GoodDollarExpansionController.t.sol new file mode 100644 index 00000000..4e46e620 --- /dev/null +++ b/test/unit/goodDollar/GoodDollarExpansionController.t.sol @@ -0,0 +1,627 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +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 { ERC20Mock } from "openzeppelin-contracts-next/contracts/mocks/ERC20Mock.sol"; +import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarExpansionController.sol"; + +import { IGoodDollarExpansionController } from "contracts/interfaces/IGoodDollarExpansionController.sol"; +import { IGoodDollarExchangeProvider } from "contracts/interfaces/IGoodDollarExchangeProvider.sol"; +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; +import { IDistributionHelper } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; + +import { GoodDollarExpansionControllerHarness } from "test/utils/harnesses/GoodDollarExpansionControllerHarness.sol"; + +contract GoodDollarExpansionControllerTest is Test { + /* ------- Events from IGoodDollarExpansionController ------- */ + + event GoodDollarExchangeProviderUpdated(address indexed exchangeProvider); + + event DistributionHelperUpdated(address indexed distributionHelper); + + event ReserveUpdated(address indexed reserve); + + event AvatarUpdated(address indexed avatar); + + event ExpansionConfigSet(bytes32 indexed exchangeId, uint64 expansionRate, uint32 expansionFrequency); + + event RewardMinted(bytes32 indexed exchangeId, address indexed to, uint256 amount); + + event InterestUBIMinted(bytes32 indexed exchangeId, uint256 amount); + + event ExpansionUBIMinted(bytes32 indexed exchangeId, uint256 amount); + + /* ------------------------------------------- */ + + ERC20Mock public reserveToken; + ERC20Mock public token; + + address public exchangeProvider; + address public distributionHelper; + address public reserveAddress; + address public avatarAddress; + + bytes32 exchangeId = "ExchangeId"; + + uint64 expansionRate = 1e18 * 0.01; + uint32 expansionFrequency = uint32(1 days); + + IBancorExchangeProvider.PoolExchange pool; + + function setUp() public virtual { + reserveToken = new ERC20Mock("cUSD", "cUSD", address(this), 1); + token = new ERC20Mock("Good$", "G$", address(this), 1); + + exchangeProvider = makeAddr("ExchangeProvider"); + distributionHelper = makeAddr("DistributionHelper"); + reserveAddress = makeAddr("Reserve"); + avatarAddress = makeAddr("Avatar"); + + pool = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: 7 * 1e9 * 1e18, + reserveBalance: 200_000 * 1e18, + reserveRatio: 0.2 * 1e8, // 20% + exitContribution: 0.1 * 1e8 // 10% + }); + + vm.mockCall( + exchangeProvider, + abi.encodeWithSelector(IBancorExchangeProvider(exchangeProvider).getPoolExchange.selector), + abi.encode(pool) + ); + } + + function initializeGoodDollarExpansionController() internal returns (GoodDollarExpansionController) { + GoodDollarExpansionController expansionController = new GoodDollarExpansionController(false); + + expansionController.initialize(exchangeProvider, distributionHelper, reserveAddress, avatarAddress); + + return expansionController; + } +} + +contract GoodDollarExpansionControllerTest_initializerSettersGetters is GoodDollarExpansionControllerTest { + GoodDollarExpansionController expansionController; + + function setUp() public override { + super.setUp(); + expansionController = initializeGoodDollarExpansionController(); + } + + /* ---------- Initilizer ---------- */ + + function test_initializer() public view { + assertEq(address(expansionController.distributionHelper()), distributionHelper); + assertEq(expansionController.reserve(), reserveAddress); + assertEq(address(expansionController.goodDollarExchangeProvider()), exchangeProvider); + assertEq(expansionController.AVATAR(), avatarAddress); + } + + /* ---------- Getters ---------- */ + + function test_getExpansionConfig_whenConfigIsNotSet_shouldRevert() public { + vm.expectRevert("Expansion config not set"); + expansionController.getExpansionConfig("NotSetExchangeId"); + } + + function test_getExpansionConfig_whenConfigIsSet_shouldReturnConfig() public { + vm.prank(avatarAddress); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( + exchangeId + ); + + assertEq(config.expansionRate, expansionRate); + assertEq(config.expansionFrequency, expansionFrequency); + assertEq(config.lastExpansion, 0); + } + + /* ---------- Setters ---------- */ + + function test_setGoodDollarExchangeProvider_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + expansionController.setGoodDollarExchangeProvider(makeAddr("NewExchangeProvider")); + } + + function test_setGoodDollarExchangeProvider_whenAddressIsZero_shouldRevert() public { + vm.expectRevert("GoodDollarExchangeProvider address must be set"); + expansionController.setGoodDollarExchangeProvider(address(0)); + } + + function test_setGoodDollarExchangeProvider_whenSenderIsOwner_shouldUpdateAndEmit() public { + address newExchangeProvider = makeAddr("NewExchangeProvider"); + vm.expectEmit(true, true, true, true); + emit GoodDollarExchangeProviderUpdated(newExchangeProvider); + expansionController.setGoodDollarExchangeProvider(newExchangeProvider); + + 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"); + expansionController.setDistributionHelper(makeAddr("NewDistributionHelper")); + } + + 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_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 { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + expansionController.setReserve(makeAddr("NewReserve")); + } + + function test_setReserve_whenAddressIsZero_shouldRevert() public { + vm.expectRevert("Reserve address must be set"); + expansionController.setReserve(address(0)); + } + + function test_setReserve_whenCallerIsOwner_shouldUpdateAndEmit() public { + address newReserve = makeAddr("NewReserve"); + vm.expectEmit(true, true, true, true); + emit ReserveUpdated(newReserve); + expansionController.setReserve(newReserve); + + assertEq(expansionController.reserve(), newReserve); + } + + function test_setAvatar_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + expansionController.setAvatar(makeAddr("NewAvatar")); + } + + function test_setAvatar_whenAddressIsZero_shouldRevert() public { + vm.expectRevert("Avatar address must be set"); + expansionController.setAvatar(address(0)); + } + + function test_setAvatar_whenCallerIsOwner_shouldUpdateAndEmit() public { + address newAvatar = makeAddr("NewAvatar"); + vm.expectEmit(true, true, true, true); + emit AvatarUpdated(newAvatar); + expansionController.setAvatar(newAvatar); + + assertEq(expansionController.AVATAR(), newAvatar); + } + + function test_setExpansionConfig_whenSenderIsNotAvatar_shouldRevert() public { + vm.prank(makeAddr("NotAvatar")); + vm.expectRevert("Only Avatar can call this function"); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + } + + function test_setExpansionConfig_whenExpansionRateIsLargerOrEqualToOne_shouldRevert() public { + expansionRate = 1e18; + + vm.prank(avatarAddress); + vm.expectRevert("Expansion rate must be less than 100%"); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + } + + function test_setExpansionConfig_whenExpansionRateIsZero_shouldRevert() public { + expansionRate = 0; + + vm.prank(avatarAddress); + vm.expectRevert("Expansion rate must be greater than 0"); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + } + + function test_setExpansionConfig_whenExpansionFrequencyIsZero_shouldRevert() public { + expansionFrequency = 0; + + vm.prank(avatarAddress); + vm.expectRevert("Expansion frequency must be greater than 0"); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + } + + function test_setExpansionConfig_whenCallerIsAvatar_shouldUpdateAndEmit() public { + vm.prank(avatarAddress); + vm.expectEmit(true, true, true, true); + emit ExpansionConfigSet(exchangeId, expansionRate, expansionFrequency); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( + exchangeId + ); + + assertEq(config.expansionRate, expansionRate); + assertEq(config.expansionFrequency, expansionFrequency); + assertEq(config.lastExpansion, 0); + } +} + +contract GoodDollarExpansionControllerTest_mintUBIFromInterest is GoodDollarExpansionControllerTest { + GoodDollarExpansionController expansionController; + + function setUp() public override { + super.setUp(); + expansionController = initializeGoodDollarExpansionController(); + vm.prank(avatarAddress); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + + vm.mockCall( + exchangeProvider, + abi.encodeWithSelector(IGoodDollarExchangeProvider(exchangeProvider).mintFromInterest.selector), + abi.encode(1000e18) + ); + } + + function test_mintUBIFromInterest_whenReserveInterestIs0_shouldRevert() public { + vm.expectRevert("Reserve interest must be greater than 0"); + expansionController.mintUBIFromInterest(exchangeId, 0); + } + + function test_mintUBIFromInterest_whenAmountToMintIsLargerThan0_shouldMintTransferAndEmit() public { + uint256 reserveInterest = 1000e18; + uint256 amountToMint = 1000e18; + address interestCollector = makeAddr("InterestCollector"); + + deal(address(reserveToken), interestCollector, reserveInterest); + assertEq(reserveToken.balanceOf(interestCollector), reserveInterest); + + uint256 interestCollectorBalanceBefore = reserveToken.balanceOf(interestCollector); + uint256 reserveBalanceBefore = reserveToken.balanceOf(reserveAddress); + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.startPrank(interestCollector); + reserveToken.approve(address(expansionController), reserveInterest); + + vm.expectEmit(true, true, true, true); + emit InterestUBIMinted(exchangeId, amountToMint); + expansionController.mintUBIFromInterest(exchangeId, reserveInterest); + + assertEq(reserveToken.balanceOf(reserveAddress), reserveBalanceBefore + reserveInterest); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + assertEq(reserveToken.balanceOf(interestCollector), interestCollectorBalanceBefore - reserveInterest); + } +} + +contract GoodDollarExpansionControllerTest_mintUBIFromReserveBalance is GoodDollarExpansionControllerTest { + GoodDollarExpansionController expansionController; + + function setUp() public override { + super.setUp(); + expansionController = initializeGoodDollarExpansionController(); + vm.prank(avatarAddress); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + + vm.mockCall( + exchangeProvider, + abi.encodeWithSelector(IGoodDollarExchangeProvider(exchangeProvider).mintFromInterest.selector), + abi.encode(1000e18) + ); + } + + function test_mintUBIFromReserveBalance_whenAdditionalReserveBalanceIs0_shouldReturn0() public { + deal(address(reserveToken), reserveAddress, pool.reserveBalance); + uint256 amountMinted = expansionController.mintUBIFromReserveBalance(exchangeId); + assertEq(amountMinted, 0); + } + + function test_mintUBIFromReserveBalance_whenAdditionalReserveBalanceIsLargerThan0_shouldMintAndEmit() public { + uint256 amountToMint = 1000e18; + uint256 additionalReserveBalance = 1000e18; + + deal(address(reserveToken), reserveAddress, pool.reserveBalance + additionalReserveBalance); + + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.expectEmit(true, true, true, true); + emit InterestUBIMinted(exchangeId, amountToMint); + uint256 amountMinted = expansionController.mintUBIFromReserveBalance(exchangeId); + + assertEq(amountMinted, amountToMint); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + } +} + +contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExpansionControllerTest { + GoodDollarExpansionController expansionController; + + function setUp() public override { + super.setUp(); + expansionController = initializeGoodDollarExpansionController(); + vm.prank(avatarAddress); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + + vm.mockCall( + exchangeProvider, + abi.encodeWithSelector(IGoodDollarExchangeProvider(exchangeProvider).mintFromExpansion.selector), + abi.encode(1000e18) + ); + + vm.mockCall( + distributionHelper, + abi.encodeWithSelector(IDistributionHelper(distributionHelper).onDistribution.selector), + abi.encode(true) + ); + } + + function test_mintUBIFromExpansion_whenExpansionConfigIsNotSet_shouldRevert() public { + vm.expectRevert("Expansion config not set"); + expansionController.mintUBIFromExpansion("NotSetExchangeId"); + } + + function test_mintUBIFromExpansion_whenShouldNotExpand_shouldNotExpand() public { + // doing one initial expansion to not be first expansion + // since on first expansion the expansion is always applied once. + expansionController.mintUBIFromExpansion(exchangeId); + + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( + exchangeId + ); + uint256 lastExpansion = config.lastExpansion; + skip(lastExpansion + config.expansionFrequency - 1); + + assertEq(expansionController.mintUBIFromExpansion(exchangeId), 0); + } + + function test_mintUBIFromExpansion_whenFirstExpansionAndLessThanExpansionFrequencyPassed_shouldExpand1Time() public { + // 1 day has passed since last expansion and expansion rate is 1% so the rate passed to the exchangeProvider + // should be 0.99^1 = 0.99 + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( + exchangeId + ); + assert(block.timestamp < config.lastExpansion + config.expansionFrequency); + uint256 reserveRatioScalar = 1e18 * 0.99; + uint256 amountToMint = 1000e18; + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.expectEmit(true, true, true, true); + emit ExpansionUBIMinted(exchangeId, amountToMint); + + vm.expectCall( + exchangeProvider, + abi.encodeWithSelector( + IGoodDollarExchangeProvider(exchangeProvider).mintFromExpansion.selector, + exchangeId, + reserveRatioScalar + ) + ); + vm.expectCall( + distributionHelper, + abi.encodeWithSelector(IDistributionHelper(distributionHelper).onDistribution.selector, amountToMint) + ); + + uint256 amountMinted = expansionController.mintUBIFromExpansion(exchangeId); + config = expansionController.getExpansionConfig(exchangeId); + + assertEq(amountMinted, amountToMint); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + assertEq(config.lastExpansion, block.timestamp); + } + + function test_mintUBIFromExpansion_whenFirstExpansionAndMultipleExpansionFrequenciesPassed_shouldExpand1Time() + public + { + // 1 day has passed since last expansion and expansion rate is 1% so the rate passed to the exchangeProvider + // should be 0.99^1 = 0.99 + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( + exchangeId + ); + skip(config.expansionFrequency * 3 + 1); + assert(block.timestamp > config.lastExpansion + config.expansionFrequency * 3); + uint256 reserveRatioScalar = 1e18 * 0.99; + uint256 amountToMint = 1000e18; + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.expectEmit(true, true, true, true); + emit ExpansionUBIMinted(exchangeId, amountToMint); + + vm.expectCall( + exchangeProvider, + abi.encodeWithSelector( + IGoodDollarExchangeProvider(exchangeProvider).mintFromExpansion.selector, + exchangeId, + reserveRatioScalar + ) + ); + vm.expectCall( + distributionHelper, + abi.encodeWithSelector(IDistributionHelper(distributionHelper).onDistribution.selector, amountToMint) + ); + + uint256 amountMinted = expansionController.mintUBIFromExpansion(exchangeId); + config = expansionController.getExpansionConfig(exchangeId); + + assertEq(amountMinted, amountToMint); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + assertEq(config.lastExpansion, block.timestamp); + } + + function test_mintUBIFromExpansion_when1DayPassed_shouldCalculateCorrectRateAndExpand() public { + // doing one initial expansion to not be first expansion + // since on first expansion the expansion is always applied once. + expansionController.mintUBIFromExpansion(exchangeId); + + // 1 day has passed since last expansion and expansion rate is 1% so the rate passed to the exchangeProvider + // should be 0.99^1 = 0.99 + uint256 reserveRatioScalar = 1e18 * 0.99; + skip(expansionFrequency + 1); + + uint256 amountToMint = 1000e18; + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.expectEmit(true, true, true, true); + emit ExpansionUBIMinted(exchangeId, amountToMint); + + vm.expectCall( + exchangeProvider, + abi.encodeWithSelector( + IGoodDollarExchangeProvider(exchangeProvider).mintFromExpansion.selector, + exchangeId, + reserveRatioScalar + ) + ); + vm.expectCall( + distributionHelper, + abi.encodeWithSelector(IDistributionHelper(distributionHelper).onDistribution.selector, amountToMint) + ); + + uint256 amountMinted = expansionController.mintUBIFromExpansion(exchangeId); + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( + exchangeId + ); + + assertEq(amountMinted, amountToMint); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + assertEq(config.lastExpansion, block.timestamp); + } + + function test_mintUBIFromExpansion_whenMultipleDaysPassed_shouldCalculateCorrectRateAndExpand() public { + // doing one initial expansion to not be first expansion + // since on first expansion the expansion is always applied once. + expansionController.mintUBIFromExpansion(exchangeId); + + // 3 days have passed since last expansion and expansion rate is 1% so the rate passed to the exchangeProvider + // should be 0.99^3 = 0.970299 + uint256 reserveRatioScalar = 1e18 * 0.970299; + + skip(3 * expansionFrequency + 1); + + uint256 amountToMint = 1000e18; + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.expectEmit(true, true, true, true); + emit ExpansionUBIMinted(exchangeId, amountToMint); + + vm.expectCall( + exchangeProvider, + abi.encodeWithSelector( + IGoodDollarExchangeProvider(exchangeProvider).mintFromExpansion.selector, + exchangeId, + reserveRatioScalar + ) + ); + vm.expectCall( + distributionHelper, + abi.encodeWithSelector(IDistributionHelper(distributionHelper).onDistribution.selector, amountToMint) + ); + + uint256 amountMinted = expansionController.mintUBIFromExpansion(exchangeId); + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( + exchangeId + ); + + assertEq(amountMinted, amountToMint); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + assertEq(config.lastExpansion, block.timestamp); + } +} + +contract GoodDollarExpansionControllerTest_getExpansionScalar is GoodDollarExpansionControllerTest { + GoodDollarExpansionControllerHarness expansionController; + + function setUp() public override { + super.setUp(); + expansionController = new GoodDollarExpansionControllerHarness(false); + } + + function test_getExpansionScaler_whenExpansionRateIs0_shouldReturn1e18() public { + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = IGoodDollarExpansionController + .ExchangeExpansionConfig(0, 1, 0); + assertEq(expansionController.exposed_getReserveRatioScalar(config), 1e18); + } + + function test_getExpansionScaler_whenExpansionRateIs1_shouldReturn1() public { + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = IGoodDollarExpansionController + .ExchangeExpansionConfig(1e18 - 1, 1, 0); + assertEq(expansionController.exposed_getReserveRatioScalar(config), 1); + } + + function testFuzz_getExpansionScaler( + uint256 _expansionRate, + uint256 _expansionFrequency, + uint256 _lastExpansion, + uint256 _timeDelta + ) public { + uint64 expansionRate = uint64(bound(_expansionRate, 1, 1e18 - 1)); + uint32 expansionFrequency = uint32(bound(_expansionFrequency, 1, 1e6)); + uint32 lastExpansion = uint32(bound(_lastExpansion, 0, 1e6)); + uint32 timeDelta = uint32(bound(_timeDelta, 0, 1e6)); + + skip(lastExpansion + timeDelta); + + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = IGoodDollarExpansionController + .ExchangeExpansionConfig(expansionRate, expansionFrequency, lastExpansion); + uint256 scaler = expansionController.exposed_getReserveRatioScalar(config); + assert(scaler >= 0 && scaler <= 1e18); + } +} + +contract GoodDollarExpansionControllerTest_mintRewardFromReserveRatio is GoodDollarExpansionControllerTest { + GoodDollarExpansionController expansionController; + + function setUp() public override { + super.setUp(); + expansionController = initializeGoodDollarExpansionController(); + vm.prank(avatarAddress); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + + vm.mockCall( + exchangeProvider, + abi.encodeWithSelector(IGoodDollarExchangeProvider(exchangeProvider).updateRatioForReward.selector), + abi.encode(true) + ); + } + + function test_mintRewardFromReserveRatio_whenCallerIsNotAvatar_shouldRevert() public { + vm.prank(makeAddr("NotAvatar")); + vm.expectRevert("Only Avatar can call this function"); + expansionController.mintRewardFromReserveRatio(exchangeId, makeAddr("To"), 1000e18); + } + + function test_mintRewardFromReserveRatio_whenToIsZero_shouldRevert() public { + vm.prank(avatarAddress); + vm.expectRevert("Recipient address must be set"); + expansionController.mintRewardFromReserveRatio(exchangeId, address(0), 1000e18); + } + + function test_mintRewardFromReserveRatio_whenAmountIs0_shouldRevert() public { + vm.prank(avatarAddress); + vm.expectRevert("Amount must be greater than 0"); + expansionController.mintRewardFromReserveRatio(exchangeId, makeAddr("To"), 0); + } + + function test_mintRewardFromReserveRatio_whenCallerIsAvatar_shouldMintAndEmit() public { + uint256 amountToMint = 1000e18; + address to = makeAddr("To"); + uint256 toBalanceBefore = token.balanceOf(to); + + vm.expectEmit(true, true, true, true); + emit RewardMinted(exchangeId, to, amountToMint); + + vm.prank(avatarAddress); + expansionController.mintRewardFromReserveRatio(exchangeId, to, amountToMint); + + assertEq(token.balanceOf(to), toBalanceBefore + amountToMint); + } +} diff --git a/test/unit/libraries/TradingLimits.t.sol b/test/unit/libraries/TradingLimits.t.sol index 136b2bc2..878fdf24 100644 --- a/test/unit/libraries/TradingLimits.t.sol +++ b/test/unit/libraries/TradingLimits.t.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8; import { Test } from "mento-std/Test.sol"; import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; -import { ITradingLimitsHarness } from "test/utils/harnesses/ITradingLimitsHarness.sol"; +import { TradingLimitsHarness } from "test/utils/harnesses/TradingLimitsHarness.sol"; // forge test --match-contract TradingLimits -vvv contract TradingLimitsTest is Test { @@ -15,7 +15,7 @@ contract TradingLimitsTest is Test { uint8 private constant LG = 4; // 0b100 ITradingLimits.State private state; - ITradingLimitsHarness private harness; + TradingLimitsHarness private harness; function configEmpty() internal pure returns (ITradingLimits.Config memory config) {} @@ -87,7 +87,7 @@ contract TradingLimitsTest is Test { } function setUp() public { - harness = ITradingLimitsHarness(deployCode("TradingLimitsHarness")); + harness = new TradingLimitsHarness(); } /* ==================== Config#validate ==================== */ @@ -286,11 +286,16 @@ contract TradingLimitsTest is Test { assertEq(state.netflowGlobal, 100); } - function test_update_withSubUnitAmounts_updatesAs1() public { + function test_update_withPositiveSubUnitAmounts_updatesAs1() public { state = harness.update(state, configLG(500000), 1e6, 18); assertEq(state.netflowGlobal, 1); } + function test_update_withNegativeSubUnitAmounts_updatesAsMinus1() public { + state = harness.update(state, configLG(500000), -1e6, 18); + assertEq(state.netflowGlobal, -1); + } + function test_update_withTooLargeAmount_reverts() public { vm.expectRevert(bytes("dFlow too large")); state = harness.update(state, configLG(500000), 3 * 10e32, 18); diff --git a/test/unit/swap/Broker.t.sol b/test/unit/swap/Broker.t.sol index 06ef2d80..98a57893 100644 --- a/test/unit/swap/Broker.t.sol +++ b/test/unit/swap/Broker.t.sol @@ -12,7 +12,7 @@ import { TestERC20 } from "test/utils/mocks/TestERC20.sol"; import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; import { IStableTokenV2 } from "contracts/interfaces/IStableTokenV2.sol"; -import { IBroker } from "contracts/interfaces/IBroker.sol"; +import { Broker } from "contracts/swap/Broker.sol"; import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; // forge test --match-contract Broker -vvv @@ -40,24 +40,27 @@ contract BrokerTest is Test { address randomAsset = makeAddr("randomAsset"); MockReserve reserve; + MockReserve reserve1; TestERC20 stableAsset; TestERC20 collateralAsset; - IBroker broker; + Broker broker; MockExchangeProvider exchangeProvider; address exchangeProvider1 = makeAddr("exchangeProvider1"); address exchangeProvider2 = makeAddr("exchangeProvider2"); address[] public exchangeProviders; + address[] public reserves; function setUp() public virtual { /* Dependencies and makeAddrs */ reserve = new MockReserve(); + reserve1 = new MockReserve(); collateralAsset = new TestERC20("Collateral", "CL"); stableAsset = new TestERC20("StableAsset", "SA0"); randomAsset = makeAddr("randomAsset"); - broker = IBroker(deployCode("Broker", abi.encode(true))); + broker = new Broker(true); exchangeProvider = new MockExchangeProvider(); reserve.addToken(address(stableAsset)); @@ -66,7 +69,12 @@ contract BrokerTest is Test { exchangeProviders.push(exchangeProvider1); exchangeProviders.push(exchangeProvider2); exchangeProviders.push((address(exchangeProvider))); - broker.initialize(exchangeProviders, address(reserve)); + reserves.push(address(reserve)); + reserves.push(address(reserve)); + reserves.push(address(reserve)); + + vm.prank(deployer); + broker.initialize(exchangeProviders, reserves); } } @@ -74,15 +82,16 @@ contract BrokerTest_initilizerAndSetters is BrokerTest { /* ---------- Initilizer ---------- */ function test_initilize_shouldSetOwner() public view { - assertEq(broker.owner(), address(this)); + assertEq(broker.owner(), deployer); } function test_initilize_shouldSetExchangeProviderAddresseses() public view { assertEq(broker.getExchangeProviders(), exchangeProviders); } - - function test_initilize_shouldSetReserve() public view { - assertEq(address(broker.reserve()), address(reserve)); + function test_initilize_shouldSetReserves() public view { + assertEq(address(broker.exchangeReserve(exchangeProvider1)), address(reserve)); + assertEq(address(broker.exchangeReserve(exchangeProvider2)), address(reserve)); + assertEq(address(broker.exchangeReserve(address(exchangeProvider))), address(reserve)); } /* ---------- Setters ---------- */ @@ -90,43 +99,62 @@ contract BrokerTest_initilizerAndSetters is BrokerTest { function test_addExchangeProvider_whenSenderIsNotOwner_shouldRevert() public { vm.expectRevert("Ownable: caller is not the owner"); vm.prank(notDeployer); - broker.addExchangeProvider(address(0)); + broker.addExchangeProvider(address(0), address(0)); } - function test_addExchangeProvider_whenAddressIsZero_shouldRevert() public { + function test_addExchangeProvider_whenExchangeProviderAddressIsZero_shouldRevert() public { vm.expectRevert("ExchangeProvider address can't be 0"); - broker.addExchangeProvider(address(0)); + vm.prank(deployer); + broker.addExchangeProvider(address(0), address(reserve)); + } + + function test_addExchangeProvider_whenReserveAddressIsZero_shouldRevert() public { + changePrank(deployer); + vm.expectRevert("Reserve address can't be 0"); + broker.addExchangeProvider(makeAddr("newExchangeProvider"), address(0)); } function test_addExchangeProvider_whenSenderIsOwner_shouldUpdateAndEmit() public { address newExchangeProvider = makeAddr("newExchangeProvider"); - vm.expectEmit(true, false, false, false); + + vm.expectEmit(true, true, true, true); emit ExchangeProviderAdded(newExchangeProvider); - broker.addExchangeProvider(newExchangeProvider); + vm.expectEmit(true, true, true, true); + emit ReserveSet(newExchangeProvider, address(reserve1)); + + vm.prank(deployer); + broker.addExchangeProvider(newExchangeProvider, address(reserve1)); + address[] memory updatedExchangeProviders = broker.getExchangeProviders(); assertEq(updatedExchangeProviders[updatedExchangeProviders.length - 1], newExchangeProvider); assertEq(broker.isExchangeProvider(newExchangeProvider), true); + assertEq(broker.exchangeReserve(newExchangeProvider), address(reserve1)); } function test_addExchangeProvider_whenAlreadyAdded_shouldRevert() public { vm.expectRevert("ExchangeProvider already exists in the list"); - broker.addExchangeProvider(address(exchangeProvider)); + vm.prank(deployer); + broker.addExchangeProvider(address(exchangeProvider), address(reserve1)); } function test_removeExchangeProvider_whenSenderIsOwner_shouldUpdateAndEmit() public { vm.expectEmit(true, true, true, true); emit ExchangeProviderRemoved(exchangeProvider1); + vm.prank(deployer); broker.removeExchangeProvider(exchangeProvider1, 0); assert(broker.getExchangeProviders()[0] != exchangeProvider1); + assertEq(broker.exchangeReserve(exchangeProvider1), address(0)); } function test_removeExchangeProvider_whenAddressDoesNotExist_shouldRevert() public { vm.expectRevert("index doesn't match provider"); + vm.prank(deployer); broker.removeExchangeProvider(notDeployer, 1); } function test_removeExchangeProvider_whenIndexOutOfRange_shouldRevert() public { vm.expectRevert("index doesn't match provider"); + vm.prank(deployer); broker.removeExchangeProvider(exchangeProvider1, 1); } @@ -136,24 +164,48 @@ contract BrokerTest_initilizerAndSetters is BrokerTest { broker.removeExchangeProvider(exchangeProvider1, 0); } - function test_setReserve_whenSenderIsNotOwner_shouldRevert() public { + function test_setReserves_whenSenderIsNotOwner_shouldRevert() public { vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); - broker.setReserve(address(0)); + broker.setReserves(new address[](0), new address[](0)); + } + + function test_setReserves_whenExchangeProviderIsNotAdded_shouldRevert() public { + address[] memory exchangeProviders = new address[](1); + exchangeProviders[0] = makeAddr("newExchangeProvider"); + address[] memory reserves = new address[](1); + reserves[0] = makeAddr("newReserve"); + changePrank(deployer); + vm.expectRevert("ExchangeProvider does not exist"); + broker.setReserves(exchangeProviders, reserves); } - function test_setReserve_whenAddressIsZero_shouldRevert() public { - vm.expectRevert("Reserve address must be set"); - broker.setReserve(address(0)); + function test_setReserves_whenReserveAddressIsZero_shouldRevert() public { + address[] memory exchangeProviders = new address[](1); + exchangeProviders[0] = exchangeProvider1; + address[] memory reserves = new address[](1); + reserves[0] = address(0); + changePrank(deployer); + vm.expectRevert("Reserve address can't be 0"); + broker.setReserves(exchangeProviders, reserves); } - function test_setReserve_whenSenderIsOwner_shouldUpdateAndEmit() public { - address newReserve = makeAddr("newReserve"); - vm.expectEmit(true, false, false, false); - emit ReserveSet(newReserve, address(reserve)); + function test_setReserves_whenSenderIsOwner_shouldUpdateAndEmit() public { + address[] memory exchangeProviders = new address[](2); + exchangeProviders[0] = exchangeProvider1; + exchangeProviders[1] = exchangeProvider2; - broker.setReserve(newReserve); - assertEq(address(broker.reserve()), newReserve); + address[] memory reserves = new address[](2); + reserves[0] = makeAddr("newReserve"); + reserves[1] = makeAddr("newReserve2"); + changePrank(deployer); + vm.expectEmit(true, true, true, true); + emit ReserveSet(exchangeProvider1, reserves[0]); + vm.expectEmit(true, true, true, true); + emit ReserveSet(exchangeProvider2, reserves[1]); + broker.setReserves(exchangeProviders, reserves); + assertEq(address(broker.exchangeReserve(address(exchangeProvider1))), reserves[0]); + assertEq(address(broker.exchangeReserve(address(exchangeProvider2))), reserves[1]); } } @@ -173,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_whenExchangeProviderIsSet_shouldReceiveCall() public view { - uint256 amountIn = broker.getAmountIn( + 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 { + 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( @@ -205,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); } } @@ -219,12 +379,6 @@ contract BrokerTest_getAmounts is BrokerTest { contract BrokerTest_BurnStableTokens is BrokerTest { uint256 burnAmount = 1; - function test_burnStableTokens_whenTokenIsNotReserveStable_shouldRevert() public { - vm.prank(notDeployer); - vm.expectRevert("Token must be a reserve stable asset"); - broker.burnStableTokens(randomAsset, 2); - } - function test_burnStableTokens_whenTokenIsAReserveStable_shouldBurnAndEmit() public { stableAsset.mint(notDeployer, 2); vm.prank(notDeployer); @@ -488,6 +642,7 @@ contract BrokerTest_swap is BrokerTest { vm.expectEmit(true, true, true, true); emit TradingLimitConfigured(exchangeId, address(stableAsset), config); + vm.prank(deployer); broker.configureTradingLimit(exchangeId, address(stableAsset), config); vm.prank(trader); @@ -504,6 +659,7 @@ contract BrokerTest_swap is BrokerTest { vm.expectEmit(true, true, true, true); emit TradingLimitConfigured(exchangeId, address(stableAsset), config); + vm.prank(deployer); broker.configureTradingLimit(exchangeId, address(stableAsset), config); vm.expectRevert(bytes("L0 Exceeded")); diff --git a/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol b/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol new file mode 100644 index 00000000..ad6f080f --- /dev/null +++ b/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; +// solhint-disable func-name-mixedcase + +import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarExpansionController.sol"; + +contract GoodDollarExpansionControllerHarness is GoodDollarExpansionController { + constructor(bool disabled) GoodDollarExpansionController(disabled) {} + + function exposed_getReserveRatioScalar(ExchangeExpansionConfig calldata config) external returns (uint256) { + return _getReserveRatioScalar(config); + } +} diff --git a/test/utils/harnesses/TradingLimitsHarness.sol b/test/utils/harnesses/TradingLimitsHarness.sol index 723ac7ae..047a0f22 100644 --- a/test/utils/harnesses/TradingLimitsHarness.sol +++ b/test/utils/harnesses/TradingLimitsHarness.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity 0.8.18; pragma experimental ABIEncoderV2; import { TradingLimits } from "contracts/libraries/TradingLimits.sol"; @@ -10,18 +10,18 @@ contract TradingLimitsHarness is ITradingLimitsHarness { using TradingLimits for ITradingLimits.State; using TradingLimits for ITradingLimits.Config; - function validate(ITradingLimits.Config memory config) public view { + function validate(ITradingLimits.Config memory config) public pure { return config.validate(); } - function verify(ITradingLimits.State memory state, ITradingLimits.Config memory config) public view { + function verify(ITradingLimits.State memory state, ITradingLimits.Config memory config) public pure { return state.verify(config); } function reset( ITradingLimits.State memory state, ITradingLimits.Config memory config - ) public view returns (ITradingLimits.State memory) { + ) public pure returns (ITradingLimits.State memory) { return state.reset(config); }