diff --git a/.github/actions/setup-repo/action.yml b/.github/actions/setup-repo/action.yml new file mode 100644 index 00000000..2ca622bd --- /dev/null +++ b/.github/actions/setup-repo/action.yml @@ -0,0 +1,35 @@ +name: Setup repo +description: Runs all steps to setup the repo (install node_modules, build, etc...) +inputs: + registry-token: + description: 'PAT to access registries' +runs: + using: 'composite' + steps: + - name: Get yarn cache directory path + id: yarn-cache-dir-path + shell: bash + run: | + echo "::set-output name=dir::$(yarn cache dir)" + echo "::set-output name=version::$(yarn -v)" + + - uses: actions/setup-node@v3 + with: + node-version: '20' + + - uses: actions/cache@v2 + id: yarn-cache + with: + path: | + **/node_modules + ${{ steps.yarn-cache-dir-path.outputs.dir }} + + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install dependencies + shell: bash + run: echo "//npm.pkg.github.com/:_authToken=$GH_REGISTRY_ACCESS_TOKEN" >> .npmrc && yarn install --frozen-lockfile --verbose && rm -f .npmrc + env: + GH_REGISTRY_ACCESS_TOKEN: ${{ inputs.registry-token }} diff --git a/.github/workflows/ci-deep.yml b/.github/workflows/ci-deep.yml index cba986cf..eb02bb1b 100644 --- a/.github/workflows/ci-deep.yml +++ b/.github/workflows/ci-deep.yml @@ -32,6 +32,11 @@ jobs: node-version: 18 cache: "yarn" + - name: Setup repo + uses: ./.github/actions/setup-repo + with: + registry-token: ${{ secrets.GH_REGISTRY_ACCESS_TOKEN }} + - name: Install dependencies run: yarn install @@ -55,8 +60,13 @@ jobs: with: version: nightly - - name: Install node_modules - run: yarn install + - name: Setup repo + uses: ./.github/actions/setup-repo + with: + registry-token: ${{ secrets.GH_REGISTRY_ACCESS_TOKEN }} + + - name: Install dependencies + run: yarn install --frozen-lockfile - name: Compile foundry run: yarn compile --sizes @@ -99,6 +109,11 @@ jobs: - name: Run Foundry tests run: yarn test:unit + env: + ETH_NODE_URI_POLYGON: ${{ secrets.ETH_NODE_URI_POLYGON }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_OPTIMISM: ${{ secrets.ETH_NODE_URI_OPTIMISM }} + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} test-invariant: needs: ["build", "lint"] @@ -127,6 +142,10 @@ jobs: env: FOUNDRY_INVARIANT_RUNS: ${{ github.event.inputs.invariantRuns || '300' }} FOUNDRY_INVARIANT_DEPTH: ${{ github.event.inputs.invariantDepth || '50' }} + ETH_NODE_URI_POLYGON: ${{ secrets.ETH_NODE_URI_POLYGON }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_OPTIMISM: ${{ secrets.ETH_NODE_URI_OPTIMISM }} + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} test-fuzz: needs: ["build", "lint"] @@ -153,4 +172,8 @@ jobs: - name: Run Foundry tests run: yarn test:fuzz env: - FOUNDRY_FUZZ_RUNS: ${{ github.event.inputs.fuzzRuns || '10000' }} \ No newline at end of file + FOUNDRY_FUZZ_RUNS: ${{ github.event.inputs.fuzzRuns || '10000' }} + ETH_NODE_URI_POLYGON: ${{ secrets.ETH_NODE_URI_POLYGON }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_OPTIMISM: ${{ secrets.ETH_NODE_URI_OPTIMISM }} + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27a56104..5a0eeff2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,8 +21,10 @@ jobs: node-version: 18 cache: "yarn" - - name: Install dependencies - run: yarn install + - name: Setup repo + uses: ./.github/actions/setup-repo + with: + registry-token: ${{ secrets.GH_REGISTRY_ACCESS_TOKEN }} - name: Run solhint run: yarn lint:check @@ -44,8 +46,10 @@ jobs: with: version: nightly - - name: Install node_modules - run: yarn install + - name: Setup repo + uses: ./.github/actions/setup-repo + with: + registry-token: ${{ secrets.GH_REGISTRY_ACCESS_TOKEN }} - name: Compile foundry run: yarn compile --sizes @@ -88,6 +92,11 @@ jobs: - name: Run Foundry tests run: yarn test:unit + env: + ETH_NODE_URI_POLYGON: ${{ secrets.ETH_NODE_URI_POLYGON }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_OPTIMISM: ${{ secrets.ETH_NODE_URI_OPTIMISM }} + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} test-invariant: needs: ["build", "lint"] @@ -116,6 +125,10 @@ jobs: env: FOUNDRY_INVARIANT_RUNS: "8" FOUNDRY_INVARIANT_DEPTH: "256" + ETH_NODE_URI_POLYGON: ${{ secrets.ETH_NODE_URI_POLYGON }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_OPTIMISM: ${{ secrets.ETH_NODE_URI_OPTIMISM }} + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} test-fuzz: needs: ["build", "lint"] @@ -143,6 +156,10 @@ jobs: run: yarn test:fuzz env: FOUNDRY_FUZZ_RUNS: "5000" + ETH_NODE_URI_POLYGON: ${{ secrets.ETH_NODE_URI_POLYGON }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_OPTIMISM: ${{ secrets.ETH_NODE_URI_OPTIMISM }} + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} coverage: needs: ["build", "lint"] @@ -169,6 +186,11 @@ jobs: - name: "Generate the coverage report using the unit and the integration tests" run: "yarn ci:coverage" + env: + ETH_NODE_URI_POLYGON: ${{ secrets.ETH_NODE_URI_POLYGON }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_OPTIMISM: ${{ secrets.ETH_NODE_URI_OPTIMISM }} + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} - name: "Upload coverage report to Codecov" uses: "codecov/codecov-action@v3" @@ -217,4 +239,4 @@ jobs: - name: "Add Slither summary" run: | echo "## Slither result" >> $GITHUB_STEP_SUMMARY - echo "✅ Uploaded to GitHub code scanning" >> $GITHUB_STEP_SUMMARY \ No newline at end of file + echo "✅ Uploaded to GitHub code scanning" >> $GITHUB_STEP_SUMMARY diff --git a/.gitmodules b/.gitmodules index 4a60f6ed..6809070f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,9 +17,11 @@ [submodule "lib/openzeppelin-contracts-upgradeable"] path = lib/openzeppelin-contracts-upgradeable url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable + version = v4.7.3 [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts + version = v4.7.3 [submodule "lib/utils"] path = lib/utils url = https://github.com/AngleProtocol/utils diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..af66bba8 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@angleprotocol:registry=https://npm.pkg.github.com diff --git a/.solhint.json b/.solhint.json index 99c32602..e1346902 100644 --- a/.solhint.json +++ b/.solhint.json @@ -1,8 +1,13 @@ { "extends": "solhint:recommended", - "plugins": ["prettier"], + "plugins": [ + "prettier" + ], "rules": { - "max-line-length": ["error", 120], + "max-line-length": [ + "error", + 120 + ], "avoid-call-value": "warn", "avoid-low-level-calls": "off", "avoid-tx-origin": "warn", @@ -22,7 +27,15 @@ "no-inline-assembly": "off", "no-complex-fallback": "off", "reason-string": "off", - "func-visibility": ["warn", { "ignoreConstructors": true }], - "explicit-types": ["error","explicit"] + "func-visibility": [ + "warn", + { + "ignoreConstructors": true + } + ], + "explicit-types": [ + "error", + "explicit" + ] } -} +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 3252191e..f40cc54d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,19 +1,33 @@ { - "solidity.formatter": "prettier", "[typescript]": { "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" }, "[solidity]": { "editor.defaultFormatter": "JuanBlanco.solidity" }, - "editor.codeActionsOnSave": { - "source.fixAll": true - }, + "editor.formatOnSave": true, "workbench.colorCustomizations": { "diffEditor.insertedTextBackground": "#00bb0044", "diffEditor.removedTextBackground": "#ff000044" }, "slither.solcPath": "", "slither.hiddenDetectors": [], - "solidity.compileUsingRemoteVersion": "v0.8.19+commit.7dd6d404" + "solidity.compileUsingRemoteVersion": "v0.8.22", + "files.insertFinalNewline": true, + "solidity.remappings": [ + "ds-test/=lib/forge-std/lib/ds-test/src/", + "forge-std/=lib/forge-std/src/", + "stringutils/=lib/solidity-stringutils", + "contracts/=contracts/", + "test/=test/", + "interfaces/=contracts/interfaces/", + "oz/=lib/openzeppelin-contracts/contracts/", + "oz-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", + "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", + "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", + "mock/=test/mock/", + "prb/math/=lib/prb-math/src/", + "borrow/=lib/borrow-contracts/contracts", + "utils/=lib/utils" + ] } diff --git a/contracts/helpers/Rebalancer.sol b/contracts/helpers/Rebalancer.sol index 974674a0..42e6a660 100644 --- a/contracts/helpers/Rebalancer.sol +++ b/contracts/helpers/Rebalancer.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.19; import { IERC20 } from "oz/interfaces/IERC20.sol"; -import { IERC20Metadata } from "oz/interfaces/IERC20Metadata.sol"; import { SafeERC20 } from "oz/token/ERC20/utils/SafeERC20.sol"; import { SafeCast } from "oz/utils/math/SafeCast.sol"; diff --git a/contracts/interfaces/IGetters.sol b/contracts/interfaces/IGetters.sol index 7c186dd5..858612da 100644 --- a/contracts/interfaces/IGetters.sol +++ b/contracts/interfaces/IGetters.sol @@ -89,7 +89,8 @@ interface IGetters { OracleReadType oracleType, OracleReadType targetType, bytes memory oracleData, - bytes memory targetData + bytes memory targetData, + bytes memory hyperparameters ); /// @notice Returns if the associated functionality is paused or not diff --git a/contracts/interfaces/ISetters.sol b/contracts/interfaces/ISetters.sol index fe208db8..953f6355 100644 --- a/contracts/interfaces/ISetters.sol +++ b/contracts/interfaces/ISetters.sol @@ -40,6 +40,9 @@ interface ISettersGovernor { /// @notice Sets the `oracleConfig` used to read the value of `collateral` for the mint, burn and redemption /// operations function setOracle(address collateral, bytes memory oracleConfig) external; + + /// @notice Update oracle data for a given `collateral` + function updateOracle(address collateral) external; } /// @title ISettersGovernor diff --git a/contracts/interfaces/ITransmuterOracle.sol b/contracts/interfaces/ITransmuterOracle.sol index d7ed32b0..2b53b655 100644 --- a/contracts/interfaces/ITransmuterOracle.sol +++ b/contracts/interfaces/ITransmuterOracle.sol @@ -15,4 +15,7 @@ interface ITransmuterOracle { /// @notice Reads the oracle value for asset to use in a burn transaction as well as the ratio /// between the current price and the target price for the asset function readBurn() external view returns (uint256 oracleValue, uint256 ratio); + + /// @notice Reads the oracle value for asset + function read() external view returns (uint256); } diff --git a/contracts/interfaces/external/morpho/IMorphoOracle.sol b/contracts/interfaces/external/morpho/IMorphoOracle.sol new file mode 100644 index 00000000..e0cfdc19 --- /dev/null +++ b/contracts/interfaces/external/morpho/IMorphoOracle.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.5.0; + +/// @title IMorphoOracle +/// @notice Interface for the oracle contracts used within Morpho +interface IMorphoOracle { + function price() external view returns (uint256); +} diff --git a/contracts/transmuter/Storage.sol b/contracts/transmuter/Storage.sol index 4eaa8b64..1854eafe 100644 --- a/contracts/transmuter/Storage.sol +++ b/contracts/transmuter/Storage.sol @@ -47,7 +47,9 @@ enum OracleReadType { CBETH, RETH, SFRXETH, - PYTH + PYTH, + MAX, + MORPHO_ORACLE } enum OracleQuoteType { @@ -64,70 +66,70 @@ enum WhitelistType { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ struct Permit2Details { - address to; // Address that will receive the funds - uint256 nonce; // Nonce of the transaction - bytes signature; // Permit signature of the user + address to; // Address that will receive the funds + uint256 nonce; // Nonce of the transaction + bytes signature; // Permit signature of the user } struct FacetCut { - address facetAddress; // Facet contract address - FacetCutAction action; // Can be add, remove or replace - bytes4[] functionSelectors; // Ex. bytes4(keccak256("transfer(address,uint256)")) + address facetAddress; // Facet contract address + FacetCutAction action; // Can be add, remove or replace + bytes4[] functionSelectors; // Ex. bytes4(keccak256("transfer(address,uint256)")) } struct Facet { - address facetAddress; // Facet contract address - bytes4[] functionSelectors; // Ex. bytes4(keccak256("transfer(address,uint256)")) + address facetAddress; // Facet contract address + bytes4[] functionSelectors; // Ex. bytes4(keccak256("transfer(address,uint256)")) } struct FacetInfo { - address facetAddress; // Facet contract address - uint16 selectorPosition; // Position in the list of all selectors + address facetAddress; // Facet contract address + uint16 selectorPosition; // Position in the list of all selectors } struct DiamondStorage { - bytes4[] selectors; // List of all available selectors - mapping(bytes4 => FacetInfo) selectorInfo; // Selector to (address, position in list) - IAccessControlManager accessControlManager; // Contract handling access control + bytes4[] selectors; // List of all available selectors + mapping(bytes4 => FacetInfo) selectorInfo; // Selector to (address, position in list) + IAccessControlManager accessControlManager; // Contract handling access control } struct ImplementationStorage { - address implementation; // Dummy implementation address for Etherscan usability + address implementation; // Dummy implementation address for Etherscan usability } struct ManagerStorage { - IERC20[] subCollaterals; // Subtokens handled by the manager or strategies - bytes config; // Additional configuration data + IERC20[] subCollaterals; // Subtokens handled by the manager or strategies + bytes config; // Additional configuration data } struct Collateral { - uint8 isManaged; // If the collateral is managed through external strategies - uint8 isMintLive; // If minting from this asset is unpaused - uint8 isBurnLive; // If burning to this asset is unpaused - uint8 decimals; // IERC20Metadata(collateral).decimals() - uint8 onlyWhitelisted; // If only whitelisted addresses can burn or redeem for this token - uint216 normalizedStables; // Normalized amount of stablecoins issued from this collateral - uint64[] xFeeMint; // Increasing exposures in [0,BASE_9[ - int64[] yFeeMint; // Mint fees at the exposures specified in `xFeeMint` - uint64[] xFeeBurn; // Decreasing exposures in ]0,BASE_9] - int64[] yFeeBurn; // Burn fees at the exposures specified in `xFeeBurn` - bytes oracleConfig; // Data about the oracle used for the collateral - bytes whitelistData; // For whitelisted collateral, data used to verify whitelists - ManagerStorage managerData; // For managed collateral, data used to handle the strategies + uint8 isManaged; // If the collateral is managed through external strategies + uint8 isMintLive; // If minting from this asset is unpaused + uint8 isBurnLive; // If burning to this asset is unpaused + uint8 decimals; // IERC20Metadata(collateral).decimals() + uint8 onlyWhitelisted; // If only whitelisted addresses can burn or redeem for this token + uint216 normalizedStables; // Normalized amount of stablecoins issued from this collateral + uint64[] xFeeMint; // Increasing exposures in [0,BASE_9[ + int64[] yFeeMint; // Mint fees at the exposures specified in `xFeeMint` + uint64[] xFeeBurn; // Decreasing exposures in ]0,BASE_9] + int64[] yFeeBurn; // Burn fees at the exposures specified in `xFeeBurn` + bytes oracleConfig; // Data about the oracle used for the collateral + bytes whitelistData; // For whitelisted collateral, data used to verify whitelists + ManagerStorage managerData; // For managed collateral, data used to handle the strategies } struct TransmuterStorage { - IAgToken agToken; // agToken handled by the system - uint8 isRedemptionLive; // If redemption is unpaused - uint8 statusReentrant; // If call is reentrant or not - uint128 normalizedStables; // Normalized amount of stablecoins issued by the system - uint128 normalizer; // To reconcile `normalizedStables` values with the actual amount - address[] collateralList; // List of collateral assets supported by the system - uint64[] xRedemptionCurve; // Increasing collateral ratios > 0 - int64[] yRedemptionCurve; // Value of the redemption fees at `xRedemptionCurve` - mapping(address => Collateral) collaterals; // Maps a collateral asset to its parameters - mapping(address => uint256) isTrusted; // If an address is trusted to update the normalizer value - mapping(address => uint256) isSellerTrusted; // If an address is trusted to sell accruing reward tokens + IAgToken agToken; // agToken handled by the system + uint8 isRedemptionLive; // If redemption is unpaused + uint8 statusReentrant; // If call is reentrant or not + uint128 normalizedStables; // Normalized amount of stablecoins issued by the system + uint128 normalizer; // To reconcile `normalizedStables` values with the actual amount + address[] collateralList; // List of collateral assets supported by the system + uint64[] xRedemptionCurve; // Increasing collateral ratios > 0 + int64[] yRedemptionCurve; // Value of the redemption fees at `xRedemptionCurve` + mapping(address => Collateral) collaterals; // Maps a collateral asset to its parameters + mapping(address => uint256) isTrusted; // If an address is trusted to update the normalizer value + mapping(address => uint256) isSellerTrusted; // If an address is trusted to sell accruing reward tokens or to run keeper jobs on oracles mapping(WhitelistType => mapping(address => uint256)) isWhitelistedForType; - // Whether an address is whitelisted for a specific whitelist type + // Whether an address is whitelisted for a specific whitelist type } diff --git a/contracts/transmuter/configs/FakeGnosis.sol b/contracts/transmuter/configs/FakeGnosis.sol index 9589d973..c392f3bd 100644 --- a/contracts/transmuter/configs/FakeGnosis.sol +++ b/contracts/transmuter/configs/FakeGnosis.sol @@ -85,7 +85,8 @@ contract FakeGnosis { Storage.OracleReadType.CHAINLINK_FEEDS, Storage.OracleReadType.STABLE, readData, - targetData + targetData, + abi.encode(uint128(0), uint128(50 * BPS)) ); collaterals[i] = CollateralSetupProd( _collateralAddresses[i], diff --git a/contracts/transmuter/configs/Production.sol b/contracts/transmuter/configs/Production.sol index a20c9079..72cd9e55 100644 --- a/contracts/transmuter/configs/Production.sol +++ b/contracts/transmuter/configs/Production.sol @@ -6,15 +6,18 @@ import "./ProductionTypes.sol"; /// @dev This contract is used only once to initialize the diamond proxy. contract Production { + error WrongSetup(); + + address constant EUROC = 0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c; + address constant BC3M = 0x2F123cF3F37CE3328CC9B5b8415f9EC5109b45e7; + function initialize( IAccessControlManager _accessControlManager, address _agToken, address dummyImplementation ) external { - address euroc = 0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c; - address bc3m = 0x2F123cF3F37CE3328CC9B5b8415f9EC5109b45e7; - require(address(_accessControlManager) == 0x5bc6BEf80DA563EBf6Df6D6913513fa9A7ec89BE); - require(address(_agToken) == 0x1a7e4e63778B4f12a199C062f3eFdD288afCBce8); + if (address(_accessControlManager) != 0x5bc6BEf80DA563EBf6Df6D6913513fa9A7ec89BE) revert NotTrusted(); + if (address(_agToken) != 0x1a7e4e63778B4f12a199C062f3eFdD288afCBce8) revert NotTrusted(); // Check this docs for simulations: // https://docs.google.com/spreadsheets/d/1UxS1m4sG8j2Lv02wONYJNkF4S7NDLv-5iyAzFAFTfXw/edit#gid=0 @@ -47,31 +50,35 @@ contract Production { bytes memory oracleConfig; { // Pyth oracle for EUROC - bytes32[] memory feedIds = new bytes32[](2); - uint32[] memory stalePeriods = new uint32[](2); - uint8[] memory isMultiplied = new uint8[](2); - // pyth address - address pyth = 0x4305FB66699C3B2702D4d05CF36551390A4c69C6; - // EUROC/USD - feedIds[0] = 0x76fa85158bf14ede77087fe3ae472f66213f6ea2f5b411cb2de472794990fa5c; - // USD/EUR - feedIds[1] = 0xa995d00bb36a63cef7fd2c287dc105fc8f3d93779f062f09551b0af3e81ec30b; - stalePeriods[0] = 14 days; - stalePeriods[1] = 14 days; - isMultiplied[0] = 1; - isMultiplied[1] = 0; - OracleQuoteType quoteType = OracleQuoteType.UNIT; - bytes memory readData = abi.encode(pyth, feedIds, stalePeriods, isMultiplied, quoteType); + bytes memory readData; + { + bytes32[] memory feedIds = new bytes32[](2); + uint32[] memory stalePeriods = new uint32[](2); + uint8[] memory isMultiplied = new uint8[](2); + // pyth address + address pyth = 0x4305FB66699C3B2702D4d05CF36551390A4c69C6; + // EUROC/USD + feedIds[0] = 0x76fa85158bf14ede77087fe3ae472f66213f6ea2f5b411cb2de472794990fa5c; + // USD/EUR + feedIds[1] = 0xa995d00bb36a63cef7fd2c287dc105fc8f3d93779f062f09551b0af3e81ec30b; + stalePeriods[0] = 14 days; + stalePeriods[1] = 14 days; + isMultiplied[0] = 1; + isMultiplied[1] = 0; + OracleQuoteType quoteType = OracleQuoteType.UNIT; + readData = abi.encode(pyth, feedIds, stalePeriods, isMultiplied, quoteType); + } bytes memory targetData; oracleConfig = abi.encode( Storage.OracleReadType.PYTH, Storage.OracleReadType.STABLE, readData, - targetData + targetData, + abi.encode(uint128(5 * BPS), uint128(0)) ); } collaterals[0] = CollateralSetupProd( - euroc, + EUROC, oracleConfig, xMintFeeEuroc, yMintFeeEuroc, @@ -102,45 +109,53 @@ contract Production { yBurnFeeC3M[1] = int64(uint64((5 * BASE_9) / 1000)); yBurnFeeC3M[2] = int64(uint64(MAX_BURN_FEE)); - AggregatorV3Interface[] memory circuitChainlink = new AggregatorV3Interface[](1); - uint32[] memory stalePeriods = new uint32[](1); - uint8[] memory circuitChainIsMultiplied = new uint8[](1); - uint8[] memory chainlinkDecimals = new uint8[](1); - - // bC3M: Redstone as a current price (more accurate for redemptions), and Backed as a target - - // Redstone C3M Oracle - circuitChainlink[0] = AggregatorV3Interface(0x6E27A25999B3C665E44D903B2139F5a4Be2B6C26); - stalePeriods[0] = 3 days; - circuitChainIsMultiplied[0] = 1; - chainlinkDecimals[0] = 8; - OracleQuoteType quoteType = OracleQuoteType.UNIT; - bytes memory readData = abi.encode( - circuitChainlink, - stalePeriods, - circuitChainIsMultiplied, - chainlinkDecimals, - quoteType - ); + bytes memory readData; + { + AggregatorV3Interface[] memory circuitChainlink = new AggregatorV3Interface[](1); + uint32[] memory stalePeriods = new uint32[](1); + uint8[] memory circuitChainIsMultiplied = new uint8[](1); + uint8[] memory chainlinkDecimals = new uint8[](1); + + // bC3M: Redstone as a current price (more accurate for redemptions), and Backed as a target + + // Redstone C3M Oracle + circuitChainlink[0] = AggregatorV3Interface(0x6E27A25999B3C665E44D903B2139F5a4Be2B6C26); + stalePeriods[0] = 3 days; + circuitChainIsMultiplied[0] = 1; + chainlinkDecimals[0] = 8; + OracleQuoteType quoteType = OracleQuoteType.UNIT; + readData = abi.encode( + circuitChainlink, + stalePeriods, + circuitChainIsMultiplied, + chainlinkDecimals, + quoteType + ); + } // Backed C3M Oracle - circuitChainlink[0] = AggregatorV3Interface(0x83Ec02059F686E747392A22ddfED7833bA0d7cE3); - bytes memory targetData = abi.encode( - circuitChainlink, - stalePeriods, - circuitChainIsMultiplied, - chainlinkDecimals, - quoteType - ); + bytes memory targetData; + { + uint256 initialValue; + + { + (, int256 ratio, , , ) = AggregatorV3Interface(0x83Ec02059F686E747392A22ddfED7833bA0d7cE3) + .latestRoundData(); + if (ratio <= 0) revert WrongSetup(); + initialValue = (BASE_18 * uint256(ratio)) / 1e8; + } + targetData = abi.encode(initialValue); + } bytes memory oracleConfig = abi.encode( Storage.OracleReadType.CHAINLINK_FEEDS, - Storage.OracleReadType.CHAINLINK_FEEDS, + Storage.OracleReadType.MAX, readData, - targetData + targetData, + abi.encode(uint128(0), uint128(100 * BPS)) ); collaterals[1] = CollateralSetupProd( - bc3m, + BC3M, oracleConfig, xMintFeeC3M, yMintFeeC3M, @@ -176,14 +191,14 @@ contract Production { // Keyring whitelist check abi.encode(address(0x4954c61984180868495D1a7Fb193b05a2cbd9dE3)) ); - LibSetters.setWhitelistStatus(bc3m, 1, whitelistData); + LibSetters.setWhitelistStatus(BC3M, 1, whitelistData); // adjustStablecoins - LibSetters.adjustStablecoins(euroc, 8851136430000000000000000, true); - LibSetters.adjustStablecoins(bc3m, 4192643570000000000000000, true); + LibSetters.adjustStablecoins(EUROC, 8851136430000000000000000, true); + LibSetters.adjustStablecoins(BC3M, 4192643570000000000000000, true); // setRedemptionCurveParams - LibSetters.togglePause(euroc, ActionType.Redeem); + LibSetters.togglePause(EUROC, ActionType.Redeem); uint64[] memory xRedeemFee = new uint64[](4); xRedeemFee[0] = uint64((75 * BASE_9) / 100); xRedeemFee[1] = uint64((85 * BASE_9) / 100); diff --git a/contracts/transmuter/configs/ProductionUSD.sol b/contracts/transmuter/configs/ProductionUSD.sol index f8af678d..cb1080ff 100644 --- a/contracts/transmuter/configs/ProductionUSD.sol +++ b/contracts/transmuter/configs/ProductionUSD.sol @@ -6,12 +6,13 @@ import "./ProductionTypes.sol"; /// @dev This contract is used only once to initialize the diamond proxy. contract ProductionUSD { + address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + function initialize( IAccessControlManager _accessControlManager, address _agToken, address dummyImplementation ) external { - address usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; /* require(address(_accessControlManager) == 0x5bc6BEf80DA563EBf6Df6D6913513fa9A7ec89BE); require(address(_agToken) == 0x0000206329b97DB379d5E1Bf586BbDB969C63274); @@ -35,34 +36,38 @@ contract ProductionUSD { bytes memory oracleConfig; { - AggregatorV3Interface[] memory circuitChainlink = new AggregatorV3Interface[](1); - uint32[] memory stalePeriods = new uint32[](1); - uint8[] memory circuitChainIsMultiplied = new uint8[](1); - uint8[] memory chainlinkDecimals = new uint8[](1); - - // Chainlink USDC/USD oracle - circuitChainlink[0] = AggregatorV3Interface(0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6); - stalePeriods[0] = 1 days; - circuitChainIsMultiplied[0] = 1; - chainlinkDecimals[0] = 8; - OracleQuoteType quoteType = OracleQuoteType.UNIT; - bytes memory readData = abi.encode( - circuitChainlink, - stalePeriods, - circuitChainIsMultiplied, - chainlinkDecimals, - quoteType - ); + bytes memory readData; + { + AggregatorV3Interface[] memory circuitChainlink = new AggregatorV3Interface[](1); + uint32[] memory stalePeriods = new uint32[](1); + uint8[] memory circuitChainIsMultiplied = new uint8[](1); + uint8[] memory chainlinkDecimals = new uint8[](1); + + // Chainlink USDC/USD oracle + circuitChainlink[0] = AggregatorV3Interface(0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6); + stalePeriods[0] = 1 days; + circuitChainIsMultiplied[0] = 1; + chainlinkDecimals[0] = 8; + OracleQuoteType quoteType = OracleQuoteType.UNIT; + readData = abi.encode( + circuitChainlink, + stalePeriods, + circuitChainIsMultiplied, + chainlinkDecimals, + quoteType + ); + } bytes memory targetData; oracleConfig = abi.encode( Storage.OracleReadType.CHAINLINK_FEEDS, Storage.OracleReadType.STABLE, readData, - targetData + targetData, + abi.encode(uint128(0), uint128(50 * BPS)) ); } collaterals[0] = CollateralSetupProd( - usdc, + USDC, oracleConfig, xMintFeeUsdc, yMintFeeUsdc, @@ -93,7 +98,7 @@ contract ProductionUSD { } // setRedemptionCurveParams - LibSetters.togglePause(usdc, ActionType.Redeem); + LibSetters.togglePause(USDC, ActionType.Redeem); uint64[] memory xRedeemFee = new uint64[](4); xRedeemFee[0] = uint64((75 * BASE_9) / 100); xRedeemFee[1] = uint64((85 * BASE_9) / 100); diff --git a/contracts/transmuter/configs/Test.sol b/contracts/transmuter/configs/Test.sol index 96b1a9d7..1ee5f11f 100644 --- a/contracts/transmuter/configs/Test.sol +++ b/contracts/transmuter/configs/Test.sol @@ -53,7 +53,13 @@ contract Test { bytes memory targetData; LibSetters.setOracle( eurA.collateral, - abi.encode(OracleReadType.CHAINLINK_FEEDS, OracleReadType.STABLE, readData, targetData) + abi.encode( + OracleReadType.CHAINLINK_FEEDS, + OracleReadType.STABLE, + readData, + targetData, + abi.encode(uint128(0), uint128(0)) + ) ); // Fees @@ -103,7 +109,13 @@ contract Test { readData = abi.encode(circuitChainlink, stalePeriods, circuitChainIsMultiplied, chainlinkDecimals, quoteType); LibSetters.setOracle( eurB.collateral, - abi.encode(OracleReadType.CHAINLINK_FEEDS, OracleReadType.STABLE, readData, targetData) + abi.encode( + OracleReadType.CHAINLINK_FEEDS, + OracleReadType.STABLE, + readData, + targetData, + abi.encode(uint128(0), uint128(0)) + ) ); // Fees @@ -153,7 +165,13 @@ contract Test { readData = abi.encode(circuitChainlink, stalePeriods, circuitChainIsMultiplied, chainlinkDecimals, quoteType); LibSetters.setOracle( eurY.collateral, - abi.encode(OracleReadType.CHAINLINK_FEEDS, OracleReadType.STABLE, readData, targetData) + abi.encode( + OracleReadType.CHAINLINK_FEEDS, + OracleReadType.STABLE, + readData, + targetData, + abi.encode(uint128(0), uint128(0)) + ) ); // Fees diff --git a/contracts/transmuter/facets/DiamondEtherscan.sol b/contracts/transmuter/facets/DiamondEtherscan.sol index 533ba1ec..94faa3e9 100644 --- a/contracts/transmuter/facets/DiamondEtherscan.sol +++ b/contracts/transmuter/facets/DiamondEtherscan.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.19; import { IDiamondEtherscan } from "interfaces/IDiamondEtherscan.sol"; -import { LibDiamond } from "../libraries/LibDiamond.sol"; import { LibDiamondEtherscan } from "../libraries/LibDiamondEtherscan.sol"; import { AccessControlModifiers } from "./AccessControlModifiers.sol"; diff --git a/contracts/transmuter/facets/Getters.sol b/contracts/transmuter/facets/Getters.sol index 2d3bd31d..cdacb445 100644 --- a/contracts/transmuter/facets/Getters.sol +++ b/contracts/transmuter/facets/Getters.sol @@ -132,7 +132,13 @@ contract Getters is IGetters { ) external view - returns (OracleReadType oracleType, OracleReadType targetType, bytes memory oracleData, bytes memory targetData) + returns ( + OracleReadType oracleType, + OracleReadType targetType, + bytes memory oracleData, + bytes memory targetData, + bytes memory hyperparameters + ) { return LibOracle.getOracle(collateral); } diff --git a/contracts/transmuter/facets/Redeemer.sol b/contracts/transmuter/facets/Redeemer.sol index d54fd635..792f121c 100644 --- a/contracts/transmuter/facets/Redeemer.sol +++ b/contracts/transmuter/facets/Redeemer.sol @@ -133,11 +133,14 @@ contract Redeemer is IRedeemer, AccessControlModifiers { // If a token is in the `forfeitTokens` list, then it is not sent as part of the redemption process if (amounts[i] > 0 && LibHelpers.checkList(tokens[i], forfeitTokens) < 0) { Collateral storage collatInfo = ts.collaterals[collateralListMem[indexCollateral]]; - if (collatInfo.onlyWhitelisted > 0 && !LibWhitelist.checkWhitelist(collatInfo.whitelistData, to)) + if (collatInfo.onlyWhitelisted > 0 && !LibWhitelist.checkWhitelist(collatInfo.whitelistData, to)) { revert NotWhitelisted(); - if (collatInfo.isManaged > 0) + } + if (collatInfo.isManaged > 0) { LibManager.release(tokens[i], to, amounts[i], collatInfo.managerData.config); - else IERC20(tokens[i]).safeTransfer(to, amounts[i]); + } else { + IERC20(tokens[i]).safeTransfer(to, amounts[i]); + } } if (subCollateralsTracker[indexCollateral] - 1 <= i) ++indexCollateral; } @@ -166,8 +169,7 @@ contract Redeemer is IRedeemer, AccessControlModifiers { penaltyFactor = uint64(LibHelpers.piecewiseLinear(collatRatio, xRedemptionCurveMem, yRedemptionCurveMem)); } - uint256 balancesLength = balances.length; - for (uint256 i; i < balancesLength; ++i) { + for (uint256 i; i < balances.length; ++i) { // The amount given for each token in reserves does not depend on the price of the tokens in reserve: // it is a proportion of the balance for each token computed as the ratio between the stablecoins // burnt relative to the amount of stablecoins issued. diff --git a/contracts/transmuter/facets/SettersGovernor.sol b/contracts/transmuter/facets/SettersGovernor.sol index 7c5f72ed..6cbdf971 100644 --- a/contracts/transmuter/facets/SettersGovernor.sol +++ b/contracts/transmuter/facets/SettersGovernor.sol @@ -9,6 +9,7 @@ import { IAccessControlManager } from "interfaces/IAccessControlManager.sol"; import { ISettersGovernor } from "interfaces/ISetters.sol"; import { LibManager } from "../libraries/LibManager.sol"; +import { LibOracle } from "../libraries/LibOracle.sol"; import { LibSetters } from "../libraries/LibSetters.sol"; import { LibStorage as s } from "../libraries/LibStorage.sol"; import { AccessControlModifiers } from "./AccessControlModifiers.sol"; @@ -90,6 +91,11 @@ contract SettersGovernor is AccessControlModifiers, ISettersGovernor { LibSetters.setOracle(collateral, oracleConfig); } + function updateOracle(address collateral) external { + if (s.transmuterStorage().isSellerTrusted[msg.sender] == 0) revert NotTrusted(); + LibOracle.updateOracle(collateral); + } + /// @inheritdoc ISettersGovernor function setWhitelistStatus( address collateral, diff --git a/contracts/transmuter/libraries/LibOracle.sol b/contracts/transmuter/libraries/LibOracle.sol index b61603a0..8eff7d35 100644 --- a/contracts/transmuter/libraries/LibOracle.sol +++ b/contracts/transmuter/libraries/LibOracle.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.19; import { ITransmuterOracle } from "interfaces/ITransmuterOracle.sol"; import { AggregatorV3Interface } from "interfaces/external/chainlink/AggregatorV3Interface.sol"; +import { IMorphoOracle } from "interfaces/external/morpho/IMorphoOracle.sol"; import { IPyth, PythStructs } from "interfaces/external/pyth/IPyth.sol"; import { LibStorage as s } from "./LibStorage.sol"; @@ -21,39 +22,46 @@ library LibOracle { /// @notice Reads the oracle value used during a redemption to compute collateral ratio for `oracleConfig` /// @dev This value is only sensitive to compute the collateral ratio and deduce a penalty factor - function readRedemption(bytes memory oracleConfig) internal view returns (uint256) { + function readRedemption(bytes memory oracleConfig) internal view returns (uint256 oracleValue) { ( OracleReadType oracleType, OracleReadType targetType, bytes memory oracleData, - bytes memory targetData + bytes memory targetData, + ) = _parseOracleConfig(oracleConfig); if (oracleType == OracleReadType.EXTERNAL) { ITransmuterOracle externalOracle = abi.decode(oracleData, (ITransmuterOracle)); return externalOracle.readRedemption(); - } else return read(oracleType, read(targetType, BASE_18, targetData), oracleData); + } else { + (oracleValue, ) = readSpotAndTarget(oracleType, targetType, oracleData, targetData, 0); + return oracleValue; + } } /// @notice Reads the oracle value used during mint operations for an asset with `oracleConfig` - /// @dev For assets which do not rely on external oracles, this value is the minimum between the asset oracle - /// value and its target price + /// @dev For assets which do not rely on external oracles, this value is the minimum between the processed oracle + /// value for the asset and its target price function readMint(bytes memory oracleConfig) internal view returns (uint256 oracleValue) { ( OracleReadType oracleType, OracleReadType targetType, bytes memory oracleData, - bytes memory targetData + bytes memory targetData, + bytes memory hyperparameters ) = _parseOracleConfig(oracleConfig); if (oracleType == OracleReadType.EXTERNAL) { ITransmuterOracle externalOracle = abi.decode(oracleData, (ITransmuterOracle)); return externalOracle.readMint(); } - uint256 _targetPrice = read(targetType, BASE_18, targetData); - oracleValue = read(oracleType, _targetPrice, oracleData); - if (_targetPrice < oracleValue) oracleValue = _targetPrice; + + (uint128 userDeviation, ) = abi.decode(hyperparameters, (uint128, uint128)); + uint256 targetPrice; + (oracleValue, targetPrice) = readSpotAndTarget(oracleType, targetType, oracleData, targetData, userDeviation); + if (targetPrice < oracleValue) oracleValue = targetPrice; } - /// @notice Reads the oracle value that will be used for a burn operation for an asset with `oracleConfig` + /// @notice Reads the oracle value used for a burn operation for an asset with `oracleConfig` /// @return oracleValue The actual oracle value obtained /// @return ratio If `oracle value < target price`, the ratio between the oracle value and the target /// price, otherwise `BASE_18` @@ -62,16 +70,24 @@ library LibOracle { OracleReadType oracleType, OracleReadType targetType, bytes memory oracleData, - bytes memory targetData + bytes memory targetData, + bytes memory hyperparameters ) = _parseOracleConfig(oracleConfig); if (oracleType == OracleReadType.EXTERNAL) { ITransmuterOracle externalOracle = abi.decode(oracleData, (ITransmuterOracle)); return externalOracle.readBurn(); } - uint256 _targetPrice = read(targetType, BASE_18, targetData); - oracleValue = read(oracleType, _targetPrice, oracleData); + (uint128 userDeviation, uint128 burnRatioDeviation) = abi.decode(hyperparameters, (uint128, uint128)); + uint256 targetPrice; + (oracleValue, targetPrice) = readSpotAndTarget(oracleType, targetType, oracleData, targetData, userDeviation); + // Firewall in case the oracle value reported is low compared to the target + // If the oracle value is slightly below its target, then no deviation is reported for the oracle and + // the price of burning the stablecoin for other assets is not impacted. Also, the oracle value of this asset + // is set to the target price, to not be open to direct arbitrage ratio = BASE_18; - if (oracleValue < _targetPrice) ratio = (oracleValue * BASE_18) / _targetPrice; + if (oracleValue * BASE_18 < targetPrice * (BASE_18 - burnRatioDeviation)) + ratio = (oracleValue * BASE_18) / targetPrice; + else if (oracleValue < targetPrice) oracleValue = targetPrice; } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -81,7 +97,7 @@ library LibOracle { /// @notice Internal version of the `getOracle` function function getOracle( address collateral - ) internal view returns (OracleReadType, OracleReadType, bytes memory, bytes memory) { + ) internal view returns (OracleReadType, OracleReadType, bytes memory, bytes memory, bytes memory) { return _parseOracleConfig(s.transmuterStorage().collaterals[collateral].oracleConfig); } @@ -99,7 +115,9 @@ library LibOracle { uint256 ratioObserved = BASE_18; if (collateralList[i] != collateral) { (, ratioObserved) = readBurn(ts.collaterals[collateralList[i]].oracleConfig); - } else (oracleValue, ratioObserved) = readBurn(oracleConfig); + } else { + (oracleValue, ratioObserved) = readBurn(oracleConfig); + } if (ratioObserved < minRatio) minRatio = ratioObserved; } } @@ -113,6 +131,24 @@ library LibOracle { else return baseValue; } + function readSpotAndTarget( + OracleReadType oracleType, + OracleReadType targetType, + bytes memory oracleData, + bytes memory targetData, + uint256 deviation + ) internal view returns (uint256 oracleValue, uint256 targetPrice) { + targetPrice = read(targetType, BASE_18, targetData); + oracleValue = read(oracleType, targetPrice, oracleData); + // System may tolerate small deviations from target + // If the oracle value reported is reasonably close to the target + // --> disregard the oracle value and return the target price + if ( + targetPrice * (BASE_18 - deviation) < oracleValue * BASE_18 && + oracleValue * BASE_18 < targetPrice * (BASE_18 + deviation) + ) oracleValue = targetPrice; + } + /// @notice Reads an oracle value (or a target oracle value) for an asset based on its data parsed `oracleConfig` function read(OracleReadType readType, uint256 baseValue, bytes memory data) internal view returns (uint256) { if (readType == OracleReadType.CHAINLINK_FEEDS) { @@ -135,13 +171,19 @@ library LibOracle { ); } return quotePrice; - } else if (readType == OracleReadType.STABLE) return BASE_18; - else if (readType == OracleReadType.NO_ORACLE) return baseValue; - else if (readType == OracleReadType.WSTETH) return STETH.getPooledEthByShares(1 ether); - else if (readType == OracleReadType.CBETH) return CBETH.exchangeRate(); - else if (readType == OracleReadType.RETH) return RETH.getExchangeRate(); - else if (readType == OracleReadType.SFRXETH) return SFRXETH.pricePerShare(); - else if (readType == OracleReadType.PYTH) { + } else if (readType == OracleReadType.STABLE) { + return BASE_18; + } else if (readType == OracleReadType.NO_ORACLE) { + return baseValue; + } else if (readType == OracleReadType.WSTETH) { + return STETH.getPooledEthByShares(1 ether); + } else if (readType == OracleReadType.CBETH) { + return CBETH.exchangeRate(); + } else if (readType == OracleReadType.RETH) { + return RETH.getExchangeRate(); + } else if (readType == OracleReadType.SFRXETH) { + return SFRXETH.pricePerShare(); + } else if (readType == OracleReadType.PYTH) { ( address pyth, bytes32[] memory feedIds, @@ -155,10 +197,18 @@ library LibOracle { quotePrice = readPythFeed(quotePrice, feedIds[i], pyth, isMultiplied[i], stalePeriods[i]); } return quotePrice; + } else if (readType == OracleReadType.MAX) { + uint256 maxValue = abi.decode(data, (uint256)); + return maxValue; + } else if (readType == OracleReadType.MORPHO_ORACLE) { + (address contractAddress, uint256 normalizationFactor) = abi.decode(data, (address, uint256)); + return IMorphoOracle(contractAddress).price() / normalizationFactor; } // If the `OracleReadType` is `EXTERNAL`, it means that this function is called to compute a // `targetPrice` in which case the `baseValue` is returned here - else return baseValue; + else { + return baseValue; + } } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -207,7 +257,35 @@ library LibOracle { /// @notice Parses an `oracleConfig` into several sub fields function _parseOracleConfig( bytes memory oracleConfig - ) private pure returns (OracleReadType, OracleReadType, bytes memory, bytes memory) { - return abi.decode(oracleConfig, (OracleReadType, OracleReadType, bytes, bytes)); + ) private pure returns (OracleReadType, OracleReadType, bytes memory, bytes memory, bytes memory) { + return abi.decode(oracleConfig, (OracleReadType, OracleReadType, bytes, bytes, bytes)); + } + + function updateOracle(address collateral) internal { + TransmuterStorage storage ts = s.transmuterStorage(); + if (ts.collaterals[collateral].decimals == 0) revert NotCollateral(); + + ( + OracleReadType oracleType, + OracleReadType targetType, + bytes memory oracleData, + bytes memory targetData, + bytes memory hyperparameters + ) = _parseOracleConfig(ts.collaterals[collateral].oracleConfig); + + if (targetType != OracleReadType.MAX) revert OracleUpdateFailed(); + uint256 oracleValue = read(oracleType, BASE_18, oracleData); + + uint256 maxValue = abi.decode(targetData, (uint256)); + if (oracleValue > maxValue) + ts.collaterals[collateral].oracleConfig = abi.encode( + oracleType, + targetType, + oracleData, + // There are no checks whether the value increased or not + abi.encode(oracleValue), + hyperparameters + ); + else revert OracleUpdateFailed(); } } diff --git a/contracts/utils/Constants.sol b/contracts/utils/Constants.sol index bad794ae..34fcdd83 100644 --- a/contracts/utils/Constants.sol +++ b/contracts/utils/Constants.sol @@ -31,6 +31,7 @@ uint256 constant BASE_6 = 1e6; uint256 constant BASE_8 = 1e8; uint256 constant BASE_9 = 1e9; uint256 constant BASE_12 = 1e12; +uint256 constant BPS = 1e14; uint256 constant BASE_18 = 1e18; uint256 constant HALF_BASE_27 = 1e27 / 2; uint256 constant BASE_27 = 1e27; diff --git a/contracts/utils/Errors.sol b/contracts/utils/Errors.sol index 9dd71aa0..3115946a 100644 --- a/contracts/utils/Errors.sol +++ b/contracts/utils/Errors.sol @@ -33,6 +33,7 @@ error NotGovernorOrGuardian(); error NotTrusted(); error NotWhitelisted(); error OneInchSwapFailed(); +error OracleUpdateFailed(); error Paused(); error ReentrantCall(); error RemoveFacetAddressMustBeZeroAddress(address _facetAddress); diff --git a/foundry.toml b/foundry.toml index cc5021f7..d4075eaa 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,5 +1,5 @@ [profile.default] -src = 'contracts' +src = 'contract' out = 'out' test = 'test' libs = ['node_modules', 'lib'] @@ -10,17 +10,22 @@ via_ir = true sizes = true optimizer = true optimizer_runs=1000 -solc_version = '0.8.19' +solc_version = '0.8.22' ffi = true -fs_permissions = [{ access = "read-write", path = "./scripts/selectors.json"}, { access = "read-write", path = "./scripts/vanity.json"}] +fs_permissions = [ + { access = "read-write", path = "./scripts/selectors.json"}, + { access = "read-write", path = "./scripts/selectors_replace.json"}, + { access = "read-write", path = "./scripts/selectors_add.json"}, + { access = "read-write", path = "./scripts/vanity.json"} +] memory_limit = 1000043554432 [fuzz] runs = 10000 [invariant] -runs = 1000 -depth = 30 +runs = 8 +depth = 256 [rpc_endpoints] arbitrum = "${ETH_NODE_URI_ARBITRUM}" @@ -59,8 +64,8 @@ gas_reports = ["*"] runs = 2000 [profile.dev.invariant] -runs = 10 -depth = 1 +runs = 100 +depth = 30 fail_on_revert = false [profile.ci] @@ -70,7 +75,7 @@ via_ir = false gas_reports = ["*"] [profile.ci.fuzz] -runs = 100 +runs = 1000 [profile.ci.invariant] runs = 10 diff --git a/helpers/fork.sh b/helpers/fork.sh new file mode 100644 index 00000000..6102bbf3 --- /dev/null +++ b/helpers/fork.sh @@ -0,0 +1,46 @@ +#! /bin/bash + +source lib/utils/helpers/common.sh + +function main { + if [ ! -f .env ]; then + echo ".env not found!" + exit 1 + fi + source .env + + echo "Which chain would you like to fork ?" + echo "- 1: Ethereum Mainnet" + echo "- 2: Arbitrum" + echo "- 3: Polygon" + echo "- 4: Gnosis" + echo "- 5: Avalanche" + echo "- 6: Base" + echo "- 7: Binance Smart Chain" + echo "- 8: Celo" + echo "- 9: Polygon ZkEvm" + echo "- 10: Optimism" + echo "- 11: Linea" + + read option + + uri=$(chain_to_uri $option) + if [ -z "$uri" ]; then + echo "Unknown network" + exit 1 + fi + + echo "What block do you want to fork ? (Can leave empty for instant)" + + read block + + if [ -z "$block" ]; then + echo "Forking $uri" + anvil --fork-url $uri + else + echo "Forking $uri at block $block" + anvil --fork-url $uri --fork-block-number $block + fi +} + +main diff --git a/lib/utils b/lib/utils index e33f4b9f..41388c6e 160000 --- a/lib/utils +++ b/lib/utils @@ -1 +1 @@ -Subproject commit e33f4b9f042704eb02f37e32d93c108f5005cd3f +Subproject commit 41388c6ea9c45b05b92155356d6e700802fdb566 diff --git a/package.json b/package.json index a8b4e8d2..690660c2 100644 --- a/package.json +++ b/package.json @@ -7,28 +7,19 @@ "coverage": "FOUNDRY_PROFILE=dev forge coverage --report lcov && yarn lcov:clean && yarn lcov:generate-html", "compile": "forge build", "compile:dev": "FOUNDRY_PROFILE=dev forge build", - "deploy": "forge script --skip test --broadcast --verify --slow -vvvv scripts/DeploySavingsNoImplem.s.sol --rpc-url", - "deploy:fork": "source .env && forge script --skip test --slow --fork-url fork --broadcast scripts/DeploySavingsNoImplem.s.sol -vvvv", + "deploy": "forge script --skip test --broadcast --verify --slow -vvvv --rpc-url", + "deploy:fork": "forge script --skip test --slow --fork-url fork --broadcast -vvvv", "generate": "FOUNDRY_PROFILE=dev forge script scripts/utils/GenerateSelectors.s.sol", "deploy:check": "FOUNDRY_PROFILE=dev forge script --fork-url fork scripts/test/CheckTransmuter.s.sol", - "gas": "yarn test --gas-report", - "fork": "source .env && anvil --fork-url $ETH_NODE_URI_BASE", - "fork:arbitrum": "source .env && anvil --fork-url $ETH_NODE_URI_ARBITRUM", - "fork:polygon": "source .env && anvil --fork-url $ETH_NODE_URI_POLYGON", - "fork:gnosis": "source .env && anvil --fork-url $ETH_NODE_URI_GNOSIS", - "fork:avalanche": "source .env && anvil --fork-url $ETH_NODE_URI_AVALANCHE", - "fork:base": "source .env && anvil --fork-url $ETH_NODE_URI_BASE", - "fork:bsc": "source .env && anvil --fork-url $ETH_NODE_URI_BSC", - "fork:celo": "source .env && anvil --fork-url $ETH_NODE_URI_CELO", - "fork:zkevm": "source .env && anvil --fork-url $ETH_NODE_URI_POLYGONZKEVM", - "fork:optimism": "source .env && anvil --fork-url $ETH_NODE_URI_OPTIMISM", + "gas": "FOUNDRY_PROFILE=dev yarn test --gas-report", + "fork": "bash helpers/fork.sh", "run": "docker run -it --rm -v $(pwd):/app -w /app ghcr.io/foundry-rs/foundry sh", - "script:fork": "source .env && forge script --skip test --fork-url fork --broadcast -vvvv", + "script:fork": "FOUNDRY_PROFILE=dev forge script --skip test --fork-url fork --broadcast -vvvv", "test:unit": "forge test -vvvv --gas-report --match-path \"test/units/**/*.sol\"", "test:invariant": "forge test -vvv --gas-report --match-path \"test/invariants/**/*.sol\"", "test:fuzz": "forge test -vvv --gas-report --match-path \"test/fuzz/**/*.sol\"", "slither": "chmod +x ./slither.sh && ./slither.sh", - "test": "FOUNDRY_PROFILE=dev forge test -vvv", + "test": "forge test -vvvv", "lcov:clean": "lcov --remove lcov.info -o lcov.info 'test/**' 'scripts/**' 'contracts/transmuter/configs/**' 'contracts/utils/**'", "lcov:generate-html": "genhtml lcov.info --output=coverage", "size": "forge build --skip test --sizes", @@ -53,6 +44,6 @@ "solhint-plugin-prettier": "^0.0.5" }, "dependencies": { - "@angleprotocol/sdk": "^3.0.129" + "@angleprotocol/sdk": "^0.37.1" } } diff --git a/scripts/Constants.s.sol b/scripts/Constants.s.sol index 1737646c..17593753 100644 --- a/scripts/Constants.s.sol +++ b/scripts/Constants.s.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.19; -import "contracts/utils/Constants.sol"; -import { CHAIN_ETHEREUM } from "utils/src/Constants.sol"; +import { MAX_MINT_FEE, MAX_BURN_FEE, BASE_6, BASE_8, BPS } from "contracts/utils/Constants.sol"; +import "utils/src/Constants.sol"; /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// MAINNET CONSTANTS @@ -10,19 +10,46 @@ import { CHAIN_ETHEREUM } from "utils/src/Constants.sol"; uint256 constant CHAIN_SOURCE = CHAIN_ETHEREUM; -address constant IMMUTABLE_CREATE2_FACTORY_ADDRESS = 0x0000000000FFe8B47B3e2130213B802212439497; address constant DEPLOYER = 0xfdA462548Ce04282f4B6D6619823a7C64Fdc0185; +address constant KEEPER = 0xcC617C6f9725eACC993ac626C7efC6B96476916E; +address constant NEW_DEPLOYER = 0xA9DdD91249DFdd450E81E1c56Ab60E1A62651701; +address constant NEW_KEEPER = 0xa9bbbDDe822789F123667044443dc7001fb43C01; address constant EUROC = 0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c; address constant EUROE = 0x820802Fa8a99901F52e39acD21177b0BE6EE2974; address constant EURE = 0x3231Cb76718CDeF2155FC47b5286d82e6eDA273f; address constant BC3M = 0x2F123cF3F37CE3328CC9B5b8415f9EC5109b45e7; - address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; +address constant BERNX = 0x3f95AA88dDbB7D9D484aa3D482bf0a80009c52c9; +address constant STEAK_USDC = 0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB; +address constant BIB01 = 0xCA30c93B02514f86d5C86a6e375E3A330B435Fb5; + +// EUROC +uint128 constant FIREWALL_BURN_RATIO_EUROC = uint128(0); +uint128 constant USER_PROTECTION_EUROC = uint128(10 * BPS); + +// BC3M +uint128 constant FIREWALL_BURN_RATIO_BC3M = uint128(10 * BPS); +uint128 constant USER_PROTECTION_BC3M = uint128(0); + +// ERNX +uint128 constant FIREWALL_BURN_RATIO_ERNX = uint128(20 * BPS); +uint128 constant USER_PROTECTION_ERNX = uint128(0); + +uint128 constant FIREWALL_BURN_RATIO_USDC = uint128(0); +uint128 constant USER_PROTECTION_USDC = uint128(5 * BPS); + +uint128 constant FIREWALL_BURN_RATIO_STEAK_USDC = uint128(5 * BPS); +uint128 constant USER_PROTECTION_STEAK_USDC = uint128(0); + +uint128 constant FIREWALL_BURN_RATIO_IB01 = uint128(20 * BPS); +uint128 constant USER_PROTECTION_IB01 = uint128(0); + +uint32 constant HEARTBEAT = uint32(7 days); /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FACET ADDRESSES - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ address constant DIAMOND_CUT_FACET = 0x53B7d70013dEC21A97F216e80eEFCF45F25c2900; address constant DIAMOND_ETHERSCAN_FACET = 0xFa94Cd9d711de75695693c877BecA5473462Cf12; diff --git a/scripts/DeploySavingsNoImplem.s.sol b/scripts/DeploySavingsNoImplem.s.sol index f3e2fe2d..52c90fc9 100644 --- a/scripts/DeploySavingsNoImplem.s.sol +++ b/scripts/DeploySavingsNoImplem.s.sol @@ -15,7 +15,7 @@ import { TransparentUpgradeableProxy } from "oz/proxy/transparent/TransparentUpg import { ImmutableCreate2Factory } from "./utils/TransmuterDeploymentHelper.s.sol"; -import { MockTreasury } from "../test/mock/MockTreasury.sol"; +import { MockTreasury } from "test/mock/MockTreasury.sol"; /// @dev To deploy on a different chain, just replace the chainId and be sure the sdk has the required addresses contract DeploySavingsNoImplem is Utils { diff --git a/scripts/DeployTransmuter.s.sol b/scripts/DeployTransmuter.s.sol index 50c42cfb..6234c75d 100644 --- a/scripts/DeployTransmuter.s.sol +++ b/scripts/DeployTransmuter.s.sol @@ -42,7 +42,7 @@ contract DeployTransmuter is TransmuterDeploymentHelper { abi.encodeWithSelector( Production.initialize.selector, _chainToContract(CHAIN_SOURCE, ContractType.CoreBorrow), - AGEUR, + _chainToContract(CHAIN_SOURCE, ContractType.AgEUR), dummyImplementation ) ); diff --git a/scripts/DeployTransmuterWithoutFacets.s.sol b/scripts/DeployTransmuterWithoutFacets.s.sol index e5b86cff..98f6044c 100644 --- a/scripts/DeployTransmuterWithoutFacets.s.sol +++ b/scripts/DeployTransmuterWithoutFacets.s.sol @@ -23,7 +23,7 @@ import { DummyDiamondImplementation } from "./generated/DummyDiamondImplementati import { Swapper } from "contracts/transmuter/facets/Swapper.sol"; import { ITransmuter } from "interfaces/ITransmuter.sol"; -import { MockTreasury } from "../test/mock/MockTreasury.sol"; +import { MockTreasury } from "test/mock/MockTreasury.sol"; import { MockToken } from "borrow/mock/MockToken.sol"; contract DeployTransmuterWithoutFacets is TransmuterDeploymentHelper { diff --git a/scripts/Helpers.s.sol b/scripts/Helpers.s.sol new file mode 100644 index 00000000..c2aedf11 --- /dev/null +++ b/scripts/Helpers.s.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "./Constants.s.sol"; +import "./utils/Utils.s.sol"; + +/// @title Utils +/// @author Angle Labs, Inc. +contract Helpers is Utils { + mapping(uint256 => uint256) internal forkIdentifier; + uint256 public arbitrumFork; + uint256 public avalancheFork; + uint256 public ethereumFork; + uint256 public optimismFork; + uint256 public polygonFork; + uint256 public gnosisFork; + uint256 public bnbFork; + uint256 public celoFork; + uint256 public polygonZkEVMFork; + uint256 public baseFork; + uint256 public lineaFork; + + address public alice; + address public bob; + address public charlie; + address public dylan; + address public sweeper; + + function setUp() public virtual { + arbitrumFork = vm.createFork(vm.envString("ETH_NODE_URI_ARBITRUM")); + avalancheFork = vm.createFork(vm.envString("ETH_NODE_URI_AVALANCHE")); + ethereumFork = vm.createFork(vm.envString("ETH_NODE_URI_MAINNET")); + optimismFork = vm.createFork(vm.envString("ETH_NODE_URI_OPTIMISM")); + polygonFork = vm.createFork(vm.envString("ETH_NODE_URI_POLYGON")); + gnosisFork = vm.createFork(vm.envString("ETH_NODE_URI_GNOSIS")); + bnbFork = vm.createFork(vm.envString("ETH_NODE_URI_BSC")); + celoFork = vm.createFork(vm.envString("ETH_NODE_URI_CELO")); + polygonZkEVMFork = vm.createFork(vm.envString("ETH_NODE_URI_POLYGON_ZKEVM")); + baseFork = vm.createFork(vm.envString("ETH_NODE_URI_BASE")); + lineaFork = vm.createFork(vm.envString("ETH_NODE_URI_LINEA")); + + forkIdentifier[CHAIN_ARBITRUM] = arbitrumFork; + forkIdentifier[CHAIN_AVALANCHE] = avalancheFork; + forkIdentifier[CHAIN_ETHEREUM] = ethereumFork; + forkIdentifier[CHAIN_OPTIMISM] = optimismFork; + forkIdentifier[CHAIN_POLYGON] = polygonFork; + forkIdentifier[CHAIN_GNOSIS] = gnosisFork; + forkIdentifier[CHAIN_BNB] = bnbFork; + forkIdentifier[CHAIN_CELO] = celoFork; + forkIdentifier[CHAIN_POLYGONZKEVM] = polygonZkEVMFork; + forkIdentifier[CHAIN_BASE] = baseFork; + forkIdentifier[CHAIN_LINEA] = lineaFork; + + alice = vm.addr(1); + bob = vm.addr(2); + charlie = vm.addr(3); + dylan = vm.addr(4); + sweeper = address(uint160(uint256(keccak256(abi.encodePacked("sweeper"))))); + } +} diff --git a/scripts/SetupDeployedTransmuter.s.sol b/scripts/SetupDeployedTransmuter.s.sol new file mode 100644 index 00000000..a709e2d8 --- /dev/null +++ b/scripts/SetupDeployedTransmuter.s.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import { Utils } from "./utils/Utils.s.sol"; +import { console } from "forge-std/console.sol"; +import { stdJson } from "forge-std/StdJson.sol"; +import "stringutils/strings.sol"; +import "./Constants.s.sol"; + +import "contracts/transmuter/Storage.sol" as Storage; +import { ITransmuter } from "interfaces/ITransmuter.sol"; +import "interfaces/external/chainlink/AggregatorV3Interface.sol"; +import "interfaces/external/IERC4626.sol"; + +import { CollateralSetupProd } from "contracts/transmuter/configs/ProductionTypes.sol"; + +contract SetupDeployedTransmuter is Utils { + function run() external { + uint256 deployerPrivateKey = vm.deriveKey(vm.envString("MNEMONIC_MAINNET"), "m/44'/60'/0'/0/", 0); + address deployer = vm.addr(deployerPrivateKey); + console.log("Address: %s", deployer); + console.log(deployer.balance); + vm.startBroadcast(deployerPrivateKey); + + ITransmuter usdaTransmuter = ITransmuter(0x222222fD79264BBE280b4986F6FEfBC3524d0137); + console.log(address(usdaTransmuter)); + + // TODO Run this script after facet upgrade script otherwise it won't work due to oracles calibrated + // in a different manner + + // For USDC, we just need to update the oracle as the fees have already been properly set for this use case + + { + bytes memory oracleConfig; + bytes memory readData; + { + AggregatorV3Interface[] memory circuitChainlink = new AggregatorV3Interface[](1); + uint32[] memory stalePeriods = new uint32[](1); + uint8[] memory circuitChainIsMultiplied = new uint8[](1); + uint8[] memory chainlinkDecimals = new uint8[](1); + + // Chainlink USDC/USD oracle + circuitChainlink[0] = AggregatorV3Interface(0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6); + stalePeriods[0] = ((1 days) * 3) / 2; + circuitChainIsMultiplied[0] = 1; + chainlinkDecimals[0] = 8; + Storage.OracleQuoteType quoteType = Storage.OracleQuoteType.UNIT; + readData = abi.encode( + circuitChainlink, + stalePeriods, + circuitChainIsMultiplied, + chainlinkDecimals, + quoteType + ); + } + bytes memory targetData; + oracleConfig = abi.encode( + Storage.OracleReadType.CHAINLINK_FEEDS, + Storage.OracleReadType.STABLE, + readData, + targetData, + abi.encode(uint128(5 * BPS), uint128(0)) + ); + usdaTransmuter.setOracle(USDC, oracleConfig); + } + + // Set Collaterals + CollateralSetupProd[] memory collaterals = new CollateralSetupProd[](2); + + // IB01 + { + uint64[] memory xMintFeeIB01 = new uint64[](3); + xMintFeeIB01[0] = uint64(0); + xMintFeeIB01[1] = uint64((49 * BASE_9) / 100); + xMintFeeIB01[2] = uint64((50 * BASE_9) / 100); + + int64[] memory yMintFeeIB01 = new int64[](3); + yMintFeeIB01[0] = int64(0); + yMintFeeIB01[1] = int64(0); + yMintFeeIB01[2] = int64(uint64(MAX_MINT_FEE)); + + uint64[] memory xBurnFeeIB01 = new uint64[](3); + xBurnFeeIB01[0] = uint64(BASE_9); + xBurnFeeIB01[1] = uint64((16 * BASE_9) / 100); + xBurnFeeIB01[2] = uint64((15 * BASE_9) / 100); + + int64[] memory yBurnFeeIB01 = new int64[](3); + yBurnFeeIB01[0] = int64(uint64((50 * BASE_9) / 10000)); + yBurnFeeIB01[1] = int64(uint64((50 * BASE_9) / 10000)); + yBurnFeeIB01[2] = int64(uint64(MAX_BURN_FEE)); + + bytes memory oracleConfig; + { + bytes memory readData; + { + AggregatorV3Interface[] memory circuitChainlink = new AggregatorV3Interface[](1); + uint32[] memory stalePeriods = new uint32[](1); + uint8[] memory circuitChainIsMultiplied = new uint8[](1); + uint8[] memory chainlinkDecimals = new uint8[](1); + // Chainlink IB01/USD oracle + circuitChainlink[0] = AggregatorV3Interface(0x32d1463EB53b73C095625719Afa544D5426354cB); + stalePeriods[0] = ((1 days) * 3) / 2; + circuitChainIsMultiplied[0] = 1; + chainlinkDecimals[0] = 8; + Storage.OracleQuoteType quoteType = Storage.OracleQuoteType.UNIT; + readData = abi.encode( + circuitChainlink, + stalePeriods, + circuitChainIsMultiplied, + chainlinkDecimals, + quoteType + ); + } + + (, int256 answer, , , ) = AggregatorV3Interface(0x32d1463EB53b73C095625719Afa544D5426354cB) + .latestRoundData(); + uint256 initTarget = uint256(answer) * 1e10; + bytes memory targetData = abi.encode(initTarget); + + oracleConfig = abi.encode( + Storage.OracleReadType.CHAINLINK_FEEDS, + Storage.OracleReadType.MAX, + readData, + targetData, + abi.encode(USER_PROTECTION_IB01, FIREWALL_BURN_RATIO_IB01) + ); + } + collaterals[0] = CollateralSetupProd( + BIB01, + oracleConfig, + xMintFeeIB01, + yMintFeeIB01, + xBurnFeeIB01, + yBurnFeeIB01 + ); + } + + // steakUSDC -> max oracle or target oracle + { + uint64[] memory xMintFeeSteak = new uint64[](3); + xMintFeeSteak[0] = uint64(0); + xMintFeeSteak[1] = uint64((79 * BASE_9) / 100); + xMintFeeSteak[2] = uint64((80 * BASE_9) / 100); + + int64[] memory yMintFeeSteak = new int64[](3); + yMintFeeSteak[0] = int64(uint64((5 * BASE_9) / 10000)); + yMintFeeSteak[1] = int64(uint64((5 * BASE_9) / 10000)); + yMintFeeSteak[2] = int64(uint64(MAX_MINT_FEE)); + + uint64[] memory xBurnFeeSteak = new uint64[](3); + xBurnFeeSteak[0] = uint64(BASE_9); + xBurnFeeSteak[1] = uint64((31 * BASE_9) / 100); + xBurnFeeSteak[2] = uint64((30 * BASE_9) / 100); + + int64[] memory yBurnFeeSteak = new int64[](3); + yBurnFeeSteak[0] = int64(uint64((5 * BASE_9) / 10000)); + yBurnFeeSteak[1] = int64(uint64((5 * BASE_9) / 10000)); + yBurnFeeSteak[2] = int64(uint64(MAX_BURN_FEE)); + + bytes memory oracleConfig; + { + bytes memory readData = abi.encode(0x025106374196586E8BC91eE8818dD7B0Efd2B78B, BASE_18); + // Current price is 1.012534 -> we take a small margin + uint256 startPrice = IERC4626(STEAK_USDC).previewRedeem(1e30); + bytes memory targetData = abi.encode(startPrice); + oracleConfig = abi.encode( + Storage.OracleReadType.MORPHO_ORACLE, + Storage.OracleReadType.MAX, + readData, + targetData, + abi.encode(USER_PROTECTION_STEAK_USDC, FIREWALL_BURN_RATIO_STEAK_USDC) + ); + } + collaterals[1] = CollateralSetupProd( + STEAK_USDC, + oracleConfig, + xMintFeeSteak, + yMintFeeSteak, + xBurnFeeSteak, + yBurnFeeSteak + ); + } + + // Setup each collateral + uint256 collateralsLength = collaterals.length; + for (uint256 i; i < collateralsLength; i++) { + CollateralSetupProd memory collateral = collaterals[i]; + usdaTransmuter.addCollateral(collateral.token); + usdaTransmuter.setOracle(collateral.token, collateral.oracleConfig); + // Mint fees + usdaTransmuter.setFees(collateral.token, collateral.xMintFee, collateral.yMintFee, true); + // Burn fees + usdaTransmuter.setFees(collateral.token, collateral.xBurnFee, collateral.yBurnFee, false); + usdaTransmuter.togglePause(collateral.token, Storage.ActionType.Mint); + usdaTransmuter.togglePause(collateral.token, Storage.ActionType.Burn); + } + + // Set whitelist status for bIB01 + bytes memory whitelistData = abi.encode( + Storage.WhitelistType.BACKED, + abi.encode(address(0x9391B14dB2d43687Ea1f6E546390ED4b20766c46)) + ); + usdaTransmuter.setWhitelistStatus(BIB01, 1, whitelistData); + + usdaTransmuter.toggleTrusted(NEW_DEPLOYER, Storage.TrustedType.Seller); + usdaTransmuter.toggleTrusted(NEW_KEEPER, Storage.TrustedType.Seller); + + console.log("Transmuter setup"); + vm.stopBroadcast(); + } +} diff --git a/scripts/UpdateTransmuterFacets.s.sol b/scripts/UpdateTransmuterFacets.s.sol new file mode 100644 index 00000000..79374955 --- /dev/null +++ b/scripts/UpdateTransmuterFacets.s.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import "./utils/Utils.s.sol"; +import { console } from "forge-std/console.sol"; +import { stdJson } from "forge-std/StdJson.sol"; +import "stringutils/strings.sol"; +import "./Constants.s.sol"; +import { Helpers } from "./Helpers.s.sol"; + +import "contracts/utils/Errors.sol" as Errors; +import "contracts/transmuter/Storage.sol" as Storage; +import { Getters } from "contracts/transmuter/facets/Getters.sol"; +import { Redeemer } from "contracts/transmuter/facets/Redeemer.sol"; +import { SettersGovernor } from "contracts/transmuter/facets/SettersGovernor.sol"; +import { SettersGuardian } from "contracts/transmuter/facets/SettersGuardian.sol"; +import { Swapper } from "contracts/transmuter/facets/Swapper.sol"; +import "contracts/transmuter/libraries/LibHelpers.sol"; +import { ITransmuter } from "interfaces/ITransmuter.sol"; +import "utils/src/Constants.sol"; +import { IERC20 } from "oz/interfaces/IERC20.sol"; +import { OldTransmuter } from "test/scripts/UpdateTransmuterFacets.t.sol"; + +contract UpdateTransmuterFacets is Helpers { + string[] replaceFacetNames; + address[] facetAddressList; + + ITransmuter transmuter; + IERC20 agEUR; + address governor; + bytes public oracleConfigEUROC; + bytes public oracleConfigBC3M; + + function run() external { + // TODO: make sure that selectors are well generated `yarn generate` before running this script + // Here the `selectors.json` file is normally up to date + uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + Storage.FacetCut[] memory replaceCut; + Storage.FacetCut[] memory addCut; + + replaceFacetNames.push("Getters"); + facetAddressList.push(address(new Getters())); + console.log("Getters deployed at: ", facetAddressList[facetAddressList.length - 1]); + + replaceFacetNames.push("Redeemer"); + facetAddressList.push(address(new Redeemer())); + console.log("Redeemer deployed at: ", facetAddressList[facetAddressList.length - 1]); + + replaceFacetNames.push("SettersGovernor"); + facetAddressList.push(address(new SettersGovernor())); + console.log("SettersGovernor deployed at: ", facetAddressList[facetAddressList.length - 1]); + + replaceFacetNames.push("Swapper"); + facetAddressList.push(address(new Swapper())); + console.log("Swapper deployed at: ", facetAddressList[facetAddressList.length - 1]); + + // TODO Governance should pass tx to upgrade them from the `angle-governance` repo + } +} diff --git a/scripts/selectors.json b/scripts/selectors.json index 24868e68..0db1b010 100644 --- a/scripts/selectors.json +++ b/scripts/selectors.json @@ -36,6 +36,9 @@ "0xa52aefd400000000000000000000000000000000000000000000000000000000", "0x99eeca4900000000000000000000000000000000000000000000000000000000" ], + "Oracle": [ + "0x1cb44dfc00000000000000000000000000000000000000000000000000000000" + ], "Redeemer": [ "0xd703a0cd00000000000000000000000000000000000000000000000000000000", "0x815822c100000000000000000000000000000000000000000000000000000000", diff --git a/scripts/selectors_add.json b/scripts/selectors_add.json new file mode 100644 index 00000000..ffec3deb --- /dev/null +++ b/scripts/selectors_add.json @@ -0,0 +1,6 @@ +{ + "SettersGovernor": [ + "0x1cb44dfc00000000000000000000000000000000000000000000000000000000" + ], + "useless": "" +} \ No newline at end of file diff --git a/scripts/selectors_replace.json b/scripts/selectors_replace.json new file mode 100644 index 00000000..f45a2e9e --- /dev/null +++ b/scripts/selectors_replace.json @@ -0,0 +1,75 @@ +{ + "DiamondCut": [ + "0x1f931c1c00000000000000000000000000000000000000000000000000000000" + ], + "DiamondEtherscan": [ + "0x5c60da1b00000000000000000000000000000000000000000000000000000000", + "0xc39aa07d00000000000000000000000000000000000000000000000000000000" + ], + "DiamondLoupe": [ + "0xcdffacc600000000000000000000000000000000000000000000000000000000", + "0x52ef6b2c00000000000000000000000000000000000000000000000000000000", + "0xadfca15e00000000000000000000000000000000000000000000000000000000", + "0x7a0ed62700000000000000000000000000000000000000000000000000000000" + ], + "Getters": [ + "0xb4a0bdf300000000000000000000000000000000000000000000000000000000", + "0xee565a6300000000000000000000000000000000000000000000000000000000", + "0x847da7be00000000000000000000000000000000000000000000000000000000", + "0xeb7aac5f00000000000000000000000000000000000000000000000000000000", + "0x3335221000000000000000000000000000000000000000000000000000000000", + "0xb718136100000000000000000000000000000000000000000000000000000000", + "0xb85780bc00000000000000000000000000000000000000000000000000000000", + "0xcd377c5300000000000000000000000000000000000000000000000000000000", + "0x782513bd00000000000000000000000000000000000000000000000000000000", + "0x94e35d9e00000000000000000000000000000000000000000000000000000000", + "0x4ea3e34300000000000000000000000000000000000000000000000000000000", + "0x10d3d22e00000000000000000000000000000000000000000000000000000000", + "0x38c269eb00000000000000000000000000000000000000000000000000000000", + "0xadc9d1f700000000000000000000000000000000000000000000000000000000", + "0x8db9653f00000000000000000000000000000000000000000000000000000000", + "0x0d12662700000000000000000000000000000000000000000000000000000000", + "0x96d6487900000000000000000000000000000000000000000000000000000000", + "0xfe7d0c5400000000000000000000000000000000000000000000000000000000", + "0x77dc342900000000000000000000000000000000000000000000000000000000", + "0xf9839d8900000000000000000000000000000000000000000000000000000000", + "0xa52aefd400000000000000000000000000000000000000000000000000000000", + "0x99eeca4900000000000000000000000000000000000000000000000000000000" + ], + "Redeemer": [ + "0xd703a0cd00000000000000000000000000000000000000000000000000000000", + "0x815822c100000000000000000000000000000000000000000000000000000000", + "0x2e7639bc00000000000000000000000000000000000000000000000000000000", + "0xfd7daaf800000000000000000000000000000000000000000000000000000000" + ], + "RewardHandler": [ + "0x05b4193400000000000000000000000000000000000000000000000000000000" + ], + "SettersGovernor": [ + "0xf0d2d5a800000000000000000000000000000000000000000000000000000000", + "0xc1cdee7e00000000000000000000000000000000000000000000000000000000", + "0x87c8ab7a00000000000000000000000000000000000000000000000000000000", + "0x5c3eebda00000000000000000000000000000000000000000000000000000000", + "0x1f0ec8ee00000000000000000000000000000000000000000000000000000000", + "0x0e32cb8600000000000000000000000000000000000000000000000000000000", + "0x81ee2deb00000000000000000000000000000000000000000000000000000000", + "0xb13b084700000000000000000000000000000000000000000000000000000000", + "0x1b0c718200000000000000000000000000000000000000000000000000000000", + "0x7c0343a100000000000000000000000000000000000000000000000000000000" + ], + "SettersGuardian": [ + "0x629feb6200000000000000000000000000000000000000000000000000000000", + "0x4eec47b900000000000000000000000000000000000000000000000000000000", + "0xa9e6a1a400000000000000000000000000000000000000000000000000000000", + "0xb607d09900000000000000000000000000000000000000000000000000000000" + ], + "Swapper": [ + "0x4583aea600000000000000000000000000000000000000000000000000000000", + "0x9525f3ab00000000000000000000000000000000000000000000000000000000", + "0x3b6a1fe000000000000000000000000000000000000000000000000000000000", + "0xd92c6cb200000000000000000000000000000000000000000000000000000000", + "0xb92567fa00000000000000000000000000000000000000000000000000000000", + "0xc10a628700000000000000000000000000000000000000000000000000000000" + ], + "useless": "" +} \ No newline at end of file diff --git a/scripts/test/CheckFakeTransmuter.s.sol b/scripts/test/CheckFakeTransmuter.s.sol index fd708294..530ab7b1 100644 --- a/scripts/test/CheckFakeTransmuter.s.sol +++ b/scripts/test/CheckFakeTransmuter.s.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.19; -import { Utils } from "../utils/Utils.s.sol"; +import { Utils, AssertUtils } from "../utils/Utils.s.sol"; import { console } from "forge-std/console.sol"; import { StdAssertions } from "forge-std/Test.sol"; import { ITransmuter } from "interfaces/ITransmuter.sol"; import "stringutils/strings.sol"; import "contracts/transmuter/Storage.sol" as Storage; -contract CheckFakeTransmuter is Utils { +contract CheckFakeTransmuter is Utils, AssertUtils { using strings for *; ITransmuter public constant transmuter = ITransmuter(0x4A44f77978Daa3E92Eb3D97210bd11645cF935Ab); @@ -66,7 +66,7 @@ contract CheckFakeTransmuter is Utils { // Checks all valid selectors are here bytes4[] memory selectors = _generateSelectors("ITransmuter"); - for (uint i = 0; i < selectors.length; ++i) { + for (uint256 i = 0; i < selectors.length; ++i) { assertEq(transmuter.isValidSelector(selectors[i]), true); } assertEq(address(transmuter.accessControlManager()), address(CORE_BORROW)); diff --git a/scripts/test/CheckTransmuter.s.sol b/scripts/test/CheckTransmuter.s.sol index d9d2bff0..786b2d3a 100644 --- a/scripts/test/CheckTransmuter.s.sol +++ b/scripts/test/CheckTransmuter.s.sol @@ -11,13 +11,15 @@ import "stringutils/strings.sol"; import "../Constants.s.sol"; import "contracts/transmuter/Storage.sol" as Storage; -contract CheckTransmuter is Utils, StdCheats { +contract CheckTransmuter is Utils, AssertUtils, StdCheats { using strings for *; // TODO: replace with deployed Transmuter address ITransmuter public constant transmuter = ITransmuter(0x1757a98c1333B9dc8D408b194B2279b5AFDF70Cc); + address public AGEUR; function run() external { + AGEUR = _chainToContract(CHAIN_SOURCE, ContractType.AgEUR); /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FEE STRUCTURE //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ diff --git a/scripts/test/CheckTransmuterUSD.s.sol b/scripts/test/CheckTransmuterUSD.s.sol index 6d2ec7d6..a080f99e 100644 --- a/scripts/test/CheckTransmuterUSD.s.sol +++ b/scripts/test/CheckTransmuterUSD.s.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.19; -import { Utils } from "../utils/Utils.s.sol"; +import { Utils, AssertUtils } from "../utils/Utils.s.sol"; import { console } from "forge-std/console.sol"; import { StdCheats } from "forge-std/Test.sol"; import { IERC20 } from "oz/token/ERC20/IERC20.sol"; @@ -11,13 +11,15 @@ import "stringutils/strings.sol"; import "../Constants.s.sol"; import "contracts/transmuter/Storage.sol" as Storage; -contract CheckTransmuterUSD is Utils, StdCheats { +contract CheckTransmuterUSD is Utils, AssertUtils, StdCheats { using strings for *; // TODO: replace with deployed Transmuter address ITransmuter public constant transmuter = ITransmuter(0x712B29A840d717C5B1150f02cCaA01fedaD78F4c); + address public AGEUR; function run() external { + AGEUR = _chainToContract(CHAIN_SOURCE, ContractType.AgEUR); address stablecoin = address(transmuter.agToken()); /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FEE STRUCTURE diff --git a/scripts/utils/Utils.s.sol b/scripts/utils/Utils.s.sol index e19d6285..a3d59e91 100644 --- a/scripts/utils/Utils.s.sol +++ b/scripts/utils/Utils.s.sol @@ -5,34 +5,21 @@ import "forge-std/Script.sol"; import { StdAssertions } from "forge-std/Test.sol"; import "stringutils/strings.sol"; -import { TransparentUpgradeableProxy } from "oz/proxy/transparent/TransparentUpgradeableProxy.sol"; import { CommonUtils } from "utils/src/CommonUtils.sol"; import { ContractType } from "utils/src/Constants.sol"; -contract Utils is Script, StdAssertions, CommonUtils { +contract Utils is Script, CommonUtils { using strings for *; string constant JSON_SELECTOR_PATH = "./scripts/selectors.json"; + string constant JSON_SELECTOR_PATH_REPLACE = "./scripts/selectors_replace.json"; + string constant JSON_SELECTOR_PATH_ADD = "./scripts/selectors_add.json"; string constant JSON_VANITY_PATH = "./scripts/vanity.json"; /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// HELPERS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - function _assertArrayUint64(uint64[] memory _a, uint64[] memory _b) internal { - assertEq(_a.length, _b.length); - for (uint i = 0; i < _a.length; ++i) { - assertEq(_a[i], _b[i]); - } - } - - function _assertArrayInt64(int64[] memory _a, int64[] memory _b) internal { - assertEq(_a.length, _b.length); - for (uint i = 0; i < _a.length; ++i) { - assertEq(_a[i], _b[i]); - } - } - function _bytes4ToBytes32(bytes4 _in) internal pure returns (bytes32 out) { assembly { out := _in @@ -41,21 +28,37 @@ contract Utils is Script, StdAssertions, CommonUtils { function _arrayBytes4ToBytes32(bytes4[] memory _in) internal pure returns (bytes32[] memory out) { out = new bytes32[](_in.length); - for (uint i = 0; i < _in.length; ++i) { + for (uint256 i = 0; i < _in.length; ++i) { out[i] = _bytes4ToBytes32(_in[i]); } } function _arrayBytes32ToBytes4(bytes32[] memory _in) internal pure returns (bytes4[] memory out) { out = new bytes4[](_in.length); - for (uint i = 0; i < _in.length; ++i) { + for (uint256 i = 0; i < _in.length; ++i) { out[i] = bytes4(_in[i]); } } function consoleLogBytes4Array(bytes4[] memory _in) internal view { - for (uint i = 0; i < _in.length; ++i) { + for (uint256 i = 0; i < _in.length; ++i) { console.logBytes4(_in[i]); } } } + +contract AssertUtils is StdAssertions { + function _assertArrayUint64(uint64[] memory _a, uint64[] memory _b) internal { + assertEq(_a.length, _b.length); + for (uint256 i = 0; i < _a.length; ++i) { + assertEq(_a[i], _b[i]); + } + } + + function _assertArrayInt64(int64[] memory _a, int64[] memory _b) internal { + assertEq(_a.length, _b.length); + for (uint256 i = 0; i < _a.length; ++i) { + assertEq(_a[i], _b[i]); + } + } +} diff --git a/slither.config.json b/slither.config.json index 7ae70c20..9146b6c8 100644 --- a/slither.config.json +++ b/slither.config.json @@ -1,19 +1,19 @@ -{ - "detectors_to_exclude": "naming-convention,solc-version", - "filter_paths": "(lib|test|external)", - "solc_remaps": [ - "ds-test/=lib/forge-std/lib/ds-test/src/", - "forge-std/=lib/forge-std/src/", - "stringutils/=lib/solidity-stringutils", - "contracts/=contracts/", - "test/=test/", - "interfaces/=contracts/interfaces/", - "oz/=lib/openzeppelin-contracts/contracts/", - "oz-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", - "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", - "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", - "mock/=test/mock/", - "prb/math/=lib/prb-math/src/", - "borrow/=lib/borrow-contracts/contracts" - ] -} +{ + "detectors_to_exclude": "naming-convention,solc-version", + "filter_paths": "(lib|test|external|scripts)", + "solc_remaps": [ + "ds-test/=lib/forge-std/lib/ds-test/src/", + "forge-std/=lib/forge-std/src/", + "stringutils/=lib/solidity-stringutils", + "contracts/=contracts/", + "test/=test/", + "interfaces/=contracts/interfaces/", + "oz/=lib/openzeppelin-contracts/contracts/", + "oz-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", + "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", + "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", + "mock/=test/mock/", + "prb/math/=lib/prb-math/src/", + "borrow/=lib/borrow-contracts/contracts" + ] +} \ No newline at end of file diff --git a/test/fuzz/BurnTest.t.sol b/test/fuzz/BurnTest.t.sol index e67dca49..48bde0af 100644 --- a/test/fuzz/BurnTest.t.sol +++ b/test/fuzz/BurnTest.t.sol @@ -6,6 +6,7 @@ import { stdError } from "forge-std/Test.sol"; import { ManagerStorage, ManagerType, WhitelistType } from "contracts/transmuter/Storage.sol"; import "contracts/utils/Errors.sol" as Errors; +import "contracts/transmuter/Storage.sol" as Storage; import "../Fixture.sol"; import { IERC20Metadata } from "../mock/MockTokenPermit.sol"; @@ -663,6 +664,143 @@ contract BurnTest is Fixture, FunctionUtils { // This will crash if the } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + FIREWALL + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testFuzz_QuoteBurnExactInput_WithFirewall_FixPiecewiseFees( + uint256[3] memory initialAmounts, + uint256 transferProportion, + uint256[3] memory latestOracleValue, + uint128[6] memory userAndBurnFirewall, + int64 upperFees, + uint256 stableAmount, + uint256 fromToken + ) public { + _updateOracleFirewalls(userAndBurnFirewall); + // let's first load the reserves of the protocol + (uint256 mintedStables, uint256[] memory collateralMintedStables) = _loadReserves( + charlie, + sweeper, + initialAmounts, + transferProportion + ); + if (mintedStables == 0) return; + _updateOracles(latestOracleValue); + + fromToken = bound(fromToken, 0, _collaterals.length - 1); + stableAmount = bound(stableAmount, 0, collateralMintedStables[fromToken]); + if (stableAmount == 0) return; + upperFees = int64(bound(int256(upperFees), 0, int256((BASE_9 * 999) / 1000) - 1)); + uint64[] memory xFeeBurn = new uint64[](3); + xFeeBurn[0] = uint64(BASE_9); + xFeeBurn[1] = uint64((BASE_9 * 99) / 100); + xFeeBurn[2] = uint64(BASE_9 / 2); + int64[] memory yFeeBurn = new int64[](3); + yFeeBurn[0] = int64(0); + yFeeBurn[1] = int64(0); + yFeeBurn[2] = upperFees; + vm.prank(governor); + transmuter.setFees(_collaterals[fromToken], xFeeBurn, yFeeBurn, false); + + uint256 supposedAmountOut; + { + uint256 copyStableAmount = stableAmount; + uint256[] memory exposures = _getExposures(mintedStables, collateralMintedStables); + ( + uint256 amountFromPrevBreakpoint, + uint256 amountToNextBreakpoint, + uint256 lowerIndex + ) = _amountToPrevAndNextExposure( + mintedStables, + fromToken, + collateralMintedStables, + exposures[fromToken], + xFeeBurn + ); + // this is to handle in easy tests + if (lowerIndex == xFeeBurn.length - 1) return; + + if (lowerIndex == 0) { + if (copyStableAmount <= amountToNextBreakpoint) { + collateralMintedStables[fromToken] -= copyStableAmount; + mintedStables -= copyStableAmount; + // first burn segment are always constant fees + supposedAmountOut += (copyStableAmount * (BASE_9 - uint64(yFeeBurn[0]))) / BASE_9; + copyStableAmount = 0; + } else { + collateralMintedStables[fromToken] -= amountToNextBreakpoint; + mintedStables -= amountToNextBreakpoint; + // first burn segment are always constant fees + supposedAmountOut += (amountToNextBreakpoint * (BASE_9 - uint64(yFeeBurn[0]))) / BASE_9; + copyStableAmount -= amountToNextBreakpoint; + + exposures = _getExposures(mintedStables, collateralMintedStables); + (amountFromPrevBreakpoint, amountToNextBreakpoint, lowerIndex) = _amountToPrevAndNextExposure( + mintedStables, + fromToken, + collateralMintedStables, + exposures[fromToken], + xFeeBurn + ); + } + } + if (copyStableAmount > 0) { + if (copyStableAmount <= amountToNextBreakpoint) { + collateralMintedStables[fromToken] -= copyStableAmount; + int256 midFees; + { + int256 currentFees; + uint256 slope = (uint256(uint64(yFeeBurn[lowerIndex + 1] - yFeeBurn[lowerIndex])) * BASE_36) / + (amountToNextBreakpoint + amountFromPrevBreakpoint); + currentFees = yFeeBurn[lowerIndex] + int256((slope * amountFromPrevBreakpoint) / BASE_36); + int256 endFees = yFeeBurn[lowerIndex] + + int256((slope * (amountFromPrevBreakpoint + copyStableAmount)) / BASE_36); + midFees = (currentFees + endFees) / 2; + } + supposedAmountOut += (copyStableAmount * (BASE_9 - uint64(uint256(midFees)))) / BASE_9; + } else { + collateralMintedStables[fromToken] -= amountToNextBreakpoint; + { + int256 midFees; + { + uint256 slope = (uint256(uint64(yFeeBurn[lowerIndex + 1] - yFeeBurn[lowerIndex])) * + BASE_36) / (amountToNextBreakpoint + amountFromPrevBreakpoint); + int256 currentFees = yFeeBurn[lowerIndex] + + int256((slope * amountFromPrevBreakpoint) / BASE_36); + int256 endFees = yFeeBurn[lowerIndex + 1]; + midFees = (currentFees + endFees) / 2; + } + supposedAmountOut += (amountToNextBreakpoint * (BASE_9 - uint64(uint256(midFees)))) / BASE_9; + } + // next part is just with end fees + supposedAmountOut += + ((copyStableAmount - amountToNextBreakpoint) * (BASE_9 - uint64(yFeeBurn[lowerIndex + 1]))) / + BASE_9; + } + } + } + supposedAmountOut = _convertDecimalTo( + _getBurnOracle(supposedAmountOut, fromToken), + 18, + IERC20Metadata(_collaterals[fromToken]).decimals() + ); + if (supposedAmountOut > initialAmounts[fromToken]) vm.expectRevert(Errors.InvalidSwap.selector); + uint256 amountOut = transmuter.quoteIn(stableAmount, address(agToken), _collaterals[fromToken]); + if (amountOut == 0 || supposedAmountOut > initialAmounts[fromToken]) return; + + if (stableAmount > _minWallet) { + _assertApproxEqRelDecimalWithTolerance( + supposedAmountOut, + amountOut, + amountOut, + // precision of 0.01% + _MAX_PERCENTAGE_DEVIATION * 100, + 18 + ); + } + } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// INDEPENDENT PATH //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ @@ -1256,13 +1394,66 @@ contract BurnTest is Fixture, FunctionUtils { uint256 minDeviation = BASE_8; uint256 oracleValue; for (uint256 i; i < _oracles.length; i++) { + uint128 userFirewall; + uint128 burnRatioDeviation; + { + (, , , , bytes memory hyperparameters) = transmuter.getOracle(address(_collaterals[i])); + (userFirewall, burnRatioDeviation) = abi.decode(hyperparameters, (uint128, uint128)); + } (, int256 oracleValueTmp, , , ) = _oracles[i].latestRoundData(); - if (minDeviation > uint256(oracleValueTmp)) minDeviation = uint256(oracleValueTmp); - if (i == fromToken) oracleValue = uint256(oracleValueTmp); + if ( + BASE_8 * (BASE_18 - userFirewall) > uint256(oracleValueTmp) * BASE_18 && + uint256(oracleValueTmp) * BASE_18 < BASE_8 * (BASE_18 - burnRatioDeviation) && + minDeviation > uint256(oracleValueTmp) + ) minDeviation = uint256(oracleValueTmp); + if (i == fromToken) { + oracleValue = uint256(oracleValueTmp); + if ( + // We are in the user deviation tolerance + (BASE_8 * (BASE_18 - userFirewall) < oracleValue * BASE_18 && + oracleValue * BASE_18 < BASE_8 * (BASE_18 + userFirewall)) || + // Or we are in the burn deviation tolerance + (BASE_8 * (BASE_18 - burnRatioDeviation) <= oracleValue * BASE_18 && oracleValue <= BASE_8) + ) oracleValue = BASE_8; + } } return (amount * minDeviation) / oracleValue; } + function _updateOracleFirewalls(uint128[6] memory userAndBurnFirewall) internal returns (uint128[6] memory) { + uint128[] memory userFirewall = new uint128[](3); + uint128[] memory burnFirewall = new uint128[](3); + for (uint256 i; i < _collaterals.length; i++) { + userFirewall[i] = uint128(bound(userAndBurnFirewall[i], 0, BASE_18)); + burnFirewall[i] = uint128(bound(userAndBurnFirewall[i + 3], 0, BASE_18)); + userAndBurnFirewall[i] = userFirewall[i]; + userAndBurnFirewall[i + 3] = burnFirewall[i]; + } + + vm.startPrank(governor); + for (uint256 i; i < _collaterals.length; i++) { + ( + Storage.OracleReadType readType, + Storage.OracleReadType targetType, + bytes memory data, + bytes memory targetData, + + ) = transmuter.getOracle(address(_collaterals[i])); + transmuter.setOracle( + _collaterals[i], + abi.encode( + readType, + targetType, + data, + targetData, + abi.encode(uint128(userFirewall[i]), uint128(burnFirewall[i])) + ) + ); + } + vm.stopPrank(); + return userAndBurnFirewall; + } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ACTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ diff --git a/test/fuzz/MintTest.t.sol b/test/fuzz/MintTest.t.sol index 699dd8e1..23dc67ad 100644 --- a/test/fuzz/MintTest.t.sol +++ b/test/fuzz/MintTest.t.sol @@ -6,6 +6,7 @@ import { SafeERC20 } from "oz/token/ERC20/utils/SafeERC20.sol"; import { stdError } from "forge-std/Test.sol"; import "contracts/utils/Errors.sol" as Errors; +import "contracts/transmuter/Storage.sol" as Storage; import "../Fixture.sol"; import { IERC20Metadata } from "../mock/MockTokenPermit.sol"; @@ -464,6 +465,127 @@ contract MintTest is Fixture, FunctionUtils { } } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + FIREWALL + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testFuzz_QuoteMintExactOutput_WithFirewall_FixPiecewiseFees( + uint256[3] memory initialAmounts, + uint256 transferProportion, + uint256[3] memory latestOracleValue, + uint128[6] memory userAndBurnFirewall, + int64 upperFees, + uint256 stableAmount, + uint256 fromToken + ) public { + _updateOracleFirewalls(userAndBurnFirewall); + // let's first load the reserves of the protocol + (uint256 mintedStables, uint256[] memory collateralMintedStables) = _loadReserves( + charlie, + sweeper, + initialAmounts, + transferProportion + ); + if (mintedStables == 0) return; + _updateOracles(latestOracleValue); + + fromToken = bound(fromToken, 0, _collaterals.length - 1); + stableAmount = bound(stableAmount, 1, _maxAmountWithoutDecimals * BASE_18); + upperFees = int64(bound(int256(upperFees), 0, int256(BASE_12) - 1)); + int64[] memory yFeeMint = new int64[](2); + uint256 amountFromPrevBreakpoint; + uint256 amountToNextBreakpoint; + uint256 lowerIndex; + { + uint64[] memory xFeeMint = new uint64[](2); + xFeeMint[0] = uint64(0); + xFeeMint[1] = uint64(BASE_9 / 2); + yFeeMint[0] = int64(0); + yFeeMint[1] = upperFees; + vm.prank(governor); + transmuter.setFees(_collaterals[fromToken], xFeeMint, yFeeMint, true); + + { + uint256[] memory exposures = _getExposures(mintedStables, collateralMintedStables); + (amountFromPrevBreakpoint, amountToNextBreakpoint, lowerIndex) = _amountToPrevAndNextExposure( + mintedStables, + fromToken, + collateralMintedStables, + exposures[fromToken], + xFeeMint + ); + } + } + // this is to handle in easy tests + if (lowerIndex == type(uint256).max) return; + + uint256 supposedAmountIn; + if (stableAmount <= amountToNextBreakpoint) { + collateralMintedStables[fromToken] += stableAmount; + { + int256 midFees; + { + int256 currentFees; + uint256 slope = (uint256(uint64(yFeeMint[lowerIndex + 1] - yFeeMint[lowerIndex])) * BASE_36) / + (amountToNextBreakpoint + amountFromPrevBreakpoint); + currentFees = yFeeMint[lowerIndex] + int256((slope * amountFromPrevBreakpoint) / BASE_36); + int256 endFees = yFeeMint[lowerIndex] + + int256((slope * (amountFromPrevBreakpoint + stableAmount)) / BASE_36); + midFees = (currentFees + endFees) / 2; + } + supposedAmountIn = (stableAmount * (BASE_9 + uint256(midFees))); + } + uint256 mintOracleValue; + { + (, int256 oracleValue, , , ) = _oracles[fromToken].latestRoundData(); + mintOracleValue = _getMintOracle(oracleValue, fromToken, userAndBurnFirewall); + } + supposedAmountIn = _convertDecimalTo( + supposedAmountIn / (10 * mintOracleValue), + 18, + IERC20Metadata(_collaterals[fromToken]).decimals() + ); + } else { + collateralMintedStables[fromToken] += amountToNextBreakpoint; + { + int256 midFees; + { + uint256 slope = ((uint256(uint64(yFeeMint[lowerIndex + 1] - yFeeMint[lowerIndex])) * BASE_36) / + (amountToNextBreakpoint + amountFromPrevBreakpoint)); + int256 currentFees = yFeeMint[lowerIndex] + int256((slope * amountFromPrevBreakpoint) / BASE_36); + int256 endFees = yFeeMint[lowerIndex + 1]; + midFees = (currentFees + endFees) / 2; + } + supposedAmountIn = (amountToNextBreakpoint * (BASE_9 + uint256(midFees))); + } + // next part is just with end fees + supposedAmountIn += (stableAmount - amountToNextBreakpoint) * (BASE_9 + uint64(yFeeMint[lowerIndex + 1])); + uint256 mintOracleValue; + { + (, int256 oracleValue, , , ) = _oracles[fromToken].latestRoundData(); + mintOracleValue = _getMintOracle(oracleValue, fromToken, userAndBurnFirewall); + } + supposedAmountIn = _convertDecimalTo( + supposedAmountIn / (10 * mintOracleValue), + 18, + IERC20Metadata(_collaterals[fromToken]).decimals() + ); + } + + uint256 amountIn = transmuter.quoteOut(stableAmount, _collaterals[fromToken], address(agToken)); + + if (stableAmount > _minWallet) { + _assertApproxEqRelDecimalWithTolerance( + supposedAmountIn, + amountIn, + amountIn, + // precision of 0.1% + _MAX_PERCENTAGE_DEVIATION * 100, + 18 + ); + } + } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// INDEPENDANT PATH //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ @@ -744,4 +866,55 @@ contract MintTest is Fixture, FunctionUtils { } vm.stopPrank(); } + + function _getMintOracle( + int256 oracleValue, + uint256 fromToken, + uint128[6] memory userAndBurnFirewall + ) internal view returns (uint256 mintOracleValue) { + mintOracleValue = uint256(oracleValue); + if ( + BASE_8 * (BASE_18 - userAndBurnFirewall[fromToken]) < uint256(oracleValue) * BASE_18 && + BASE_8 * (BASE_18 + userAndBurnFirewall[fromToken]) > uint256(oracleValue) * BASE_18 + ) { + mintOracleValue = BASE_8; + } + if (BASE_8 < uint256(oracleValue)) { + mintOracleValue = BASE_8; + } + } + + function _updateOracleFirewalls(uint128[6] memory userAndBurnFirewall) internal returns (uint128[6] memory) { + uint128[] memory userFirewall = new uint128[](3); + uint128[] memory burnFirewall = new uint128[](3); + for (uint256 i; i < _collaterals.length; i++) { + userFirewall[i] = uint128(bound(userAndBurnFirewall[i], 0, BASE_18)); + burnFirewall[i] = uint128(bound(userAndBurnFirewall[i + 3], 0, BASE_18)); + userAndBurnFirewall[i] = userFirewall[i]; + userAndBurnFirewall[i + 3] = burnFirewall[i]; + } + + vm.startPrank(governor); + for (uint256 i; i < _collaterals.length; i++) { + ( + Storage.OracleReadType readType, + Storage.OracleReadType targetType, + bytes memory data, + bytes memory targetData, + + ) = transmuter.getOracle(address(_collaterals[i])); + transmuter.setOracle( + _collaterals[i], + abi.encode( + readType, + targetType, + data, + targetData, + abi.encode(uint128(userFirewall[i]), uint128(burnFirewall[i])) + ) + ); + } + vm.stopPrank(); + return userAndBurnFirewall; + } } diff --git a/test/fuzz/OracleTest.t.sol b/test/fuzz/OracleTest.t.sol index 9be81c5c..5adba20d 100644 --- a/test/fuzz/OracleTest.t.sol +++ b/test/fuzz/OracleTest.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.19; import { SafeERC20 } from "oz/token/ERC20/utils/SafeERC20.sol"; import { ITransmuterOracle, MockExternalOracle } from "../mock/MockExternalOracle.sol"; +import { MockMorphoOracle } from "../mock/MockMorphoOracle.sol"; import { MockPyth } from "../mock/MockPyth.sol"; import { IERC20Metadata } from "../mock/MockTokenPermit.sol"; import "../Fixture.sol"; @@ -21,6 +22,7 @@ contract OracleTest is Fixture, FunctionUtils { uint256 internal _maxAmountWithoutDecimals = 10 ** 15; uint256 internal _minOracleValue = 10 ** 3; // 10**(-5) + uint256 internal _maxOracleValue = BASE_18 / 100; uint256 internal _minWallet = 10 ** 18; // in base 18 uint256 internal _maxWallet = 10 ** (18 + 12); // in base 18 @@ -170,17 +172,28 @@ contract OracleTest is Fixture, FunctionUtils { uint8[3] memory newTargetType, uint256[4] memory latestExchangeRateStakeETH ) public { - for (uint i; i < _collaterals.length; i++) { + for (uint256 i; i < _collaterals.length; i++) { bytes memory oracleData; bytes memory targetData; { Storage.OracleReadType readType; Storage.OracleReadType targetType; - (readType, targetType, oracleData, targetData) = transmuter.getOracle(address(_collaterals[i])); + uint256 userFirewall; + uint256 burnFirewall; + { + bytes memory hyperparameters; + (readType, targetType, oracleData, targetData, hyperparameters) = transmuter.getOracle( + address(_collaterals[i]) + ); + (userFirewall, burnFirewall) = abi.decode(hyperparameters, (uint128, uint128)); + } - assertEq(uint(readType), uint(Storage.OracleReadType.CHAINLINK_FEEDS)); - assertEq(uint(targetType), uint(Storage.OracleReadType.STABLE)); + assertEq(uint256(readType), uint256(Storage.OracleReadType.CHAINLINK_FEEDS)); + assertEq(uint256(targetType), uint256(Storage.OracleReadType.STABLE)); + assertEq(userFirewall, 0); + assertEq(burnFirewall, 0); } + ( AggregatorV3Interface[] memory circuitChainlink, uint32[] memory stalePeriods, @@ -195,7 +208,7 @@ contract OracleTest is Fixture, FunctionUtils { assertEq(address(circuitChainlink[0]), address(_oracles[i])); assertEq(circuitChainIsMultiplied[0], 1); assertEq(chainlinkDecimals[0], 8); - assertEq(uint(quoteType), uint(Storage.OracleQuoteType.UNIT)); + assertEq(uint256(quoteType), uint256(Storage.OracleQuoteType.UNIT)); } _updateStakeETHExchangeRates(latestExchangeRateStakeETH); @@ -207,14 +220,14 @@ contract OracleTest is Fixture, FunctionUtils { newTargetType ); - for (uint i; i < _collaterals.length; i++) { + for (uint256 i; i < _collaterals.length; i++) { { bytes memory data; { bytes memory targetData; Storage.OracleReadType readType; Storage.OracleReadType targetType; - (readType, targetType, data, targetData) = transmuter.getOracle(address(_collaterals[i])); + (readType, targetType, data, targetData, ) = transmuter.getOracle(address(_collaterals[i])); assertEq(uint8(readType), newReadType[i]); assertEq(uint8(targetType), newTargetType[i]); @@ -246,7 +259,7 @@ contract OracleTest is Fixture, FunctionUtils { if (newTargetType[i] == 0) { bytes memory targetData; - (, , , targetData) = transmuter.getOracle(address(_collaterals[i])); + (, , , targetData, ) = transmuter.getOracle(address(_collaterals[i])); ( AggregatorV3Interface[] memory circuitChainlink, uint32[] memory stalePeriods, @@ -273,7 +286,7 @@ contract OracleTest is Fixture, FunctionUtils { READREDEMPTION //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - function testFuzz_OracleReadRedemptionSuccess( + function testFuzz_OracleReadRedemption_Success( uint8[3] memory newChainlinkDecimals, uint8[3] memory newCircuitChainIsMultiplied, uint8[3] memory newQuoteType, @@ -286,7 +299,7 @@ contract OracleTest is Fixture, FunctionUtils { _updateOracleValues(latestOracleValue); _updateOracles(newChainlinkDecimals, newCircuitChainIsMultiplied, newQuoteType, newReadType, newTargetType); - for (uint i; i < _collaterals.length; i++) { + for (uint256 i; i < _collaterals.length; i++) { (, , , , uint256 redemption) = transmuter.getOracleValues(address(_collaterals[i])); uint256 oracleRedemption; uint256 targetPrice; @@ -319,7 +332,7 @@ contract OracleTest is Fixture, FunctionUtils { READMINT //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - function testFuzz_OracleReadMintSuccess( + function testFuzz_OracleReadMint_Success( uint8[3] memory newChainlinkDecimals, uint8[3] memory newCircuitChainIsMultiplied, uint8[3] memory newQuoteType, @@ -332,7 +345,7 @@ contract OracleTest is Fixture, FunctionUtils { _updateOracleValues(latestOracleValue); _updateOracles(newChainlinkDecimals, newCircuitChainIsMultiplied, newQuoteType, newReadType, newTargetType); - for (uint i; i < _collaterals.length; i++) { + for (uint256 i; i < _collaterals.length; i++) { (uint256 mint, , , , ) = transmuter.getOracleValues(address(_collaterals[i])); uint256 oracleMint; uint256 targetPrice; @@ -367,7 +380,7 @@ contract OracleTest is Fixture, FunctionUtils { READBURN //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - function testFuzz_OracleReadBurnSuccess( + function testFuzz_OracleReadBurn_Success( uint8[3] memory newChainlinkDecimals, uint8[3] memory newCircuitChainIsMultiplied, uint8[3] memory newQuoteType, @@ -382,7 +395,7 @@ contract OracleTest is Fixture, FunctionUtils { uint256 minDeviation; uint256 minRatio; - for (uint i; i < _collaterals.length; i++) { + for (uint256 i; i < _collaterals.length; i++) { uint256 burn; uint256 deviation; (, burn, deviation, minRatio, ) = transmuter.getOracleValues(address(_collaterals[i])); @@ -470,10 +483,16 @@ contract OracleTest is Fixture, FunctionUtils { readData = abi.encode(address(pyth), feedIds, stalePeriods, isMultiplied, quoteType); } vm.expectRevert(Errors.InvalidRate.selector); - transmuter.setOracle(_collaterals[i], abi.encode(readType, targetType, readData, targetData)); + transmuter.setOracle( + _collaterals[i], + abi.encode(readType, targetType, readData, targetData, abi.encode(uint128(0), uint128(0))) + ); pyth.setParams(110000000, -8); - transmuter.setOracle(_collaterals[i], abi.encode(readType, targetType, readData, targetData)); + transmuter.setOracle( + _collaterals[i], + abi.encode(readType, targetType, readData, targetData, abi.encode(uint128(0), uint128(0))) + ); } if (i == 0) { (uint256 mint, uint256 burn, uint256 ratio, uint256 minRatio, uint256 redemption) = transmuter @@ -510,6 +529,424 @@ contract OracleTest is Fixture, FunctionUtils { vm.stopPrank(); } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + MORPHO + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testFuzz_ReadMorphoFeed(uint256[3] memory baseValues, uint256[3] memory normalizers) public { + vm.startPrank(governor); + for (uint256 i; i < _collaterals.length; i++) { + baseValues[i] = uint256(bound(baseValues[i], 100, 1e5)); + normalizers[i] = uint256(bound(normalizers[i], 1, 18)); + MockMorphoOracle morphoOracle = new MockMorphoOracle(baseValues[i] * 1e36); + { + Storage.OracleReadType readType = Storage.OracleReadType.MORPHO_ORACLE; + Storage.OracleReadType targetType = Storage.OracleReadType.MAX; + bytes memory readData = abi.encode(address(morphoOracle), 10 ** normalizers[i]); + bytes memory targetData = abi.encode( + (baseValues[i] * 1e36) / 10 ** normalizers[i], + uint96(block.timestamp), + 0, + 1 days + ); + transmuter.setOracle( + _collaterals[i], + abi.encode(readType, targetType, readData, targetData, abi.encode(uint128(0), uint128(0))) + ); + } + (uint256 mint, uint256 burn, uint256 ratio, uint256 minRatio, uint256 redemption) = transmuter + .getOracleValues(address(_collaterals[i])); + assertEq(mint, (baseValues[i] * 1e36) / 10 ** normalizers[i]); + assertEq(burn, (baseValues[i] * 1e36) / 10 ** normalizers[i]); + assertEq(ratio, BASE_18); + assertEq(minRatio, BASE_18); + assertEq(redemption, (baseValues[i] * 1e36) / 10 ** normalizers[i]); + if (i == 2) { + morphoOracle.setValue((baseValues[i] * 1e36 * 9) / 10); + (mint, burn, ratio, minRatio, redemption) = transmuter.getOracleValues(address(_collaterals[i])); + assertEq(mint, ((baseValues[i] * 1e36) * 9) / 10 ** normalizers[i] / 10); + assertEq(burn, ((baseValues[i] * 1e36) * 9) / 10 ** normalizers[i] / 10); + assertEq(ratio, (BASE_18 * 9) / 10); + assertEq(minRatio, (BASE_18 * 9) / 10); + assertEq(redemption, ((baseValues[i] * 1e36) * 9) / 10 ** normalizers[i] / 10); + } + } + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + FIREWALL + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testFuzz_OracleReadMint_WithFirewall_Success( + uint8[3] memory newChainlinkDecimals, + uint8[3] memory newCircuitChainIsMultiplied, + uint8[3] memory newQuoteType, + uint8[3] memory newReadType, + uint8[3] memory newTargetType, + uint128[6] memory userAndBurnFirewall, + uint256[3] memory latestOracleValue, + uint256[4] memory latestExchangeRateStakeETH + ) public { + _updateStakeETHExchangeRates(latestExchangeRateStakeETH); + _updateOracleValues(latestOracleValue); + _updateOracles(newChainlinkDecimals, newCircuitChainIsMultiplied, newQuoteType, newReadType, newTargetType); + userAndBurnFirewall = _updateOracleFirewalls(userAndBurnFirewall); + + for (uint256 i; i < _collaterals.length; i++) { + (uint256 mint, , , , ) = transmuter.getOracleValues(address(_collaterals[i])); + uint256 oracleMint; + uint256 targetPrice; + if (newTargetType[i] == 0) { + (, int256 value, , , ) = _oracles[i].latestRoundData(); + targetPrice = newCircuitChainIsMultiplied[i] == 1 + ? (BASE_18 * uint256(value)) / 10 ** (newChainlinkDecimals[i]) + : (BASE_18 * 10 ** (newChainlinkDecimals[i])) / uint256(value); + } else if (newTargetType[i] == 1 || newTargetType[i] == 2 || newTargetType[i] == 3) targetPrice = BASE_18; + else targetPrice = latestExchangeRateStakeETH[newTargetType[i] - 4]; + + uint256 quoteAmount = newQuoteType[i] == 0 ? BASE_18 : targetPrice; + + if (newReadType[i] == 0) { + (, int256 value, , , ) = _oracles[i].latestRoundData(); + oracleMint = newCircuitChainIsMultiplied[i] == 1 + ? (quoteAmount * uint256(value)) / 10 ** (newChainlinkDecimals[i]) + : (quoteAmount * 10 ** (newChainlinkDecimals[i])) / uint256(value); + } else if (newReadType[i] == 1) { + (, int256 value, , , ) = _oracles[i].latestRoundData(); + oracleMint = uint256(value) * 1e12; + } else if (newReadType[i] == 2) oracleMint = targetPrice; + else if (newReadType[i] == 3) oracleMint = BASE_18; + else oracleMint = latestExchangeRateStakeETH[newReadType[i] - 4]; + + if (newReadType[i] != 1) { + if ( + targetPrice * (BASE_18 - userAndBurnFirewall[i]) < oracleMint * BASE_18 && + targetPrice * (BASE_18 + userAndBurnFirewall[i]) > oracleMint * BASE_18 + ) oracleMint = targetPrice; + if (targetPrice < oracleMint) oracleMint = targetPrice; + } + assertEq(mint, oracleMint); + } + } + + function testFuzz_OracleReadBurn_WithFirewall_Success( + uint8[3] memory newCircuitChainIsMultiplied, + uint8[3] memory newQuoteType, + uint8[3] memory newReadType, + uint8[3] memory newTargetType, + uint128[6] memory userAndBurnFirewall, + uint256[3] memory latestOracleValue, + uint256[4] memory latestExchangeRateStakeETH + ) public { + _updateStakeETHExchangeRates(latestExchangeRateStakeETH); + _updateOracleValues(latestOracleValue); + { + uint8[3] memory newChainlinkDecimals = [8, 8, 8]; + _updateOracles(newChainlinkDecimals, newCircuitChainIsMultiplied, newQuoteType, newReadType, newTargetType); + } + userAndBurnFirewall = _updateOracleFirewalls(userAndBurnFirewall); + + uint256 minDeviation; + uint256 minRatio; + for (uint256 i; i < _collaterals.length; i++) { + uint256 burn; + uint256 deviation; + (, burn, deviation, minRatio, ) = transmuter.getOracleValues(address(_collaterals[i])); + if (i == 0) minDeviation = deviation; + if (deviation < minDeviation) minDeviation = deviation; + + uint256 targetPrice; + if (newTargetType[i] == 0) { + (, int256 value, , , ) = _oracles[i].latestRoundData(); + targetPrice = newCircuitChainIsMultiplied[i] == 1 + ? (BASE_18 * uint256(value)) / 10 ** 8 + : (BASE_18 * 10 ** 8) / uint256(value); + } else if (newTargetType[i] == 1 || newTargetType[i] == 2 || newTargetType[i] == 3) targetPrice = BASE_18; + else targetPrice = latestExchangeRateStakeETH[newTargetType[i] - 4]; + + uint256 oracleBurn; + if (newReadType[i] == 0) { + (, int256 value, , , ) = _oracles[i].latestRoundData(); + if (newQuoteType[i] == 0) { + if (newCircuitChainIsMultiplied[i] == 1) { + oracleBurn = (BASE_18 * uint256(value)) / 10 ** 8; + } else { + oracleBurn = (BASE_18 * 10 ** 8) / uint256(value); + } + } else { + if (newCircuitChainIsMultiplied[i] == 1) { + oracleBurn = (targetPrice * uint256(value)) / 10 ** 8; + } else { + oracleBurn = (targetPrice * 10 ** 8) / uint256(value); + } + } + } else if (newReadType[i] == 1) { + (, int256 value, , , ) = _oracles[i].latestRoundData(); + oracleBurn = uint256(value) * 1e12; + } else if (newReadType[i] == 2) oracleBurn = targetPrice; + else if (newReadType[i] == 3) oracleBurn = BASE_18; + else oracleBurn = latestExchangeRateStakeETH[newReadType[i] - 4]; + + { + uint256 oracleDeviation = BASE_18; + if (newReadType[i] != 1) { + if ( + targetPrice * (BASE_18 - userAndBurnFirewall[i]) < oracleBurn * BASE_18 && + targetPrice * (BASE_18 + userAndBurnFirewall[i]) > oracleBurn * BASE_18 + ) oracleBurn = targetPrice; + if (oracleBurn * BASE_18 < targetPrice * (BASE_18 - userAndBurnFirewall[i + 3])) + oracleDeviation = (oracleBurn * BASE_18) / targetPrice; + else if (oracleBurn < targetPrice) oracleBurn = targetPrice; + assertEq(deviation, oracleDeviation); + } + } + assertEq(burn, oracleBurn); + } + assertEq(minDeviation, minRatio); + } + + function testFuzz_Simple_ReadPythFeed_WithFirewalls(uint8[3] memory circuitIsMultiplied) public { + vm.startPrank(governor); + for (uint256 i; i < _collaterals.length; i++) { + MockPyth pyth = new MockPyth(); + int64 price; + int32 expo; + circuitIsMultiplied[i] = uint8(bound(circuitIsMultiplied[i], 0, 1)); + { + Storage.OracleReadType readType = Storage.OracleReadType.PYTH; + Storage.OracleReadType targetType = Storage.OracleReadType.STABLE; + Storage.OracleQuoteType quoteType = Storage.OracleQuoteType.UNIT; + bytes memory readData; + bytes memory targetData; + { + bytes32[] memory feedIds = new bytes32[](1); + uint32[] memory stalePeriods = new uint32[](1); + uint8[] memory isMultiplied = new uint8[](1); + feedIds[0] = bytes32(0); + stalePeriods[0] = 1 hours; + isMultiplied[0] = circuitIsMultiplied[i]; + readData = abi.encode(address(pyth), feedIds, stalePeriods, isMultiplied, quoteType); + } + + if (i == 0) pyth.setParams(110000000, -8); + else if (i == 1) pyth.setParams(9000000000, -10); + else if (i == 2) pyth.setParams(96000, -5); + { + bytes memory hyperParameters = abi.encode(uint128(0), uint128(0)); + if (i == 0) hyperParameters = abi.encode(uint128(0.05 ether), uint128(0.07 ether)); + else if (i == 1) hyperParameters = abi.encode(uint128(0.03 ether), uint128(0.099 ether)); + else if (i == 2) hyperParameters = abi.encode(uint128(0.5 ether), uint128(0.1 ether)); + transmuter.setOracle( + _collaterals[i], + abi.encode(readType, targetType, readData, targetData, hyperParameters) + ); + } + } + } + for (uint256 i; i < _collaterals.length; i++) { + (uint256 mint, uint256 burn, uint256 ratio, uint256 minRatio, uint256 redemption) = transmuter + .getOracleValues(address(_collaterals[i])); + if (i == 0) { + if (circuitIsMultiplied[i] == 0) { + assertEq(mint, (BASE_18 * 10) / 11); + assertEq(burn, (BASE_18 * 10) / 11); + assertEq(ratio, (BASE_18 * 10) / 11); + assertEq(redemption, (BASE_18 * 10) / 11); + } else { + assertEq(mint, BASE_18); + assertEq(burn, (BASE_18 * 11) / 10); + assertEq(ratio, BASE_18); + assertEq(redemption, (BASE_18 * 11) / 10); + } + } + if (i == 1) { + if (circuitIsMultiplied[i] == 0) { + assertEq(mint, BASE_18); + assertEq(burn, (BASE_18 * 10) / 9); + assertEq(ratio, BASE_18); + assertEq(redemption, (BASE_18 * 10) / 9); + } else { + assertEq(mint, (BASE_18 * 9) / 10); + assertEq(burn, (BASE_18 * 9) / 10); + assertEq(ratio, (BASE_18 * 9) / 10); + assertEq(redemption, (BASE_18 * 9) / 10); + } + } + if (i == 2) { + if (circuitIsMultiplied[i] == 0) { + assertEq(mint, BASE_18); + assertEq(burn, BASE_18); + assertEq(ratio, BASE_18); + assertEq(redemption, (BASE_18 * 100) / 96); + } else { + assertEq(mint, BASE_18); + assertEq(burn, BASE_18); + assertEq(ratio, BASE_18); + assertEq(redemption, (BASE_18 * 96) / 100); + } + } + } + vm.stopPrank(); + } + + function test_Simple_ReadPythFeed_WithFirewalls() public { + vm.startPrank(governor); + for (uint256 i; i < _collaterals.length; i++) { + MockPyth pyth = new MockPyth(); + int64 price; + int32 expo; + { + Storage.OracleReadType readType = Storage.OracleReadType.PYTH; + Storage.OracleReadType targetType = Storage.OracleReadType.STABLE; + Storage.OracleQuoteType quoteType = Storage.OracleQuoteType.UNIT; + bytes memory readData; + bytes memory targetData; + { + bytes32[] memory feedIds = new bytes32[](1); + uint32[] memory stalePeriods = new uint32[](1); + uint8[] memory isMultiplied = new uint8[](1); + feedIds[0] = bytes32(0); + stalePeriods[0] = 1 hours; + isMultiplied[0] = 1; + readData = abi.encode(address(pyth), feedIds, stalePeriods, isMultiplied, quoteType); + } + + if (i == 0) pyth.setParams(110000000, -8); + else if (i == 1) pyth.setParams(9000000000, -10); + else if (i == 2) pyth.setParams(96000, -5); + { + bytes memory hyperParameters = abi.encode(uint128(0), uint128(0)); + if (i == 0) hyperParameters = abi.encode(uint128(0.05 ether), uint128(0.07 ether)); + else if (i == 1) hyperParameters = abi.encode(uint128(0.03 ether), uint128(0.1 ether)); + else if (i == 2) hyperParameters = abi.encode(uint128(0.5 ether), uint128(0.1 ether)); + transmuter.setOracle( + _collaterals[i], + abi.encode(readType, targetType, readData, targetData, hyperParameters) + ); + } + if (i == 1) { + (uint256 mint, uint256 burn, uint256 ratio, uint256 minRatio, uint256 redemption) = transmuter + .getOracleValues(address(_collaterals[1])); + assertEq(mint, (BASE_18 * 9) / 10); + assertEq(burn, BASE_18); + assertEq(redemption, (BASE_18 * 9) / 10); + } + } + } + + (, , , uint256 minRatio, ) = transmuter.getOracleValues(address(_collaterals[0])); + assertEq(minRatio, BASE_18); + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + UPDATE ORACLE STORAGE + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testFuzz_revertWhen_updateOracle_NotAuthorized() public { + vm.startPrank(alice); + vm.expectRevert(Errors.NotTrusted.selector); + transmuter.updateOracle(_collaterals[0]); + } + + function testFuzz_revertWhen_updateOracle_NotACollateral(address fakeCollat) public { + for (uint256 i; i < _collaterals.length; i++) { + vm.assume(fakeCollat != _collaterals[i]); + } + vm.prank(governor); + transmuter.toggleTrusted(alice, Storage.TrustedType.Seller); + + vm.prank(alice); + vm.expectRevert(Errors.NotCollateral.selector); + transmuter.updateOracle(fakeCollat); + } + + function testFuzz_revertWhen_updateOracle_NotMax() public { + vm.prank(governor); + transmuter.toggleTrusted(alice, Storage.TrustedType.Seller); + + (, , , bytes memory targetData, ) = transmuter.getOracle(_collaterals[0]); + assertEq(targetData.length, 0); + + vm.prank(alice); + vm.expectRevert(Errors.OracleUpdateFailed.selector); + transmuter.updateOracle(_collaterals[0]); + } + + function testFuzz_revertWhen_updateOracle_NoUpdate() public { + vm.prank(governor); + transmuter.toggleTrusted(alice, Storage.TrustedType.Seller); + + address collateral = _collaterals[0]; + + ( + Storage.OracleReadType readType, + Storage.OracleReadType targetType, + bytes memory data, + bytes memory targetData, + bytes memory hyperparameters + ) = transmuter.getOracle(address(collateral)); + (uint256 oracleValue, , , , ) = transmuter.getOracleValues(collateral); + + vm.prank(governor); + transmuter.setOracle( + collateral, + abi.encode(readType, Storage.OracleReadType.MAX, data, abi.encode(oracleValue), hyperparameters) + ); + + vm.prank(alice); + vm.expectRevert(Errors.OracleUpdateFailed.selector); + transmuter.updateOracle(collateral); + } + + function testFuzz_updateOracle_Success(uint256 updateOracleValue, uint32 heartbeat) public { + vm.prank(governor); + transmuter.toggleTrusted(alice, Storage.TrustedType.Seller); + + uint256 indexCollat = 0; + address collateral = _collaterals[indexCollat]; + + { + (Storage.OracleReadType readType, , bytes memory data, , ) = transmuter.getOracle(address(collateral)); + (uint256 oracleValue, , , , ) = transmuter.getOracleValues(collateral); + + vm.prank(governor); + transmuter.setOracle( + collateral, + abi.encode( + readType, + Storage.OracleReadType.MAX, + data, + abi.encode(oracleValue), + abi.encode(uint128(0), uint128(0)) + ) + ); + } + + vm.warp(block.timestamp + heartbeat); + + // Update the oracles + uint256 newOracleValue; + { + (, int256 oracleValueTmp, , , ) = _oracles[indexCollat].latestRoundData(); + updateOracleValue = bound(updateOracleValue, uint256(oracleValueTmp) + 1, _maxOracleValue); + if (updateOracleValue > _maxOracleValue) return; + + uint256[3] memory latestOracleValue = [updateOracleValue, BASE_8, BASE_8]; + latestOracleValue = _updateOracleValues(latestOracleValue); + newOracleValue = latestOracleValue[indexCollat]; + } + + vm.prank(alice); + transmuter.updateOracle(collateral); + + (, , , bytes memory targetData, ) = transmuter.getOracle(address(collateral)); + uint256 maxValue = abi.decode(targetData, (uint256)); + + assertEq(maxValue, (newOracleValue * BASE_18) / BASE_8); + } + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// UTILS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ @@ -546,19 +983,21 @@ contract OracleTest is Fixture, FunctionUtils { } function _getReadType(uint8 newReadType) internal pure returns (Storage.OracleReadType readType) { - readType = newReadType == 0 ? Storage.OracleReadType.CHAINLINK_FEEDS : newReadType == 1 - ? Storage.OracleReadType.EXTERNAL - : newReadType == 2 - ? Storage.OracleReadType.NO_ORACLE - : newReadType == 3 - ? Storage.OracleReadType.STABLE - : newReadType == 4 - ? Storage.OracleReadType.WSTETH - : newReadType == 5 - ? Storage.OracleReadType.CBETH - : newReadType == 6 - ? Storage.OracleReadType.RETH - : Storage.OracleReadType.SFRXETH; + readType = newReadType == 0 + ? Storage.OracleReadType.CHAINLINK_FEEDS + : newReadType == 1 + ? Storage.OracleReadType.EXTERNAL + : newReadType == 2 + ? Storage.OracleReadType.NO_ORACLE + : newReadType == 3 + ? Storage.OracleReadType.STABLE + : newReadType == 4 + ? Storage.OracleReadType.WSTETH + : newReadType == 5 + ? Storage.OracleReadType.CBETH + : newReadType == 6 + ? Storage.OracleReadType.RETH + : Storage.OracleReadType.SFRXETH; } function _updateOracles( @@ -610,16 +1049,20 @@ contract OracleTest is Fixture, FunctionUtils { if (readType != Storage.OracleReadType.EXTERNAL) readData = data; if (targetType == Storage.OracleReadType.CHAINLINK_FEEDS) targetData = data; } - transmuter.setOracle(_collaterals[i], abi.encode(readType, targetType, readData, targetData)); + transmuter.setOracle( + _collaterals[i], + abi.encode(readType, targetType, readData, targetData, abi.encode(uint128(0), uint128(0))) + ); } vm.stopPrank(); } - function _updateOracleValues(uint256[3] memory latestOracleValue) internal { + function _updateOracleValues(uint256[3] memory latestOracleValue) internal returns (uint256[3] memory) { for (uint256 i; i < _collaterals.length; i++) { - latestOracleValue[i] = bound(latestOracleValue[i], _minOracleValue * 10, BASE_18 / 100); + latestOracleValue[i] = bound(latestOracleValue[i], _minOracleValue * 10, _maxOracleValue); MockChainlinkOracle(address(_oracles[i])).setLatestAnswer(int256(latestOracleValue[i])); } + return latestOracleValue; } function _updateStakeETHExchangeRates(uint256[4] memory latestExchangeRateStakeETH) internal { @@ -672,9 +1115,16 @@ contract OracleTest is Fixture, FunctionUtils { quoteType ); bytes memory targetData; + (, , , , bytes memory hyperparameters) = transmuter.getOracle(address(_collaterals[i])); transmuter.setOracle( _collaterals[i], - abi.encode(Storage.OracleReadType.CHAINLINK_FEEDS, Storage.OracleReadType.STABLE, readData, targetData) + abi.encode( + Storage.OracleReadType.CHAINLINK_FEEDS, + Storage.OracleReadType.STABLE, + readData, + targetData, + hyperparameters + ) ); } vm.stopPrank(); @@ -705,22 +1155,52 @@ contract OracleTest is Fixture, FunctionUtils { chainlinkDecimals, quoteType ); + (, , , , bytes memory hyperparameters) = transmuter.getOracle(address(_collaterals[i])); transmuter.setOracle( _collaterals[i], - abi.encode(Storage.OracleReadType.STABLE, Storage.OracleReadType.CHAINLINK_FEEDS, readData, targetData) + abi.encode( + Storage.OracleReadType.STABLE, + Storage.OracleReadType.CHAINLINK_FEEDS, + readData, + targetData, + hyperparameters + ) ); } vm.stopPrank(); } - function _getBurnOracle(uint256 amount, uint256 fromToken) internal view returns (uint256) { - uint256 minDeviation = BASE_8; - uint256 oracleValue; - for (uint256 i; i < _oracles.length; i++) { - (, int256 oracleValueTmp, , , ) = _oracles[i].latestRoundData(); - if (minDeviation > uint256(oracleValueTmp)) minDeviation = uint256(oracleValueTmp); - if (i == fromToken) oracleValue = uint256(oracleValueTmp); + function _updateOracleFirewalls(uint128[6] memory userAndBurnFirewall) internal returns (uint128[6] memory) { + uint128[] memory userFirewall = new uint128[](3); + uint128[] memory burnFirewall = new uint128[](3); + for (uint256 i; i < _collaterals.length; i++) { + userFirewall[i] = uint128(bound(userAndBurnFirewall[i], 0, BASE_18)); + burnFirewall[i] = uint128(bound(userAndBurnFirewall[i + 3], 0, BASE_18)); + userAndBurnFirewall[i] = userFirewall[i]; + userAndBurnFirewall[i + 3] = burnFirewall[i]; + } + + vm.startPrank(governor); + for (uint256 i; i < _collaterals.length; i++) { + ( + Storage.OracleReadType readType, + Storage.OracleReadType targetType, + bytes memory data, + bytes memory targetData, + + ) = transmuter.getOracle(address(_collaterals[i])); + transmuter.setOracle( + _collaterals[i], + abi.encode( + readType, + targetType, + data, + targetData, + abi.encode(uint128(userFirewall[i]), uint128(burnFirewall[i])) + ) + ); } - return (amount * minDeviation) / oracleValue; + vm.stopPrank(); + return userAndBurnFirewall; } } diff --git a/test/fuzz/RedeemTest.t.sol b/test/fuzz/RedeemTest.t.sol index 3e3c1a72..d3206ee7 100644 --- a/test/fuzz/RedeemTest.t.sol +++ b/test/fuzz/RedeemTest.t.sol @@ -522,13 +522,10 @@ contract RedeemTest is Fixture, FunctionUtils { function testFuzz_QuoteRedemptionCurveWithManagerRandomRedemptionFees( uint256[3] memory initialAmounts, - uint256[3] memory nbrSubCollaterals, - bool[3] memory isManaged, - uint256[3 * _MAX_SUB_COLLATERALS] memory airdropAmounts, uint256 transferProportion, - uint256[3 * _MAX_SUB_COLLATERALS] memory latestSubCollatOracleValue, - uint256[3 * _MAX_SUB_COLLATERALS] memory subCollatDecimals, - uint256[3] memory latestOracleValue, + uint256[3 * 2] memory nbrSubCollateralsAndIsManaged, + uint256[3 * (_MAX_SUB_COLLATERALS + 1)] memory airdropAmountsAndOracleValues, + uint256[3 * 2 * _MAX_SUB_COLLATERALS] memory latestSubCollatOracleValueAndDecimals, uint64[10] memory xFeeRedeemUnbounded, int64[10] memory yFeeRedeemUnbounded ) public { @@ -536,11 +533,10 @@ contract RedeemTest is Fixture, FunctionUtils { // Randomly set subcollaterals and manager if needed (IERC20[] memory subCollaterals, AggregatorV3Interface[] memory oracles) = _createManager( _collaterals[i], - nbrSubCollaterals[i], - isManaged[i], + nbrSubCollateralsAndIsManaged[2 * i], + nbrSubCollateralsAndIsManaged[2 * i + 1], i * _MAX_SUB_COLLATERALS, - latestSubCollatOracleValue, - subCollatDecimals + latestSubCollatOracleValueAndDecimals ); if (subCollaterals.length > 0) { _subCollaterals[_collaterals[i]] = SubCollateralStorage(subCollaterals, oracles); @@ -548,27 +544,31 @@ contract RedeemTest is Fixture, FunctionUtils { } // let's first load the reserves of the protocol - (uint256 mintedStables, uint256[] memory collateralMintedStables) = _loadReserves( - initialAmounts, - transferProportion - ); - - // airdrop amounts in the subcollaterals - for (uint256 i; i < _collaterals.length; ++i) { - if (_subCollaterals[_collaterals[i]].subCollaterals.length > 0) { - _loadSubCollaterals(address(_collaterals[i]), airdropAmounts, i * _MAX_SUB_COLLATERALS); - } - } + uint256 mintedStables; uint64 collatRatio; { - bool reverted; - (collatRatio, reverted) = _updateOraclesWithSubCollaterals( - latestOracleValue, - mintedStables, - collateralMintedStables, - airdropAmounts - ); - if (reverted) return; + uint256[] memory collateralMintedStables; + (mintedStables, collateralMintedStables) = _loadReserves(initialAmounts, transferProportion); + + // airdrop amounts in the subcollaterals + for (uint256 i; i < _collaterals.length; ++i) { + if (_subCollaterals[_collaterals[i]].subCollaterals.length > 0) { + _loadSubCollaterals( + address(_collaterals[i]), + airdropAmountsAndOracleValues, + i * _MAX_SUB_COLLATERALS + ); + } + } + + { + bool reverted; + (collatRatio, reverted) = _updateOraclesWithSubCollaterals( + abi.encode(mintedStables, collateralMintedStables), + airdropAmountsAndOracleValues + ); + if (reverted) return; + } } (uint64[] memory xFeeRedeem, int64[] memory yFeeRedeem) = _randomRedeemptionFees( @@ -578,31 +578,34 @@ contract RedeemTest is Fixture, FunctionUtils { vm.startPrank(alice); uint256 amountBurnt = agToken.balanceOf(alice); - address[] memory tokens; uint256[] memory amounts; { - bool shouldReturn; + address[] memory tokens; { - uint256 totalCollateralization = _computeCollateralisation(); - if ( - mintedStables > 0 && - (totalCollateralization.mulDiv(BASE_9, mintedStables, Math.Rounding.Up)) > type(uint64).max - ) { - vm.expectRevert(bytes("SafeCast: value doesn't fit in 64 bits")); - shouldReturn = true; - } else if (mintedStables == 0) { - vm.expectRevert(stdError.divisionError); + bool shouldReturn; + { + uint256 totalCollateralization = _computeCollateralisation(); + if ( + mintedStables > 0 && + (totalCollateralization.mulDiv(BASE_9, mintedStables, Math.Rounding.Up)) > type(uint64).max + ) { + vm.expectRevert(bytes("SafeCast: value doesn't fit in 64 bits")); + shouldReturn = true; + } else if (mintedStables == 0) { + vm.expectRevert(stdError.divisionError); + } } + (tokens, amounts) = transmuter.quoteRedemptionCurve(amountBurnt); + if (shouldReturn) return; } - (tokens, amounts) = transmuter.quoteRedemptionCurve(amountBurnt); - if (shouldReturn) return; - } - vm.stopPrank(); + vm.stopPrank(); - if (mintedStables == 0) return; + if (mintedStables == 0) return; + + // compute fee at current collatRatio + _assertSizesWithManager(tokens, amounts); + } - // compute fee at current collatRatio - _assertSizesWithManager(tokens, amounts); uint64 fee; if (collatRatio >= BASE_9) fee = uint64(yFeeRedeem[yFeeRedeem.length - 1]); else fee = uint64(LibHelpers.piecewiseLinear(collatRatio, xFeeRedeem, yFeeRedeem)); @@ -611,14 +614,10 @@ contract RedeemTest is Fixture, FunctionUtils { function testFuzz_MultiRedemptionCurveWithManagerRandomRedemptionFees( uint256[3] memory initialAmounts, - uint256[3] memory nbrSubCollaterals, - bool[3] memory isManaged, - uint256[3 * _MAX_SUB_COLLATERALS] memory airdropAmounts, - uint256[3 * _MAX_SUB_COLLATERALS] memory latestSubCollatOracleValue, - uint256[3 * _MAX_SUB_COLLATERALS] memory subCollatDecimals, - uint256 transferProportion, - uint256 redeemProportion, - uint256[3] memory latestOracleValue, + uint256[2] memory transferRedeemProportion, + uint256[3 * 2] memory nbrSubCollateralsAndIsManaged, + uint256[3 * (_MAX_SUB_COLLATERALS + 1)] memory airdropAmountsAndOracleValues, + uint256[3 * 2 * _MAX_SUB_COLLATERALS] memory latestSubCollatOracleValueAndDecimals, uint64[10] memory xFeeRedeemUnbounded, int64[10] memory yFeeRedeemUnbounded ) public { @@ -626,11 +625,10 @@ contract RedeemTest is Fixture, FunctionUtils { // Randomly set subcollaterals and manager if needed (IERC20[] memory subCollaterals, AggregatorV3Interface[] memory oracles) = _createManager( _collaterals[i], - nbrSubCollaterals[i], - isManaged[i], + nbrSubCollateralsAndIsManaged[2 * i], + nbrSubCollateralsAndIsManaged[2 * i + 1], i * _MAX_SUB_COLLATERALS, - latestSubCollatOracleValue, - subCollatDecimals + latestSubCollatOracleValueAndDecimals ); _subCollaterals[_collaterals[i]] = SubCollateralStorage(subCollaterals, oracles); } @@ -638,15 +636,18 @@ contract RedeemTest is Fixture, FunctionUtils { // let's first load the reserves of the protocol (uint256 mintedStables, uint256[] memory collateralMintedStables) = _loadReserves( initialAmounts, - transferProportion + transferRedeemProportion[0] ); // airdrop amounts in the subcollaterals for (uint256 i; i < _collaterals.length; ++i) { if (_subCollaterals[_collaterals[i]].subCollaterals.length > 0) { - _loadSubCollaterals(address(_collaterals[i]), airdropAmounts, i * _MAX_SUB_COLLATERALS); + _loadSubCollaterals(address(_collaterals[i]), airdropAmountsAndOracleValues, i * _MAX_SUB_COLLATERALS); } } - _updateOraclesWithSubCollaterals(latestOracleValue, mintedStables, collateralMintedStables, airdropAmounts); + _updateOraclesWithSubCollaterals( + abi.encode(mintedStables, collateralMintedStables), + airdropAmountsAndOracleValues + ); _randomRedeemptionFees(xFeeRedeemUnbounded, yFeeRedeemUnbounded); _sweepBalancesWithManager(alice, _collaterals); _sweepBalancesWithManager(bob, _collaterals); @@ -675,8 +676,8 @@ contract RedeemTest is Fixture, FunctionUtils { } if (mintedStables == 0) vm.expectRevert(stdError.divisionError); { - address[] memory tokens; uint256[] memory amounts; + address[] memory tokens; { uint256[] memory minAmountOuts = new uint256[](quoteAmounts.length); (tokens, amounts) = transmuter.redeem(amountBurnt, alice, block.timestamp + 1 days, minAmountOuts); @@ -708,8 +709,8 @@ contract RedeemTest is Fixture, FunctionUtils { // now do a second redeem to test with non trivial ts.normalizer and ts.normalizedStables vm.startPrank(bob); - redeemProportion = bound(redeemProportion, 0, BASE_9); - amountBurntBob = (agToken.balanceOf(bob) * redeemProportion) / BASE_9; + transferRedeemProportion[1] = bound(transferRedeemProportion[1], 0, BASE_9); + amountBurntBob = (agToken.balanceOf(bob) * transferRedeemProportion[1]) / BASE_9; { bool shouldReturn; { @@ -809,15 +810,12 @@ contract RedeemTest is Fixture, FunctionUtils { } function testFuzz_MultiForfeitRedemptionCurveWithManagerRandomRedemptionFees( - uint256[6] memory initialValue, // initialAmounts of size 3 / nbrSubCollaterals of size 3 - bool[3] memory isManaged, - uint256[3 * _MAX_SUB_COLLATERALS] memory airdropAmounts, - uint256[3 * _MAX_SUB_COLLATERALS] memory latestSubCollatOracleValue, - uint256[3 * _MAX_SUB_COLLATERALS] memory subCollatDecimals, + uint256[3] memory initialValue, // initialAmounts of size 3 / nbrSubCollaterals of size 3 + uint256[2] memory transferRedeemProportion, + uint256[3 * 2] memory nbrSubCollateralsAndIsManaged, + uint256[3 * (_MAX_SUB_COLLATERALS + 1)] memory airdropAmountsAndOracleValues, + uint256[3 * 2 * _MAX_SUB_COLLATERALS] memory latestSubCollatOracleValueAndDecimals, bool[3 * (_MAX_SUB_COLLATERALS + 1)] memory areForfeit, - uint256 transferProportion, - uint256 redeemProportion, - uint256[3] memory latestOracleValue, uint64[10] memory xFeeRedeemUnbounded, // X and Y arrays of length 10 each int64[10] memory yFeeRedeemUnbounded // X and Y arrays of length 10 each ) public { @@ -825,27 +823,29 @@ contract RedeemTest is Fixture, FunctionUtils { // Randomly set subcollaterals and manager if needed (IERC20[] memory subCollaterals, AggregatorV3Interface[] memory oracles) = _createManager( _collaterals[i], - initialValue[3 + i], - isManaged[i], + nbrSubCollateralsAndIsManaged[2 * i], + nbrSubCollateralsAndIsManaged[2 * i + 1], i * _MAX_SUB_COLLATERALS, - latestSubCollatOracleValue, - subCollatDecimals + latestSubCollatOracleValueAndDecimals ); _subCollaterals[_collaterals[i]] = SubCollateralStorage(subCollaterals, oracles); } // let's first load the reserves of the protocol (uint256 mintedStables, uint256[] memory collateralMintedStables) = _loadReserves( - [initialValue[0], initialValue[1], initialValue[2]], - transferProportion + initialValue, + transferRedeemProportion[0] ); // airdrop amounts in the subcollaterals for (uint256 i; i < _collaterals.length; ++i) { if (_subCollaterals[_collaterals[i]].subCollaterals.length > 0) { - _loadSubCollaterals(address(_collaterals[i]), airdropAmounts, i * _MAX_SUB_COLLATERALS); + _loadSubCollaterals(address(_collaterals[i]), airdropAmountsAndOracleValues, i * _MAX_SUB_COLLATERALS); } } - _updateOraclesWithSubCollaterals(latestOracleValue, mintedStables, collateralMintedStables, airdropAmounts); + _updateOraclesWithSubCollaterals( + abi.encode(mintedStables, collateralMintedStables), + airdropAmountsAndOracleValues + ); _randomRedeemptionFees(xFeeRedeemUnbounded, yFeeRedeemUnbounded); _sweepBalancesWithManager(alice, _collaterals); _sweepBalancesWithManager(bob, _collaterals); @@ -912,8 +912,8 @@ contract RedeemTest is Fixture, FunctionUtils { // now do a second redeem to test with non trivial ts.normalizer and ts.normalizedStables vm.startPrank(bob); - redeemProportion = bound(redeemProportion, 0, BASE_9); - amountBurntBob = (agToken.balanceOf(bob) * redeemProportion) / BASE_9; + transferRedeemProportion[1] = bound(transferRedeemProportion[1], 0, BASE_9); + amountBurntBob = (agToken.balanceOf(bob) * transferRedeemProportion[1]) / BASE_9; { bool shouldReturn; uint256 totalCollateralization = _computeCollateralisation(); @@ -1293,25 +1293,30 @@ contract RedeemTest is Fixture, FunctionUtils { vm.expectRevert(bytes("SafeCast: value doesn't fit in 64 bits")); } } - (uint256 collatRatio, uint256 mintedStables) = transmuter.getCollateralRatio(); - if (reverted) return; - for (uint256 i; i < _oracles.length; ++i) { - IERC20[] memory listSubCollaterals = _subCollaterals[_collaterals[i]].subCollaterals; - for (uint256 k = 0; k < listSubCollaterals.length; ++k) { - uint256 expect; - uint256 subCollateralBalance; - if (address(_managers[_collaterals[i]]) == address(0)) { - subCollateralBalance = listSubCollaterals[k].balanceOf(address(transmuter)); - } else { - subCollateralBalance = listSubCollaterals[k].balanceOf(address(_managers[_collaterals[i]])); - } - if (collatRatio < BASE_9) { - expect = (subCollateralBalance * amountBurnt * fee) / (mintedStables * BASE_9); - } else { - expect = (subCollateralBalance * amountBurnt * fee) / (mintedStables * collatRatio); + uint256 collatRatio; + { + uint256 mintedStables; + (collatRatio, mintedStables) = transmuter.getCollateralRatio(); + if (reverted) return; + + for (uint256 i; i < _oracles.length; ++i) { + IERC20[] memory listSubCollaterals = _subCollaterals[_collaterals[i]].subCollaterals; + for (uint256 k = 0; k < listSubCollaterals.length; ++k) { + uint256 expect; + uint256 subCollateralBalance; + if (address(_managers[_collaterals[i]]) == address(0)) { + subCollateralBalance = listSubCollaterals[k].balanceOf(address(transmuter)); + } else { + subCollateralBalance = listSubCollaterals[k].balanceOf(address(_managers[_collaterals[i]])); + } + if (collatRatio < BASE_9) { + expect = (subCollateralBalance * amountBurnt * fee) / (mintedStables * BASE_9); + } else { + expect = (subCollateralBalance * amountBurnt * fee) / (mintedStables * collatRatio); + } + assertEq(amounts[count2++], expect); } - assertEq(amounts[count2++], expect); } } uint256 valueCheck = (amountBurnt * fee) / BASE_9; @@ -1359,7 +1364,8 @@ contract RedeemTest is Fixture, FunctionUtils { function _loadSubCollaterals( address collateral, - uint256[3 * _MAX_SUB_COLLATERALS] memory airdropAmounts, + // The last 3 values should be disregarded as they are oracle values + uint256[3 * (_MAX_SUB_COLLATERALS + 1)] memory airdropAmounts, uint256 startIndex ) internal { IERC20[] memory listSubCollaterals = _subCollaterals[collateral].subCollaterals; @@ -1415,20 +1421,30 @@ contract RedeemTest is Fixture, FunctionUtils { } function _updateOraclesWithSubCollaterals( - uint256[3] memory latestOracleValue, - uint256 mintedStables, - uint256[] memory collateralMintedStables, - uint256[3 * _MAX_SUB_COLLATERALS] memory airdropAmounts + bytes memory collateralStablesAndMintedStables, + uint256[3 * (_MAX_SUB_COLLATERALS + 1)] memory airdropAmountsAndOracleValues ) internal returns (uint64 collatRatio, bool reverted) { uint256 collateralisation; - for (uint256 i; i < latestOracleValue.length; ++i) { - latestOracleValue[i] = bound(latestOracleValue[i], _minOracleValue, BASE_18); - MockChainlinkOracle(address(_oracles[i])).setLatestAnswer(int256(latestOracleValue[i])); + for (uint256 i; i < airdropAmountsAndOracleValues.length / (_MAX_SUB_COLLATERALS + 1); ++i) { + airdropAmountsAndOracleValues[3 * _MAX_SUB_COLLATERALS + i] = bound( + airdropAmountsAndOracleValues[3 * _MAX_SUB_COLLATERALS + i], + _minOracleValue, + BASE_18 + ); + MockChainlinkOracle(address(_oracles[i])).setLatestAnswer( + int256(airdropAmountsAndOracleValues[3 * _MAX_SUB_COLLATERALS + i]) + ); - IERC20[] memory listSubCollaterals = _subCollaterals[_collaterals[i]].subCollaterals; - if (listSubCollaterals.length <= 1) { - collateralisation += (latestOracleValue[i] * collateralMintedStables[i]) / BASE_8; + if (_subCollaterals[_collaterals[i]].subCollaterals.length <= 1) { + (, uint256[] memory collateralMintedStables) = abi.decode( + collateralStablesAndMintedStables, + (uint256, uint256[]) + ); + collateralisation += + (airdropAmountsAndOracleValues[3 * _MAX_SUB_COLLATERALS + i] * collateralMintedStables[i]) / + BASE_8; } else { + IERC20[] memory listSubCollaterals = _subCollaterals[_collaterals[i]].subCollaterals; // we don't double count the real collaterals uint256 subCollateralValue = IERC20Metadata(address(listSubCollaterals[0])).balanceOf( address(_managers[_collaterals[i]]) @@ -1440,14 +1456,14 @@ contract RedeemTest is Fixture, FunctionUtils { subCollateralValue += (uint256(oracleValue) * _convertDecimalTo( - airdropAmounts[i * _MAX_SUB_COLLATERALS + k - 1], + airdropAmountsAndOracleValues[i * _MAX_SUB_COLLATERALS + k - 1], IERC20Metadata(address(listSubCollaterals[k])).decimals(), IERC20Metadata(address(listSubCollaterals[0])).decimals() )) / BASE_8; } collateralisation += - (((BASE_18 * latestOracleValue[i]) / BASE_8) * + (((BASE_18 * airdropAmountsAndOracleValues[3 * _MAX_SUB_COLLATERALS + i]) / BASE_8) * _convertDecimalTo( subCollateralValue, IERC20Metadata(address(listSubCollaterals[0])).decimals(), @@ -1457,14 +1473,17 @@ contract RedeemTest is Fixture, FunctionUtils { } } + (uint256 mintedStables, ) = abi.decode(collateralStablesAndMintedStables, (uint256, uint256[])); { - uint256 trueMintedStables = transmuter.getTotalIssued(); - if ( - trueMintedStables > 0 && - _computeCollateralisation().mulDiv(BASE_9, trueMintedStables, Math.Rounding.Up) > type(uint64).max - ) { - reverted = true; - vm.expectRevert(bytes("SafeCast: value doesn't fit in 64 bits")); + { + uint256 trueMintedStables = transmuter.getTotalIssued(); + if ( + trueMintedStables > 0 && + _computeCollateralisation().mulDiv(BASE_9, trueMintedStables, Math.Rounding.Up) > type(uint64).max + ) { + reverted = true; + vm.expectRevert(bytes("SafeCast: value doesn't fit in 64 bits")); + } } uint256 stablecoinsIssued; (collatRatio, stablecoinsIssued) = transmuter.getCollateralRatio(); @@ -1545,17 +1564,18 @@ contract RedeemTest is Fixture, FunctionUtils { function _createManager( address token, uint256 nbrSubCollaterals, - bool isManaged, + uint256 isManaged, uint256 startIndex, - uint256[3 * _MAX_SUB_COLLATERALS] memory subCollateralOracleValue, - uint256[3 * _MAX_SUB_COLLATERALS] memory subCollateralsDecimals + // The first 3 * _MAX_SUB_COLLATERALS are for the oracle values and the rest are for the decimals + uint256[2 * 3 * _MAX_SUB_COLLATERALS] memory subCollateralOracleValueAndDecimals ) internal returns (IERC20[] memory subCollaterals, AggregatorV3Interface[] memory oracles) { nbrSubCollaterals = bound(nbrSubCollaterals, 0, _MAX_SUB_COLLATERALS); + isManaged = bound(nbrSubCollaterals, 0, 1); subCollaterals = new IERC20[](nbrSubCollaterals + 1); oracles = new AggregatorV3Interface[](nbrSubCollaterals); subCollaterals[0] = IERC20(token); - if (nbrSubCollaterals == 0 && isManaged) return (subCollaterals, oracles); + if (nbrSubCollaterals == 0 && isManaged == 1) return (subCollaterals, oracles); MockManager manager = new MockManager(token); { uint8[] memory decimals = new uint8[](nbrSubCollaterals + 1); @@ -1564,7 +1584,9 @@ contract RedeemTest is Fixture, FunctionUtils { uint8[] memory oracleIsMultiplied = new uint8[](nbrSubCollaterals); uint8[] memory chainlinkDecimals = new uint8[](nbrSubCollaterals); for (uint256 i = 1; i < nbrSubCollaterals + 1; ++i) { - decimals[i] = uint8(bound(subCollateralsDecimals[startIndex + i - 1], 5, 18)); + decimals[i] = uint8( + bound(subCollateralOracleValueAndDecimals[3 * _MAX_SUB_COLLATERALS + startIndex + i - 1], 5, 18) + ); subCollaterals[i] = IERC20( address( new MockTokenPermit( @@ -1575,13 +1597,13 @@ contract RedeemTest is Fixture, FunctionUtils { ) ); oracles[i - 1] = AggregatorV3Interface(address(new MockChainlinkOracle())); - subCollateralOracleValue[startIndex + i - 1] = bound( - subCollateralOracleValue[startIndex + i - 1], + subCollateralOracleValueAndDecimals[startIndex + i - 1] = bound( + subCollateralOracleValueAndDecimals[startIndex + i - 1], _minOracleValue, BASE_18 ); MockChainlinkOracle(address(oracles[i - 1])).setLatestAnswer( - int256(subCollateralOracleValue[startIndex + i - 1]) + int256(subCollateralOracleValueAndDecimals[startIndex + i - 1]) ); stalePeriods[i - 1] = 365 days; oracleIsMultiplied[i - 1] = 1; diff --git a/test/fuzz/SavingsTest.t.sol b/test/fuzz/SavingsTest.t.sol index 169669ab..1097f0c8 100644 --- a/test/fuzz/SavingsTest.t.sol +++ b/test/fuzz/SavingsTest.t.sol @@ -288,7 +288,9 @@ contract SavingsTest is Fixture, FunctionUtils { uint256 indexReceiver, uint256[2] memory elapseTimestamps ) public { - for (uint256 i; i < amounts.length; i++) amounts[i] = bound(amounts[i], 0, _maxAmount); + for (uint256 i; i < amounts.length; i++) { + amounts[i] = bound(amounts[i], 0, _maxAmount); + } rate = bound(rate, _minRate, _maxRate); // shorten the time otherwise the DL diverge too much from the actual formula (1+rate)**seconds elapseTimestamps[0] = bound(elapseTimestamps[0], 0, _maxElapseTime); @@ -473,16 +475,13 @@ contract SavingsTest is Fixture, FunctionUtils { assertEq(_saving.balanceOf(receiver), shares); } - function testFuzz_MintNonNullRate( - uint256[2] memory shares, - uint256 rate, - uint256 indexReceiver, - uint256[2] memory elapseTimestamps - ) public { - for (uint256 i; i < shares.length; i++) { + function testFuzz_MintNonNullRate(uint256[4] memory shares, uint256[2] memory elapseTimestamps) public { + // shares[2] rate + // shares[3] indexReceiver + for (uint256 i; i < 2; i++) { shares[i] = bound(shares[i], 0, _maxAmount); } - rate = bound(rate, _minRate, _maxRate); + shares[2] = bound(shares[2], _minRate, _maxRate); // shorten the time otherwise the DL diverge too much from the actual formula (1+rate)**seconds elapseTimestamps[0] = bound(elapseTimestamps[0], 0, _maxElapseTime); elapseTimestamps[1] = bound(elapseTimestamps[1], 0, _maxElapseTime); @@ -490,21 +489,20 @@ contract SavingsTest is Fixture, FunctionUtils { _deposit(shares[0], sweeper, sweeper, 0); vm.prank(governor); - _saving.setRate(uint208(rate)); + _saving.setRate(uint208(shares[2])); // first time elapse skip(elapseTimestamps[0]); uint256 compoundAssets = ((shares[0] + _initDeposit) * - unwrap(powu(ud(BASE_18 + rate / BASE_9), elapseTimestamps[0]))) / + unwrap(powu(ud(BASE_18 + shares[2] / BASE_9), elapseTimestamps[0]))) / unwrap(powu(ud(BASE_18), elapseTimestamps[0])); - address receiver; uint256 returnAmount; { uint256 prevShares = _saving.totalSupply(); uint256 balanceAsset = _saving.totalAssets(); uint256 supposedAmount = _saving.previewMint(shares[1]); - (returnAmount, , receiver) = _mint(shares[1], supposedAmount, alice, address(0), indexReceiver); + (returnAmount, , receiver) = _mint(shares[1], supposedAmount, alice, address(0), shares[3]); uint256 expectedAmount = (shares[1] * balanceAsset) / prevShares; assertEq(shares[1], _saving.balanceOf(receiver)); assertApproxEqAbs(returnAmount, expectedAmount, 1 wei); @@ -515,7 +513,7 @@ contract SavingsTest is Fixture, FunctionUtils { skip(elapseTimestamps[1]); { - uint256 increasedRate = (BASE_18 * unwrap(powu(ud(BASE_18 + rate / BASE_9), elapseTimestamps[1]))) / + uint256 increasedRate = (BASE_18 * unwrap(powu(ud(BASE_18 + shares[2] / BASE_9), elapseTimestamps[1]))) / unwrap(powu(ud(BASE_18), elapseTimestamps[1])); uint256 newCompoundAssets = (((compoundAssets + returnAmount) * increasedRate) / BASE_18); @@ -633,8 +631,8 @@ contract SavingsTest is Fixture, FunctionUtils { } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - REDEEM - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + REDEEM + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ function testFuzz_RedeemSuccess( uint256[2] memory amounts, @@ -707,29 +705,26 @@ contract SavingsTest is Fixture, FunctionUtils { } /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - WITHDRAW + WITHDRAW //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ - function testFuzz_MaxWithdrawSuccess( - uint256[2] memory amounts, - uint256 rate, - uint256 indexReceiver, - uint256[2] memory elapseTimestamps - ) public { - for (uint256 i; i < amounts.length; i++) { + function testFuzz_MaxWithdrawSuccess(uint256[4] memory amounts, uint256[2] memory elapseTimestamps) public { + // amounts[2] rate + // amounts[3] indexReceiver + for (uint256 i; i < 2; i++) { amounts[i] = bound(amounts[i], 0, _maxAmount); } // shorten the time otherwise the DL diverge too much from the actual formula (1+rate)**seconds for (uint256 i; i < elapseTimestamps.length; i++) { elapseTimestamps[i] = bound(elapseTimestamps[i], 0, _maxElapseTime); } - rate = bound(rate, _minRate, _maxRate); - address receiver = actors[bound(indexReceiver, 0, _nbrActor - 1)]; + amounts[2] = bound(amounts[2], _minRate, _maxRate); + address receiver = actors[bound(amounts[3], 0, _nbrActor - 1)]; _deposit(amounts[0], sweeper, sweeper, 0); vm.prank(governor); - _saving.setRate(uint208(rate)); + _saving.setRate(uint208(amounts[2])); // first time elapse skip(elapseTimestamps[0]); diff --git a/test/fuzz/SwapTest.t.sol b/test/fuzz/SwapTest.t.sol index 48c274a4..1715d656 100644 --- a/test/fuzz/SwapTest.t.sol +++ b/test/fuzz/SwapTest.t.sol @@ -149,6 +149,29 @@ contract SwapTest is Fixture, FunctionUtils { transmuter.swapExactOutput(amount, 0, _collaterals[fromToken], address(agToken), alice, block.timestamp * 2); vm.expectRevert(Errors.Paused.selector); transmuter.swapExactInput(amount, 0, _collaterals[fromToken], address(agToken), alice, block.timestamp * 2); + vm.stopPrank(); + } + + function test_RevertWhen_Paused2( + uint256[3] memory initialAmounts, + uint256 transferProportion, + uint256[3] memory latestOracleValue, + uint256 amount, + uint256 fromToken + ) public { + fromToken = bound(fromToken, 0, _collaterals.length - 1); + amount = bound(amount, 2, _maxAmountWithoutDecimals * 10 ** 18); + // let's first load the reserves of the protocol + (uint256 mintedStables, ) = _loadReserves(charlie, sweeper, initialAmounts, transferProportion); + if (mintedStables == 0) return; + _updateOracles(latestOracleValue); + + vm.startPrank(governor); + transmuter.togglePause(_collaterals[fromToken], Storage.ActionType.Mint); + transmuter.togglePause(_collaterals[fromToken], Storage.ActionType.Burn); + vm.stopPrank(); + + vm.startPrank(alice); vm.expectRevert(Errors.Paused.selector); transmuter.swapExactOutput(amount, 0, address(agToken), _collaterals[fromToken], alice, block.timestamp * 2); vm.expectRevert(Errors.Paused.selector); @@ -358,4 +381,50 @@ contract SwapTest is Fixture, FunctionUtils { MockChainlinkOracle(address(_oracles[i])).setLatestAnswer(int256(latestOracleValue[i])); } } + + function _updateOracleFirewalls(uint128[6] memory userAndBurnFirewall) internal returns (uint128[6] memory) { + uint128[] memory userFirewall = new uint128[](3); + uint128[] memory burnFirewall = new uint128[](3); + for (uint256 i; i < _collaterals.length; i++) { + userFirewall[i] = uint128(bound(userAndBurnFirewall[i], 0, BASE_18)); + burnFirewall[i] = uint128(bound(userAndBurnFirewall[i + 3], 0, BASE_18)); + userAndBurnFirewall[i] = userFirewall[i]; + userAndBurnFirewall[i + 3] = burnFirewall[i]; + } + + vm.startPrank(governor); + for (uint256 i; i < _collaterals.length; i++) { + ( + Storage.OracleReadType readType, + Storage.OracleReadType targetType, + bytes memory data, + bytes memory targetData, + + ) = transmuter.getOracle(address(_collaterals[i])); + transmuter.setOracle( + _collaterals[i], + abi.encode( + readType, + targetType, + data, + targetData, + abi.encode(uint128(userFirewall[i]), uint128(burnFirewall[i])) + ) + ); + } + vm.stopPrank(); + return userAndBurnFirewall; + } + + function _userOracleProtection( + uint256 targetPrice, + uint256 oracleValue, + uint256 deviation + ) private pure returns (uint256) { + if ( + targetPrice * (BASE_18 - deviation) < oracleValue * BASE_18 && + oracleValue * BASE_18 < targetPrice * (BASE_18 + deviation) + ) oracleValue = targetPrice; + return oracleValue; + } } diff --git a/test/invariants/actors/TraderWithSplit.t.sol b/test/invariants/actors/TraderWithSplit.t.sol index 5493204d..22daeaaa 100644 --- a/test/invariants/actors/TraderWithSplit.t.sol +++ b/test/invariants/actors/TraderWithSplit.t.sol @@ -106,7 +106,7 @@ contract TraderWithSplit is BaseActor { // Approval only usefull for QuoteType.MintExactInput and QuoteType.MintExactOutput IERC20(testS.tokenIn).approve(address(_transmuter), testS.amountIn); - IERC20(testS.tokenIn).approve(address(_transmuterSplit), testS.amountIn); + IERC20(testS.tokenIn).approve(address(_transmuterSplit), testS.amountIn + 1); // Swap if (quoteType == QuoteType.MintExactInput || quoteType == QuoteType.BurnExactInput) { diff --git a/test/mock/MockExternalOracle.sol b/test/mock/MockExternalOracle.sol index 940aa1bf..4ec9acc6 100644 --- a/test/mock/MockExternalOracle.sol +++ b/test/mock/MockExternalOracle.sol @@ -26,4 +26,9 @@ contract MockExternalOracle is ITransmuterOracle { (, int256 ratio, , , ) = feed.latestRoundData(); return (uint256(ratio) * 1e12, 1e18); } + + function read() external view returns (uint256) { + (, int256 ratio, , , ) = feed.latestRoundData(); + return uint256(ratio) * 1e12; + } } diff --git a/test/mock/MockMorphoOracle.sol b/test/mock/MockMorphoOracle.sol new file mode 100644 index 00000000..0c3f1d84 --- /dev/null +++ b/test/mock/MockMorphoOracle.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.19; + +import { IMorphoOracle } from "contracts/interfaces/external/morpho/IMorphoOracle.sol"; + +contract MockMorphoOracle is IMorphoOracle { + uint256 value; + + constructor(uint256 _value) { + value = _value; + } + + function price() external view returns (uint256) { + return value; + } + + function setValue(uint256 _value) external { + value = _value; + } +} diff --git a/test/scripts/UpdateTransmuterFacets.t.sol b/test/scripts/UpdateTransmuterFacets.t.sol new file mode 100644 index 00000000..2a86748d --- /dev/null +++ b/test/scripts/UpdateTransmuterFacets.t.sol @@ -0,0 +1,890 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import { stdJson } from "forge-std/StdJson.sol"; +import { console } from "forge-std/console.sol"; +import { Test } from "forge-std/Test.sol"; + +import "../../scripts/Constants.s.sol"; + +import { Helpers } from "../../scripts/Helpers.s.sol"; +import "contracts/utils/Errors.sol" as Errors; +import "contracts/transmuter/Storage.sol" as Storage; +import { Getters } from "contracts/transmuter/facets/Getters.sol"; +import { Redeemer } from "contracts/transmuter/facets/Redeemer.sol"; +import { SettersGovernor } from "contracts/transmuter/facets/SettersGovernor.sol"; +import { SettersGuardian } from "contracts/transmuter/facets/SettersGuardian.sol"; +import { Swapper } from "contracts/transmuter/facets/Swapper.sol"; +import "contracts/transmuter/libraries/LibHelpers.sol"; +import { CollateralSetupProd } from "contracts/transmuter/configs/ProductionTypes.sol"; +import "interfaces/external/chainlink/AggregatorV3Interface.sol"; +import { ITransmuter } from "interfaces/ITransmuter.sol"; +import "utils/src/Constants.sol"; +import { IERC20 } from "oz/interfaces/IERC20.sol"; + +interface OldTransmuter { + function getOracle( + address + ) external view returns (Storage.OracleReadType, Storage.OracleReadType, bytes memory, bytes memory); +} + +contract UpdateTransmuterFacetsTest is Helpers, Test { + using stdJson for string; + + uint256 public CHAIN_SOURCE; + + address constant WHALE_AGEUR = 0x4Fa745FCCC04555F2AFA8874cd23961636CdF982; + + string[] replaceFacetNames; + string[] addFacetNames; + address[] facetAddressList; + address[] addFacetAddressList; + + ITransmuter transmuter; + IERC20 agEUR; + address governor; + bytes public oracleConfigEUROC; + bytes public oracleConfigBC3M; + bytes public oracleConfigBERNX; + + function setUp() public override { + super.setUp(); + + CHAIN_SOURCE = CHAIN_ETHEREUM; + + ethereumFork = vm.createFork(vm.envString("ETH_NODE_URI_MAINNET"), 19425035); + vm.selectFork(forkIdentifier[CHAIN_SOURCE]); + + governor = _chainToContract(CHAIN_SOURCE, ContractType.Timelock); + transmuter = ITransmuter(_chainToContract(CHAIN_SOURCE, ContractType.TransmuterAgEUR)); + agEUR = IERC20(_chainToContract(CHAIN_SOURCE, ContractType.AgEUR)); + + // First update the facets implemantations + + Storage.FacetCut[] memory replaceCut; + Storage.FacetCut[] memory addCut; + + replaceFacetNames.push("Getters"); + facetAddressList.push(address(new Getters())); + console.log("Getters deployed at: ", facetAddressList[facetAddressList.length - 1]); + + replaceFacetNames.push("Redeemer"); + facetAddressList.push(address(new Redeemer())); + console.log("Redeemer deployed at: ", facetAddressList[facetAddressList.length - 1]); + + replaceFacetNames.push("SettersGovernor"); + address settersGovernor = address(new SettersGovernor()); + facetAddressList.push(settersGovernor); + console.log("SettersGovernor deployed at: ", facetAddressList[facetAddressList.length - 1]); + + replaceFacetNames.push("Swapper"); + facetAddressList.push(address(new Swapper())); + console.log("Swapper deployed at: ", facetAddressList[facetAddressList.length - 1]); + + addFacetNames.push("SettersGovernor"); + addFacetAddressList.push(settersGovernor); + + string memory jsonReplace = vm.readFile(JSON_SELECTOR_PATH_REPLACE); + { + // Build appropriate payload + uint256 n = replaceFacetNames.length; + replaceCut = new Storage.FacetCut[](n); + for (uint256 i = 0; i < n; ++i) { + // Get Selectors from json + bytes4[] memory selectors = _arrayBytes32ToBytes4( + jsonReplace.readBytes32Array(string.concat("$.", replaceFacetNames[i])) + ); + + replaceCut[i] = Storage.FacetCut({ + facetAddress: facetAddressList[i], + action: Storage.FacetCutAction.Replace, + functionSelectors: selectors + }); + } + } + + string memory jsonAdd = vm.readFile(JSON_SELECTOR_PATH_ADD); + { + // Build appropriate payload + uint256 n = addFacetNames.length; + addCut = new Storage.FacetCut[](n); + for (uint256 i = 0; i < n; ++i) { + // Get Selectors from json + bytes4[] memory selectors = _arrayBytes32ToBytes4( + jsonAdd.readBytes32Array(string.concat("$.", addFacetNames[i])) + ); + addCut[i] = Storage.FacetCut({ + facetAddress: addFacetAddressList[i], + action: Storage.FacetCutAction.Add, + functionSelectors: selectors + }); + } + } + + vm.startPrank(governor); + + // Then update the oracle configs + // Get the previous oracles configs + ( + Storage.OracleReadType oracleTypeEUROC, + Storage.OracleReadType targetTypeEUROC, + bytes memory oracleDataEUROC, + bytes memory targetDataEUROC + ) = OldTransmuter(address(transmuter)).getOracle(address(EUROC)); + + (Storage.OracleReadType oracleTypeBC3M, , bytes memory oracleDataBC3M, ) = OldTransmuter(address(transmuter)) + .getOracle(address(BC3M)); + + (, , , , uint256 currentBC3MPrice) = transmuter.getOracleValues(address(BC3M)); + + bytes memory callData; + // set the right implementations + transmuter.diamondCut(replaceCut, address(0), callData); + transmuter.diamondCut(addCut, address(0), callData); + + // update the oracles + oracleConfigEUROC = abi.encode( + oracleTypeEUROC, + targetTypeEUROC, + oracleDataEUROC, + targetDataEUROC, + abi.encode(USER_PROTECTION_EUROC, FIREWALL_BURN_RATIO_EUROC) + ); + transmuter.setOracle(EUROC, oracleConfigEUROC); + + oracleConfigBC3M = abi.encode( + oracleTypeBC3M, + Storage.OracleReadType.MAX, + oracleDataBC3M, + abi.encode(currentBC3MPrice), + abi.encode(USER_PROTECTION_BC3M, FIREWALL_BURN_RATIO_BC3M) + ); + transmuter.setOracle(BC3M, oracleConfigBC3M); + + // Finally add the new collateral and adapt the target exposure + + // Set ERNX + { + CollateralSetupProd memory collateral; + + uint64[] memory xMintFeeERNX = new uint64[](3); + xMintFeeERNX[0] = uint64(0); + xMintFeeERNX[1] = uint64((49 * BASE_9) / 100); + xMintFeeERNX[2] = uint64((50 * BASE_9) / 100); + + int64[] memory yMintFeeERNX = new int64[](3); + yMintFeeERNX[0] = int64(0); + yMintFeeERNX[1] = int64(0); + yMintFeeERNX[2] = int64(uint64(MAX_MINT_FEE)); + + uint64[] memory xBurnFeeERNX = new uint64[](3); + xBurnFeeERNX[0] = uint64(BASE_9); + xBurnFeeERNX[1] = uint64((26 * BASE_9) / 100); + xBurnFeeERNX[2] = uint64((25 * BASE_9) / 100); + + int64[] memory yBurnFeeERNX = new int64[](3); + yBurnFeeERNX[0] = int64(uint64((50 * BASE_9) / 10000)); + yBurnFeeERNX[1] = int64(uint64((50 * BASE_9) / 10000)); + yBurnFeeERNX[2] = int64(uint64(MAX_BURN_FEE)); + + { + bytes memory readData; + { + AggregatorV3Interface[] memory circuitChainlink = new AggregatorV3Interface[](1); + uint32[] memory stalePeriods = new uint32[](1); + uint8[] memory circuitChainIsMultiplied = new uint8[](1); + uint8[] memory chainlinkDecimals = new uint8[](1); + + // Chainlink ERNX/EUR oracle + circuitChainlink[0] = AggregatorV3Interface(0x475855DAe09af1e3f2d380d766b9E630926ad3CE); + stalePeriods[0] = 3 days; + circuitChainIsMultiplied[0] = 1; + chainlinkDecimals[0] = 8; + Storage.OracleQuoteType quoteType = Storage.OracleQuoteType.UNIT; + readData = abi.encode( + circuitChainlink, + stalePeriods, + circuitChainIsMultiplied, + chainlinkDecimals, + quoteType + ); + } + + bytes memory targetData; + { + (, int256 ratio, , uint256 updatedAt, ) = AggregatorV3Interface( + 0x475855DAe09af1e3f2d380d766b9E630926ad3CE + ).latestRoundData(); + targetData = abi.encode((uint256(ratio) * BASE_18) / BASE_8); + } + + oracleConfigBERNX = abi.encode( + Storage.OracleReadType.CHAINLINK_FEEDS, + Storage.OracleReadType.MAX, + readData, + targetData, + abi.encode(USER_PROTECTION_ERNX, FIREWALL_BURN_RATIO_ERNX) + ); + } + collateral = CollateralSetupProd( + BERNX, + oracleConfigBERNX, + xMintFeeERNX, + yMintFeeERNX, + xBurnFeeERNX, + yBurnFeeERNX + ); + transmuter.addCollateral(collateral.token); + transmuter.setOracle(collateral.token, collateral.oracleConfig); + // Mint fees + transmuter.setFees(collateral.token, collateral.xMintFee, collateral.yMintFee, true); + // Burn fees + transmuter.setFees(collateral.token, collateral.xBurnFee, collateral.yBurnFee, false); + transmuter.togglePause(collateral.token, Storage.ActionType.Mint); + transmuter.togglePause(collateral.token, Storage.ActionType.Burn); + + // Set whitelist status for bC3M + bytes memory whitelistData = abi.encode( + Storage.WhitelistType.BACKED, + // Keyring whitelist check + abi.encode(address(0x9391B14dB2d43687Ea1f6E546390ED4b20766c46)) + ); + transmuter.setWhitelistStatus(BERNX, 1, whitelistData); + } + + // Set target exposures for EUROC + { + uint64[] memory xMintFeeEUROC = new uint64[](3); + xMintFeeEUROC[0] = uint64(0); + xMintFeeEUROC[1] = uint64((69 * BASE_9) / 100); + xMintFeeEUROC[2] = uint64((70 * BASE_9) / 100); + + int64[] memory yMintFeeEUROC = new int64[](3); + yMintFeeEUROC[0] = int64(0); + yMintFeeEUROC[1] = int64(0); + yMintFeeEUROC[2] = int64(uint64(MAX_MINT_FEE)); + + uint64[] memory xBurnFeeEUROC = new uint64[](3); + xBurnFeeEUROC[0] = uint64(BASE_9); + xBurnFeeEUROC[1] = uint64((11 * BASE_9) / 100); + xBurnFeeEUROC[2] = uint64((10 * BASE_9) / 100); + + int64[] memory yBurnFeeEUROC = new int64[](3); + yBurnFeeEUROC[0] = int64(0); + yBurnFeeEUROC[1] = int64(0); + yBurnFeeEUROC[2] = int64(uint64(MAX_BURN_FEE)); + + // Mint fees + transmuter.setFees(EUROC, xMintFeeEUROC, yMintFeeEUROC, true); + // Burn fees + transmuter.setFees(EUROC, xBurnFeeEUROC, yBurnFeeEUROC, false); + } + + // Set target exposures for bC3M + { + uint64[] memory xMintFeeC3M = new uint64[](3); + xMintFeeC3M[0] = uint64(0); + xMintFeeC3M[1] = uint64((49 * BASE_9) / 100); + xMintFeeC3M[2] = uint64((50 * BASE_9) / 100); + + int64[] memory yMintFeeC3M = new int64[](3); + yMintFeeC3M[0] = int64(0); + yMintFeeC3M[1] = int64(0); + yMintFeeC3M[2] = int64(uint64(MAX_MINT_FEE)); + + uint64[] memory xBurnFeeC3M = new uint64[](3); + xBurnFeeC3M[0] = uint64(BASE_9); + xBurnFeeC3M[1] = uint64((26 * BASE_9) / 100); + xBurnFeeC3M[2] = uint64((25 * BASE_9) / 100); + + int64[] memory yBurnFeeC3M = new int64[](3); + yBurnFeeC3M[0] = int64(uint64((50 * BASE_9) / 10000)); + yBurnFeeC3M[1] = int64(uint64((50 * BASE_9) / 10000)); + yBurnFeeC3M[2] = int64(uint64(MAX_BURN_FEE)); + + // Mint fees + transmuter.setFees(BC3M, xMintFeeC3M, yMintFeeC3M, true); + // Burn fees + transmuter.setFees(BC3M, xBurnFeeC3M, yBurnFeeC3M, false); + } + + transmuter.toggleWhitelist(Storage.WhitelistType.BACKED, WHALE_AGEUR); + + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + GETTERS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testUnit_Upgrade_AccessControlManager() external { + assertEq(address(transmuter.accessControlManager()), _chainToContract(CHAIN_SOURCE, ContractType.CoreBorrow)); + } + + function testUnit_Upgrade_AgToken() external { + assertEq(address(transmuter.agToken()), _chainToContract(CHAIN_SOURCE, ContractType.AgEUR)); + } + + function testUnit_Upgrade_GetCollateralList() external { + address[] memory collateralList = transmuter.getCollateralList(); + assertEq(collateralList.length, 3); + assertEq(collateralList[0], address(EUROC)); + assertEq(collateralList[1], address(BC3M)); + assertEq(collateralList[2], address(BERNX)); + } + + function testUnit_Upgrade_GetCollateralInfo() external { + { + Storage.Collateral memory collatInfoEUROC = transmuter.getCollateralInfo(address(EUROC)); + assertEq(collatInfoEUROC.isManaged, 0); + assertEq(collatInfoEUROC.isMintLive, 1); + assertEq(collatInfoEUROC.isBurnLive, 1); + assertEq(collatInfoEUROC.decimals, 6); + assertEq(collatInfoEUROC.onlyWhitelisted, 0); + assertApproxEqRel(collatInfoEUROC.normalizedStables, 9580108 * BASE_18, 100 * BPS); + assertEq(collatInfoEUROC.oracleConfig, oracleConfigEUROC); + assertEq(collatInfoEUROC.whitelistData.length, 0); + assertEq(collatInfoEUROC.managerData.subCollaterals.length, 0); + assertEq(collatInfoEUROC.managerData.config.length, 0); + + { + assertEq(collatInfoEUROC.xFeeMint.length, 3); + assertEq(collatInfoEUROC.yFeeMint.length, 3); + assertEq(collatInfoEUROC.xFeeMint[0], 0); + assertEq(collatInfoEUROC.yFeeMint[0], 0); + assertEq(collatInfoEUROC.xFeeMint[1], uint64((69 * BASE_9) / 100)); + assertEq(collatInfoEUROC.yFeeMint[1], 0); + assertEq(collatInfoEUROC.xFeeMint[2], uint64((70 * BASE_9) / 100)); + assertEq(collatInfoEUROC.yFeeMint[2], int64(uint64(MAX_MINT_FEE))); + } + { + assertEq(collatInfoEUROC.xFeeBurn.length, 3); + assertEq(collatInfoEUROC.yFeeBurn.length, 3); + assertEq(collatInfoEUROC.xFeeBurn[0], 1000000000); + assertEq(collatInfoEUROC.yFeeBurn[0], 0); + assertEq(collatInfoEUROC.xFeeBurn[1], uint64((11 * BASE_9) / 100)); + assertEq(collatInfoEUROC.yFeeBurn[1], 0); + assertEq(collatInfoEUROC.xFeeBurn[2], uint64((10 * BASE_9) / 100)); + assertEq(collatInfoEUROC.yFeeBurn[2], 999000000); + } + } + + { + Storage.Collateral memory collatInfoBC3M = transmuter.getCollateralInfo(address(BC3M)); + assertEq(collatInfoBC3M.isManaged, 0); + assertEq(collatInfoBC3M.isMintLive, 1); + assertEq(collatInfoBC3M.isBurnLive, 1); + assertEq(collatInfoBC3M.decimals, 18); + assertEq(collatInfoBC3M.onlyWhitelisted, 1); + assertApproxEqRel(collatInfoBC3M.normalizedStables, 6236650 * BASE_18, 100 * BPS); + assertEq(collatInfoBC3M.oracleConfig, oracleConfigBC3M); + { + (Storage.WhitelistType whitelist, bytes memory data) = abi.decode( + collatInfoBC3M.whitelistData, + (Storage.WhitelistType, bytes) + ); + address keyringGuard = abi.decode(data, (address)); + assertEq(uint8(whitelist), uint8(Storage.WhitelistType.BACKED)); + assertEq(keyringGuard, 0x9391B14dB2d43687Ea1f6E546390ED4b20766c46); + } + assertEq(collatInfoBC3M.managerData.subCollaterals.length, 0); + assertEq(collatInfoBC3M.managerData.config.length, 0); + + { + assertEq(collatInfoBC3M.xFeeMint.length, 3); + assertEq(collatInfoBC3M.yFeeMint.length, 3); + assertEq(collatInfoBC3M.xFeeMint[0], 0); + assertEq(collatInfoBC3M.yFeeMint[0], 0); + assertEq(collatInfoBC3M.xFeeMint[1], uint64((49 * BASE_9) / 100)); + assertEq(collatInfoBC3M.yFeeMint[1], 0); + assertEq(collatInfoBC3M.xFeeMint[2], uint64((50 * BASE_9) / 100)); + assertEq(collatInfoBC3M.yFeeMint[2], int64(uint64(MAX_MINT_FEE))); + } + { + assertEq(collatInfoBC3M.xFeeBurn.length, 3); + assertEq(collatInfoBC3M.yFeeBurn.length, 3); + assertEq(collatInfoBC3M.xFeeBurn[0], 1000000000); + assertEq(collatInfoBC3M.yFeeBurn[0], int64(uint64((50 * BASE_9) / 10000))); + assertEq(collatInfoBC3M.xFeeBurn[1], uint64((26 * BASE_9) / 100)); + assertEq(collatInfoBC3M.yFeeBurn[1], int64(uint64((50 * BASE_9) / 10000))); + assertEq(collatInfoBC3M.xFeeBurn[2], uint64((25 * BASE_9) / 100)); + assertEq(collatInfoBC3M.yFeeBurn[2], 999000000); + } + } + + { + Storage.Collateral memory collatInfoBERNX = transmuter.getCollateralInfo(address(BERNX)); + assertEq(collatInfoBERNX.isManaged, 0); + assertEq(collatInfoBERNX.isMintLive, 1); + assertEq(collatInfoBERNX.isBurnLive, 1); + assertEq(collatInfoBERNX.decimals, 18); + assertEq(collatInfoBERNX.onlyWhitelisted, 1); + assertEq(collatInfoBERNX.normalizedStables, 0); + assertEq(collatInfoBERNX.oracleConfig, oracleConfigBERNX); + { + (Storage.WhitelistType whitelist, bytes memory data) = abi.decode( + collatInfoBERNX.whitelistData, + (Storage.WhitelistType, bytes) + ); + address keyringGuard = abi.decode(data, (address)); + assertEq(uint8(whitelist), uint8(Storage.WhitelistType.BACKED)); + assertEq(keyringGuard, 0x9391B14dB2d43687Ea1f6E546390ED4b20766c46); + } + assertEq(collatInfoBERNX.managerData.subCollaterals.length, 0); + assertEq(collatInfoBERNX.managerData.config.length, 0); + + { + assertEq(collatInfoBERNX.xFeeMint.length, 3); + assertEq(collatInfoBERNX.yFeeMint.length, 3); + assertEq(collatInfoBERNX.xFeeMint[0], 0); + assertEq(collatInfoBERNX.yFeeMint[0], 0); + assertEq(collatInfoBERNX.xFeeMint[1], uint64((49 * BASE_9) / 100)); + assertEq(collatInfoBERNX.yFeeMint[1], 0); + assertEq(collatInfoBERNX.xFeeMint[2], uint64((50 * BASE_9) / 100)); + assertEq(collatInfoBERNX.yFeeMint[2], int64(uint64(MAX_MINT_FEE))); + } + { + assertEq(collatInfoBERNX.xFeeBurn.length, 3); + assertEq(collatInfoBERNX.yFeeBurn.length, 3); + assertEq(collatInfoBERNX.xFeeBurn[0], 1000000000); + assertEq(collatInfoBERNX.yFeeBurn[0], int64(uint64((50 * BASE_9) / 10000))); + assertEq(collatInfoBERNX.xFeeBurn[1], uint64((26 * BASE_9) / 100)); + assertEq(collatInfoBERNX.yFeeBurn[1], int64(uint64((50 * BASE_9) / 10000))); + assertEq(collatInfoBERNX.xFeeBurn[2], uint64((25 * BASE_9) / 100)); + assertEq(collatInfoBERNX.yFeeBurn[2], 999000000); + } + } + } + + function testUnit_Upgrade_GetCollateralDecimals() external { + assertEq(transmuter.getCollateralDecimals(address(EUROC)), 6); + assertEq(transmuter.getCollateralDecimals(address(BC3M)), 18); + assertEq(transmuter.getCollateralDecimals(address(BERNX)), 18); + } + + function testUnit_Upgrade_getCollateralMintFees() external { + { + (uint64[] memory xFeeMint, int64[] memory yFeeMint) = transmuter.getCollateralMintFees(address(EUROC)); + assertEq(xFeeMint.length, 3); + assertEq(yFeeMint.length, 3); + assertEq(xFeeMint[0], 0); + assertEq(yFeeMint[0], 0); + assertEq(xFeeMint[1], uint64((69 * BASE_9) / 100)); + assertEq(yFeeMint[1], 0); + assertEq(xFeeMint[2], uint64((70 * BASE_9) / 100)); + assertEq(yFeeMint[2], int64(uint64(MAX_MINT_FEE))); + } + { + (uint64[] memory xFeeMint, int64[] memory yFeeMint) = transmuter.getCollateralMintFees(address(BC3M)); + assertEq(xFeeMint.length, 3); + assertEq(yFeeMint.length, 3); + assertEq(xFeeMint[0], 0); + assertEq(yFeeMint[0], 0); + assertEq(xFeeMint[1], uint64((49 * BASE_9) / 100)); + assertEq(yFeeMint[1], 0); + assertEq(xFeeMint[2], uint64((50 * BASE_9) / 100)); + assertEq(yFeeMint[2], int64(uint64(MAX_MINT_FEE))); + } + { + (uint64[] memory xFeeMint, int64[] memory yFeeMint) = transmuter.getCollateralMintFees(address(BERNX)); + assertEq(xFeeMint.length, 3); + assertEq(yFeeMint.length, 3); + assertEq(xFeeMint[0], 0); + assertEq(yFeeMint[0], 0); + assertEq(xFeeMint[1], uint64((49 * BASE_9) / 100)); + assertEq(yFeeMint[1], 0); + assertEq(xFeeMint[2], uint64((50 * BASE_9) / 100)); + assertEq(yFeeMint[2], int64(uint64(MAX_MINT_FEE))); + } + } + + function testUnit_Upgrade_getCollateralBurnFees() external { + { + (uint64[] memory xFeeBurn, int64[] memory yFeeBurn) = transmuter.getCollateralBurnFees(address(EUROC)); + assertEq(xFeeBurn.length, 3); + assertEq(yFeeBurn.length, 3); + assertEq(xFeeBurn[0], 1000000000); + assertEq(yFeeBurn[0], 0); + assertEq(xFeeBurn[1], uint64((11 * BASE_9) / 100)); + assertEq(yFeeBurn[1], 0); + assertEq(xFeeBurn[2], uint64((10 * BASE_9) / 100)); + assertEq(yFeeBurn[2], 999000000); + } + { + (uint64[] memory xFeeBurn, int64[] memory yFeeBurn) = transmuter.getCollateralBurnFees(address(BC3M)); + assertEq(xFeeBurn.length, 3); + assertEq(yFeeBurn.length, 3); + assertEq(xFeeBurn[0], 1000000000); + assertEq(yFeeBurn[0], int64(uint64((50 * BASE_9) / 10000))); + assertEq(xFeeBurn[1], uint64((26 * BASE_9) / 100)); + assertEq(yFeeBurn[1], int64(uint64((50 * BASE_9) / 10000))); + assertEq(xFeeBurn[2], uint64((25 * BASE_9) / 100)); + assertEq(yFeeBurn[2], 999000000); + } + { + (uint64[] memory xFeeBurn, int64[] memory yFeeBurn) = transmuter.getCollateralBurnFees(address(BERNX)); + assertEq(xFeeBurn.length, 3); + assertEq(yFeeBurn.length, 3); + assertEq(xFeeBurn[0], 1000000000); + assertEq(yFeeBurn[0], int64(uint64((50 * BASE_9) / 10000))); + assertEq(xFeeBurn[1], uint64((26 * BASE_9) / 100)); + assertEq(yFeeBurn[1], int64(uint64((50 * BASE_9) / 10000))); + assertEq(xFeeBurn[2], uint64((25 * BASE_9) / 100)); + assertEq(yFeeBurn[2], 999000000); + } + } + + function testUnit_Upgrade_GetCollateralRatio() external { + (uint64 collatRatio, uint256 stablecoinIssued) = transmuter.getCollateralRatio(); + assertApproxEqRel(collatRatio, 1065 * 10 ** 6, BPS * 100); + assertApproxEqRel(stablecoinIssued, 15816758 * BASE_18, 100 * BPS); + } + + function testUnit_Upgrade_isTrusted() external { + assertEq(transmuter.isTrusted(address(governor)), false); + assertEq(transmuter.isTrustedSeller(address(governor)), false); + assertEq(transmuter.isTrusted(DEPLOYER), false); + assertEq(transmuter.isTrustedSeller(DEPLOYER), false); + assertEq(transmuter.isTrusted(NEW_DEPLOYER), false); + assertEq(transmuter.isTrustedSeller(NEW_DEPLOYER), false); + assertEq(transmuter.isTrusted(KEEPER), false); + assertEq(transmuter.isTrustedSeller(KEEPER), false); + assertEq(transmuter.isTrusted(NEW_KEEPER), false); + assertEq(transmuter.isTrustedSeller(NEW_KEEPER), false); + } + + function testUnit_Upgrade_IsWhitelistedForCollateral() external { + assertEq(transmuter.isWhitelistedForCollateral(address(EUROC), alice), true); + assertEq(transmuter.isWhitelistedForCollateral(address(BC3M), alice), false); + assertEq(transmuter.isWhitelistedForCollateral(address(BERNX), alice), false); + assertEq(transmuter.isWhitelistedForCollateral(address(EUROC), WHALE_AGEUR), true); + assertEq(transmuter.isWhitelistedForCollateral(address(BC3M), WHALE_AGEUR), true); + assertEq(transmuter.isWhitelistedForCollateral(address(BERNX), WHALE_AGEUR), true); + assertEq( + transmuter.isWhitelistedForCollateral(address(EUROC), 0xB00b1E53909F8253783D8e54AEe462f99bAcb435), + true + ); + assertEq( + transmuter.isWhitelistedForCollateral(address(BC3M), 0xB00b1E53909F8253783D8e54AEe462f99bAcb435), + true + ); + assertEq( + transmuter.isWhitelistedForCollateral(address(BERNX), 0xB00b1E53909F8253783D8e54AEe462f99bAcb435), + true + ); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ORACLE + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testUnit_Upgrade_getOracleValues_Success() external { + _checkOracleValues(address(EUROC), BASE_18, USER_PROTECTION_EUROC, FIREWALL_BURN_RATIO_EUROC); + _checkOracleValues(address(BC3M), (11974 * BASE_18) / 100, USER_PROTECTION_BC3M, FIREWALL_BURN_RATIO_BC3M); + _checkOracleValues(address(BERNX), (52274 * BASE_18) / 10000, USER_PROTECTION_ERNX, FIREWALL_BURN_RATIO_ERNX); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + MINT + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testFuzz_Upgrade_QuoteMintExactInput_Reflexivity(uint256 amountIn, uint256 fromToken) public { + fromToken = bound(fromToken, 0, transmuter.getCollateralList().length - 1); + address collateral = transmuter.getCollateralList()[fromToken]; + amountIn = bound(amountIn, BASE_6, collateral == EUROC ? BASE_6 * 1e6 : 1000 * BASE_18); + + uint256 amountStable = transmuter.quoteIn(amountIn, collateral, address(agEUR)); + uint256 amountInReflexive = transmuter.quoteOut(amountStable, collateral, address(agEUR)); + assertApproxEqRel(amountIn, amountInReflexive, BPS * 10); + } + + function testFuzz_Upgrade_QuoteMintExactInput_Independant( + uint256 amountIn, + uint256 splitProportion, + uint256 fromToken + ) public { + fromToken = bound(fromToken, 0, transmuter.getCollateralList().length - 1); + address collateral = transmuter.getCollateralList()[fromToken]; + amountIn = bound(amountIn, BASE_6, collateral == EUROC ? BASE_6 * 1e6 : 1000 * BASE_18); + splitProportion = bound(splitProportion, 0, BASE_9); + + uint256 amountStable = transmuter.quoteIn(amountIn, collateral, address(agEUR)); + uint256 amountInSplit1 = (amountIn * splitProportion) / BASE_9; + amountInSplit1 = amountInSplit1 == 0 ? 1 : amountInSplit1; + uint256 amountStableSplit1 = transmuter.quoteIn(amountInSplit1, collateral, address(agEUR)); + // do the swap to update the system + _mintExactInput(alice, collateral, amountInSplit1, amountStableSplit1); + uint256 amountStableSplit2 = transmuter.quoteIn(amountIn - amountInSplit1, collateral, address(agEUR)); + assertApproxEqRel(amountStableSplit1 + amountStableSplit2, amountStable, BPS * 10); + } + + function testFuzz_Upgrade_MintExactOutput(uint256 stableAmount, uint256 fromToken) public { + fromToken = bound(fromToken, 0, transmuter.getCollateralList().length - 1); + address collateral = transmuter.getCollateralList()[fromToken]; + stableAmount = bound(stableAmount, BASE_18, BASE_6 * 1e18); + + uint256 prevBalanceStable = agEUR.balanceOf(alice); + uint256 prevTransmuterCollat = IERC20(collateral).balanceOf(address(transmuter)); + uint256 prevAgTokenSupply = IERC20(agEUR).totalSupply(); + (uint256 prevStableAmountCollat, uint256 prevStableAmount) = transmuter.getIssuedByCollateral(collateral); + + uint256 amountIn = transmuter.quoteOut(stableAmount, collateral, address(agEUR)); + if (amountIn == 0 || stableAmount == 0) return; + _mintExactOutput(alice, collateral, stableAmount, amountIn); + + uint256 balanceStable = agEUR.balanceOf(alice); + + assertEq(balanceStable, prevBalanceStable + stableAmount); + assertEq(agEUR.totalSupply(), prevAgTokenSupply + stableAmount); + assertEq(IERC20(collateral).balanceOf(alice), 0); + assertEq(IERC20(collateral).balanceOf(address(transmuter)), prevTransmuterCollat + amountIn); + + (uint256 newStableAmountCollat, uint256 newStableAmount) = transmuter.getIssuedByCollateral(collateral); + + assertApproxEqAbs(newStableAmountCollat, prevStableAmountCollat + stableAmount, 1 wei); + assertApproxEqAbs(newStableAmount, prevStableAmount + stableAmount, 1 wei); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + BURN + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testFuzz_Upgrade_QuoteBurnExactInput_Reflexivity(uint256 amountStable, uint256 fromToken) public { + fromToken = bound(fromToken, 0, transmuter.getCollateralList().length - 2); + address collateral = transmuter.getCollateralList()[fromToken]; + amountStable = bound(amountStable, BASE_18, BASE_6 * 1e18); + + uint256 amountOut = transmuter.quoteIn(amountStable, address(agEUR), collateral); + uint256 amountStableReflexive = transmuter.quoteOut(amountOut, address(agEUR), collateral); + assertApproxEqRel(amountStable, amountStableReflexive, BPS * 10); + + // BERNX doesn't have any minted stables so it will be blocked + vm.expectRevert(Errors.InvalidSwap.selector); + transmuter.quoteIn(amountStable, address(agEUR), BERNX); + } + + function testFuzz_Upgrade_QuoteBurnExactInput_Independant( + uint256 amountStable, + uint256 splitProportion, + uint256 fromToken + ) public { + fromToken = bound(fromToken, 0, transmuter.getCollateralList().length - 2); + address collateral = transmuter.getCollateralList()[fromToken]; + amountStable = bound(amountStable, BASE_18, BASE_6 * 1e18); + splitProportion = bound(splitProportion, 0, BASE_9); + + uint256 amountOut = transmuter.quoteIn(amountStable, address(agEUR), collateral); + uint256 amountStableSplit1 = (amountStable * splitProportion) / BASE_9; + amountStableSplit1 = amountStableSplit1 == 0 ? 1 : amountStableSplit1; + uint256 amountOutSplit1 = transmuter.quoteIn(amountStableSplit1, address(agEUR), collateral); + // do the swap to update the system + _burnExactInput(WHALE_AGEUR, collateral, amountStableSplit1, amountOutSplit1); + uint256 amountOutSplit2 = transmuter.quoteIn(amountStable - amountStableSplit1, address(agEUR), collateral); + assertApproxEqRel(amountOutSplit1 + amountOutSplit2, amountOut, BPS * 10); + } + + function testFuzz_Upgrade_BurnExactOutput(uint256 amountOut, uint256 fromToken) public { + fromToken = bound(fromToken, 0, transmuter.getCollateralList().length - 2); + address collateral = transmuter.getCollateralList()[fromToken]; + amountOut = bound(amountOut, BASE_6, collateral == BC3M ? 1000 * BASE_18 : BASE_6 * 1e6); + + uint256 prevBalanceStable = agEUR.balanceOf(WHALE_AGEUR); + uint256 prevTransmuterCollat = IERC20(collateral).balanceOf(address(transmuter)); + uint256 prevAgTokenSupply = IERC20(agEUR).totalSupply(); + (uint256 prevStableAmountCollat, uint256 prevStableAmount) = transmuter.getIssuedByCollateral(collateral); + + uint256 stableAmount = transmuter.quoteOut(amountOut, address(agEUR), collateral); + if (amountOut == 0 || stableAmount == 0) return; + _burnExactOutput(WHALE_AGEUR, collateral, amountOut, stableAmount); + + uint256 balanceStable = agEUR.balanceOf(WHALE_AGEUR); + + assertEq(balanceStable, prevBalanceStable - stableAmount); + assertEq(agEUR.totalSupply(), prevAgTokenSupply - stableAmount); + assertEq(IERC20(collateral).balanceOf(WHALE_AGEUR), amountOut); + assertEq(IERC20(collateral).balanceOf(address(transmuter)), prevTransmuterCollat - amountOut); + + (uint256 newStableAmountCollat, uint256 newStableAmount) = transmuter.getIssuedByCollateral(collateral); + + assertApproxEqAbs(newStableAmountCollat, prevStableAmountCollat - stableAmount, 1 wei); + assertApproxEqAbs(newStableAmount, prevStableAmount - stableAmount, 1 wei); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + REDEEM + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testFuzz_Upgrade_QuoteRedeemRandomFees(uint256[3] memory latestOracleValue) public { + (uint64 collatRatio, ) = transmuter.getCollateralRatio(); + uint256 amountBurnt = agEUR.balanceOf(WHALE_AGEUR); + vm.prank(WHALE_AGEUR); + (address[] memory tokens, uint256[] memory amounts) = transmuter.quoteRedemptionCurve(amountBurnt); + + // compute fee at current collatRatio + assertEq(tokens.length, 3); + assertEq(tokens.length, amounts.length); + assertEq(tokens[0], address(EUROC)); + assertEq(tokens[1], address(BC3M)); + assertEq(tokens[2], address(BERNX)); + uint64 fee; + (uint64[] memory xFeeRedeem, int64[] memory yFeeRedeem) = transmuter.getRedemptionFees(); + if (collatRatio >= BASE_9) fee = uint64(yFeeRedeem[yFeeRedeem.length - 1]); + else fee = uint64(LibHelpers.piecewiseLinear(collatRatio, xFeeRedeem, yFeeRedeem)); + uint256 mintedStables = transmuter.getTotalIssued(); + _assertQuoteAmounts(collatRatio, mintedStables, amountBurnt, fee, amounts); + + uint256 balanceEURC = IERC20(EUROC).balanceOf(address(WHALE_AGEUR)); + uint256 balanceBC3M = IERC20(BC3M).balanceOf(address(WHALE_AGEUR)); + uint256 balanceBERNX = IERC20(BERNX).balanceOf(address(WHALE_AGEUR)); + uint256 balanceAgToken = agEUR.balanceOf(WHALE_AGEUR); + uint256[] memory minAmountOuts = new uint256[](3); + + vm.prank(WHALE_AGEUR); + transmuter.redeem(amountBurnt, WHALE_AGEUR, block.timestamp + 1000, minAmountOuts); + assertEq(IERC20(EUROC).balanceOf(address(WHALE_AGEUR)), balanceEURC + amounts[0]); + assertEq(IERC20(BC3M).balanceOf(address(WHALE_AGEUR)), balanceBC3M + amounts[1]); + assertEq(IERC20(BERNX).balanceOf(address(WHALE_AGEUR)), balanceBERNX + amounts[2]); + assertEq(agEUR.balanceOf(WHALE_AGEUR), balanceAgToken - amountBurnt); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + UTILS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function _mintExactOutput( + address owner, + address tokenIn, + uint256 amountStable, + uint256 estimatedAmountIn + ) internal { + vm.startPrank(owner); + deal(tokenIn, owner, estimatedAmountIn); + IERC20(tokenIn).approve(address(transmuter), type(uint256).max); + transmuter.swapExactOutput( + amountStable, + estimatedAmountIn, + tokenIn, + address(agEUR), + owner, + block.timestamp * 2 + ); + vm.stopPrank(); + } + + function _mintExactInput(address owner, address tokenIn, uint256 amountIn, uint256 estimatedStable) internal { + vm.startPrank(owner); + deal(tokenIn, owner, amountIn); + IERC20(tokenIn).approve(address(transmuter), type(uint256).max); + transmuter.swapExactInput(amountIn, estimatedStable, tokenIn, address(agEUR), owner, block.timestamp * 2); + vm.stopPrank(); + } + + function _burnExactInput( + address owner, + address tokenOut, + uint256 amountStable, + uint256 estimatedAmountOut + ) internal returns (bool burnMoreThanHad) { + // we need to increase the balance because fees are negative and we need to transfer + // more than what we received with the mint + if (IERC20(tokenOut).balanceOf(address(transmuter)) < estimatedAmountOut) { + deal(tokenOut, address(transmuter), estimatedAmountOut); + burnMoreThanHad = true; + } + + vm.startPrank(owner); + transmuter.swapExactInput(amountStable, estimatedAmountOut, address(agEUR), tokenOut, owner, 0); + vm.stopPrank(); + } + + function _burnExactOutput( + address owner, + address tokenOut, + uint256 amountOut, + uint256 estimatedStable + ) internal returns (bool) { + // _logIssuedCollateral(); + vm.startPrank(owner); + (uint256 maxAmount, ) = transmuter.getIssuedByCollateral(tokenOut); + uint256 balanceStableOwner = agEUR.balanceOf(owner); + if (estimatedStable > maxAmount) vm.expectRevert(); + else if (estimatedStable > balanceStableOwner) vm.expectRevert("ERC20: burn amount exceeds balance"); + transmuter.swapExactOutput(amountOut, estimatedStable, address(agEUR), tokenOut, owner, block.timestamp * 2); + if (amountOut > maxAmount) return false; + vm.stopPrank(); + return true; + } + + function _assertQuoteAmounts( + uint64 collatRatio, + uint256 mintedStables, + uint256 amountBurnt, + uint64 fee, + uint256[] memory amounts + ) internal { + uint256 amountInValueReceived; + { + (, , , , uint256 redemptionPrice) = transmuter.getOracleValues(address(EUROC)); + amountInValueReceived += (redemptionPrice * amounts[0]) / 10 ** 6; + } + { + (, , , , uint256 redemptionPrice) = transmuter.getOracleValues(address(BC3M)); + amountInValueReceived += (redemptionPrice * amounts[1]) / 10 ** 18; + } + + uint256 denom = (mintedStables * BASE_9); + uint256 valueCheck = (collatRatio * amountBurnt * fee) / BASE_18; + if (collatRatio >= BASE_9) { + denom = (mintedStables * collatRatio); + // for rounding errors + assertLe(amountInValueReceived, amountBurnt + 1); + valueCheck = (amountBurnt * fee) / BASE_9; + } + assertApproxEqAbs( + amounts[0], + (IERC20(EUROC).balanceOf(address(transmuter)) * amountBurnt * fee) / denom, + 1 wei + ); + assertApproxEqAbs(amounts[1], (IERC20(BC3M).balanceOf(address(transmuter)) * amountBurnt * fee) / denom, 1 wei); + if (collatRatio < BASE_9) { + assertLe(amountInValueReceived, (collatRatio * amountBurnt) / BASE_9 + 1); + } + assertApproxEqRel(amountInValueReceived, valueCheck, BPS * 10); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + CHECKS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function _checkOracleValues( + address collateral, + uint256 targetValue, + uint128 userProtection, + uint128 firewallBurn + ) internal { + (uint256 mint, uint256 burn, uint256 ratio, uint256 minRatio, uint256 redemption) = transmuter.getOracleValues( + collateral + ); + assertApproxEqRel(targetValue, redemption, 200 * BPS); + + if ( + targetValue * (BASE_18 - userProtection) < redemption * BASE_18 && + redemption * BASE_18 < targetValue * (BASE_18 + userProtection) + ) assertEq(burn, targetValue); + else assertEq(burn, redemption); + + if ( + targetValue * (BASE_18 - userProtection) < redemption * BASE_18 && + redemption * BASE_18 < targetValue * (BASE_18 + userProtection) + ) { + assertEq(mint, targetValue); + assertEq(ratio, BASE_18); + } else if (redemption * BASE_18 < targetValue * (BASE_18 - firewallBurn)) { + assertEq(mint, redemption); + assertEq(ratio, (redemption * BASE_18) / targetValue); + } else { + assertEq(mint, redemption); + assertEq(ratio, BASE_18); + } + } +} diff --git a/test/scripts/UpdateTransmuterFacetsUSDATest.t.sol b/test/scripts/UpdateTransmuterFacetsUSDATest.t.sol new file mode 100644 index 00000000..43f34038 --- /dev/null +++ b/test/scripts/UpdateTransmuterFacetsUSDATest.t.sol @@ -0,0 +1,689 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import { stdJson } from "forge-std/StdJson.sol"; +import { console } from "forge-std/console.sol"; +import { Test } from "forge-std/Test.sol"; + +import "../../scripts/Constants.s.sol"; + +import { Helpers } from "../../scripts/Helpers.s.sol"; +import "contracts/utils/Errors.sol" as Errors; +import "contracts/transmuter/Storage.sol" as Storage; +import { Getters } from "contracts/transmuter/facets/Getters.sol"; +import { Redeemer } from "contracts/transmuter/facets/Redeemer.sol"; +import { SettersGovernor } from "contracts/transmuter/facets/SettersGovernor.sol"; +import { SettersGuardian } from "contracts/transmuter/facets/SettersGuardian.sol"; +import { Swapper } from "contracts/transmuter/facets/Swapper.sol"; +import "contracts/transmuter/libraries/LibHelpers.sol"; +import "interfaces/external/chainlink/AggregatorV3Interface.sol"; +import { CollateralSetupProd } from "contracts/transmuter/configs/ProductionTypes.sol"; +import { ITransmuter } from "interfaces/ITransmuter.sol"; +import "utils/src/Constants.sol"; +import { IERC20 } from "oz/interfaces/IERC20.sol"; +import { IMorphoOracle, MockMorphoOracle } from "../mock/MockMorphoOracle.sol"; + +interface OldTransmuter { + function getOracle( + address + ) external view returns (Storage.OracleReadType, Storage.OracleReadType, bytes memory, bytes memory); +} + +contract UpdateTransmuterFacetsUSDATest is Helpers, Test { + using stdJson for string; + + uint256 public CHAIN_SOURCE; + + address constant WHALE_USDA = 0xA9DdD91249DFdd450E81E1c56Ab60E1A62651701; + + string[] replaceFacetNames; + string[] addFacetNames; + address[] facetAddressList; + address[] addFacetAddressList; + + ITransmuter transmuter; + IERC20 USDA; + address governor; + bytes public oracleConfigUSDC; + bytes public oracleConfigIB01; + bytes public oracleConfigSTEAK; + + function setUp() public override { + super.setUp(); + + CHAIN_SOURCE = CHAIN_ETHEREUM; + + ethereumFork = vm.createFork(vm.envString("ETH_NODE_URI_MAINNET"), 19483530); + vm.selectFork(forkIdentifier[CHAIN_SOURCE]); + + governor = DEPLOYER; + transmuter = ITransmuter(0x222222fD79264BBE280b4986F6FEfBC3524d0137); + USDA = IERC20(0x0000206329b97DB379d5E1Bf586BbDB969C63274); + + Storage.FacetCut[] memory replaceCut; + Storage.FacetCut[] memory addCut; + + replaceFacetNames.push("Getters"); + facetAddressList.push(address(new Getters())); + console.log("Getters deployed at: ", facetAddressList[facetAddressList.length - 1]); + + replaceFacetNames.push("Redeemer"); + facetAddressList.push(address(new Redeemer())); + console.log("Redeemer deployed at: ", facetAddressList[facetAddressList.length - 1]); + + replaceFacetNames.push("SettersGovernor"); + address settersGovernor = address(new SettersGovernor()); + facetAddressList.push(settersGovernor); + console.log("SettersGovernor deployed at: ", facetAddressList[facetAddressList.length - 1]); + + replaceFacetNames.push("Swapper"); + facetAddressList.push(address(new Swapper())); + console.log("Swapper deployed at: ", facetAddressList[facetAddressList.length - 1]); + + addFacetNames.push("SettersGovernor"); + addFacetAddressList.push(settersGovernor); + + string memory jsonReplace = vm.readFile(JSON_SELECTOR_PATH_REPLACE); + { + // Build appropriate payload + uint256 n = replaceFacetNames.length; + replaceCut = new Storage.FacetCut[](n); + for (uint256 i = 0; i < n; ++i) { + // Get Selectors from json + bytes4[] memory selectors = _arrayBytes32ToBytes4( + jsonReplace.readBytes32Array(string.concat("$.", replaceFacetNames[i])) + ); + + replaceCut[i] = Storage.FacetCut({ + facetAddress: facetAddressList[i], + action: Storage.FacetCutAction.Replace, + functionSelectors: selectors + }); + } + } + + string memory jsonAdd = vm.readFile(JSON_SELECTOR_PATH_ADD); + { + // Build appropriate payload + uint256 n = addFacetNames.length; + addCut = new Storage.FacetCut[](n); + for (uint256 i = 0; i < n; ++i) { + // Get Selectors from json + bytes4[] memory selectors = _arrayBytes32ToBytes4( + jsonAdd.readBytes32Array(string.concat("$.", addFacetNames[i])) + ); + addCut[i] = Storage.FacetCut({ + facetAddress: addFacetAddressList[i], + action: Storage.FacetCutAction.Add, + functionSelectors: selectors + }); + } + } + + vm.startPrank(governor); + + bytes memory callData; + // set the right implementations + transmuter.diamondCut(replaceCut, address(0), callData); + transmuter.diamondCut(addCut, address(0), callData); + + { + bytes memory oracleConfig; + bytes memory readData; + { + AggregatorV3Interface[] memory circuitChainlink = new AggregatorV3Interface[](1); + uint32[] memory stalePeriods = new uint32[](1); + uint8[] memory circuitChainIsMultiplied = new uint8[](1); + uint8[] memory chainlinkDecimals = new uint8[](1); + + // Chainlink USDC/USD oracle + circuitChainlink[0] = AggregatorV3Interface(0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6); + stalePeriods[0] = ((1 days) * 3) / 2; + circuitChainIsMultiplied[0] = 1; + chainlinkDecimals[0] = 8; + Storage.OracleQuoteType quoteType = Storage.OracleQuoteType.UNIT; + readData = abi.encode( + circuitChainlink, + stalePeriods, + circuitChainIsMultiplied, + chainlinkDecimals, + quoteType + ); + } + bytes memory targetData; + oracleConfig = abi.encode( + Storage.OracleReadType.CHAINLINK_FEEDS, + Storage.OracleReadType.STABLE, + readData, + targetData, + abi.encode(uint128(5 * BPS), uint128(0)) + ); + oracleConfigUSDC = oracleConfig; + transmuter.setOracle(USDC, oracleConfig); + (, , , , uint256 redemptionPrice) = transmuter.getOracleValues(address(USDC)); + } + + // Set Collaterals + CollateralSetupProd[] memory collaterals = new CollateralSetupProd[](2); + + // IB01 + { + uint64[] memory xMintFeeIB01 = new uint64[](3); + xMintFeeIB01[0] = uint64(0); + xMintFeeIB01[1] = uint64((49 * BASE_9) / 100); + xMintFeeIB01[2] = uint64((50 * BASE_9) / 100); + + int64[] memory yMintFeeIB01 = new int64[](3); + yMintFeeIB01[0] = int64(0); + yMintFeeIB01[1] = int64(0); + yMintFeeIB01[2] = int64(uint64(MAX_MINT_FEE)); + + uint64[] memory xBurnFeeIB01 = new uint64[](3); + xBurnFeeIB01[0] = uint64(BASE_9); + xBurnFeeIB01[1] = uint64((16 * BASE_9) / 100); + xBurnFeeIB01[2] = uint64((15 * BASE_9) / 100); + + int64[] memory yBurnFeeIB01 = new int64[](3); + yBurnFeeIB01[0] = int64(uint64((50 * BASE_9) / 10000)); + yBurnFeeIB01[1] = int64(uint64((50 * BASE_9) / 10000)); + yBurnFeeIB01[2] = int64(uint64(MAX_BURN_FEE)); + + bytes memory oracleConfig; + { + bytes memory readData; + { + AggregatorV3Interface[] memory circuitChainlink = new AggregatorV3Interface[](1); + uint32[] memory stalePeriods = new uint32[](1); + uint8[] memory circuitChainIsMultiplied = new uint8[](1); + uint8[] memory chainlinkDecimals = new uint8[](1); + // Chainlink IB01/USD oracle + circuitChainlink[0] = AggregatorV3Interface(0x32d1463EB53b73C095625719Afa544D5426354cB); + stalePeriods[0] = ((1 days) * 3) / 2; + circuitChainIsMultiplied[0] = 1; + chainlinkDecimals[0] = 8; + Storage.OracleQuoteType quoteType = Storage.OracleQuoteType.UNIT; + readData = abi.encode( + circuitChainlink, + stalePeriods, + circuitChainIsMultiplied, + chainlinkDecimals, + quoteType + ); + } + + (, int256 answer, , , ) = AggregatorV3Interface(0x32d1463EB53b73C095625719Afa544D5426354cB) + .latestRoundData(); + uint256 initTarget = uint256(answer) * 1e10; + bytes memory targetData = abi.encode(initTarget); + + oracleConfig = abi.encode( + Storage.OracleReadType.CHAINLINK_FEEDS, + Storage.OracleReadType.MAX, + readData, + targetData, + abi.encode(USER_PROTECTION_IB01, FIREWALL_BURN_RATIO_IB01) + ); + oracleConfigIB01 = oracleConfig; + } + collaterals[0] = CollateralSetupProd( + BIB01, + oracleConfig, + xMintFeeIB01, + yMintFeeIB01, + xBurnFeeIB01, + yBurnFeeIB01 + ); + } + + // steakUSDC -> max oracle or target oracle + { + uint64[] memory xMintFeeSteak = new uint64[](3); + xMintFeeSteak[0] = uint64(0); + xMintFeeSteak[1] = uint64((79 * BASE_9) / 100); + xMintFeeSteak[2] = uint64((80 * BASE_9) / 100); + + int64[] memory yMintFeeSteak = new int64[](3); + yMintFeeSteak[0] = int64(0); + yMintFeeSteak[1] = int64(0); + yMintFeeSteak[2] = int64(uint64(MAX_MINT_FEE)); + + uint64[] memory xBurnFeeSteak = new uint64[](3); + xBurnFeeSteak[0] = uint64(BASE_9); + xBurnFeeSteak[1] = uint64((31 * BASE_9) / 100); + xBurnFeeSteak[2] = uint64((30 * BASE_9) / 100); + + int64[] memory yBurnFeeSteak = new int64[](3); + yBurnFeeSteak[0] = int64(0); + yBurnFeeSteak[1] = int64(0); + yBurnFeeSteak[2] = int64(uint64(MAX_BURN_FEE)); + + bytes memory oracleConfig; + { + bytes memory readData = abi.encode(0x025106374196586E8BC91eE8818dD7B0Efd2B78B, BASE_18); + bytes memory targetData = abi.encode( + IMorphoOracle(0x025106374196586E8BC91eE8818dD7B0Efd2B78B).price() / BASE_18 + ); + oracleConfig = abi.encode( + Storage.OracleReadType.MORPHO_ORACLE, + Storage.OracleReadType.MAX, + readData, + targetData, + abi.encode(USER_PROTECTION_STEAK_USDC, FIREWALL_BURN_RATIO_STEAK_USDC) + ); + oracleConfigSTEAK = oracleConfig; + } + collaterals[1] = CollateralSetupProd( + STEAK_USDC, + oracleConfig, + xMintFeeSteak, + yMintFeeSteak, + xBurnFeeSteak, + yBurnFeeSteak + ); + } + + // Setup each collateral + uint256 collateralsLength = collaterals.length; + for (uint256 i; i < collateralsLength; i++) { + CollateralSetupProd memory collateral = collaterals[i]; + transmuter.addCollateral(collateral.token); + transmuter.setOracle(collateral.token, collateral.oracleConfig); + // Mint fees + transmuter.setFees(collateral.token, collateral.xMintFee, collateral.yMintFee, true); + // Burn fees + transmuter.setFees(collateral.token, collateral.xBurnFee, collateral.yBurnFee, false); + transmuter.togglePause(collateral.token, Storage.ActionType.Mint); + transmuter.togglePause(collateral.token, Storage.ActionType.Burn); + } + + // Set whitelist status for bIB01 + bytes memory whitelistData = abi.encode( + WhitelistType.BACKED, + // Keyring whitelist check + abi.encode(address(0x9391B14dB2d43687Ea1f6E546390ED4b20766c46)) + ); + transmuter.setWhitelistStatus(BIB01, 1, whitelistData); + transmuter.toggleWhitelist(Storage.WhitelistType.BACKED, WHALE_USDA); + + transmuter.toggleTrusted(NEW_DEPLOYER, Storage.TrustedType.Seller); + transmuter.toggleTrusted(NEW_KEEPER, Storage.TrustedType.Seller); + + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + GETTERS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testUnit_UpgradeUSDA_AgToken() external { + assertEq(address(transmuter.agToken()), 0x0000206329b97DB379d5E1Bf586BbDB969C63274); + } + + function testUnit_UpgradeUSDA_GetCollateralList() external { + address[] memory collateralList = transmuter.getCollateralList(); + assertEq(collateralList.length, 3); + assertEq(collateralList[0], address(USDC)); + assertEq(collateralList[1], address(BIB01)); + assertEq(collateralList[2], address(STEAK_USDC)); + } + + function testUnit_UpgradeUSDA_GetCollateralInfo() external { + { + Storage.Collateral memory collatInfoUSDC = transmuter.getCollateralInfo(address(USDC)); + assertEq(collatInfoUSDC.isManaged, 0); + assertEq(collatInfoUSDC.isMintLive, 1); + assertEq(collatInfoUSDC.isBurnLive, 1); + assertEq(collatInfoUSDC.decimals, 6); + assertEq(collatInfoUSDC.onlyWhitelisted, 0); + assertEq(collatInfoUSDC.oracleConfig, oracleConfigUSDC); + assertEq(collatInfoUSDC.whitelistData.length, 0); + assertEq(collatInfoUSDC.managerData.subCollaterals.length, 0); + assertEq(collatInfoUSDC.managerData.config.length, 0); + + { + assertEq(collatInfoUSDC.xFeeMint.length, 1); + assertEq(collatInfoUSDC.yFeeMint.length, 1); + assertEq(collatInfoUSDC.xFeeMint[0], 0); + assertEq(collatInfoUSDC.yFeeMint[0], 0); + } + { + assertEq(collatInfoUSDC.xFeeBurn.length, 1); + assertEq(collatInfoUSDC.yFeeBurn.length, 1); + assertEq(collatInfoUSDC.xFeeBurn[0], 1000000000); + assertEq(collatInfoUSDC.yFeeBurn[0], 0); + } + } + + { + Storage.Collateral memory collatInfoIB01 = transmuter.getCollateralInfo(address(BIB01)); + assertEq(collatInfoIB01.isManaged, 0); + assertEq(collatInfoIB01.isMintLive, 1); + assertEq(collatInfoIB01.isBurnLive, 1); + assertEq(collatInfoIB01.decimals, 18); + assertEq(collatInfoIB01.onlyWhitelisted, 1); + assertEq(collatInfoIB01.oracleConfig, oracleConfigIB01); + { + (Storage.WhitelistType whitelist, bytes memory data) = abi.decode( + collatInfoIB01.whitelistData, + (Storage.WhitelistType, bytes) + ); + address keyringGuard = abi.decode(data, (address)); + assertEq(uint8(whitelist), uint8(Storage.WhitelistType.BACKED)); + assertEq(keyringGuard, 0x9391B14dB2d43687Ea1f6E546390ED4b20766c46); + } + assertEq(collatInfoIB01.managerData.subCollaterals.length, 0); + assertEq(collatInfoIB01.managerData.config.length, 0); + + { + assertEq(collatInfoIB01.xFeeMint.length, 3); + assertEq(collatInfoIB01.yFeeMint.length, 3); + assertEq(collatInfoIB01.xFeeMint[0], 0); + assertEq(collatInfoIB01.xFeeMint[1], 490000000); + assertEq(collatInfoIB01.xFeeMint[2], 500000000); + assertEq(collatInfoIB01.yFeeMint[0], 0); + assertEq(collatInfoIB01.yFeeMint[1], 0); + assertEq(collatInfoIB01.yFeeMint[2], 999999999999); + } + { + assertEq(collatInfoIB01.xFeeBurn.length, 3); + assertEq(collatInfoIB01.yFeeBurn.length, 3); + assertEq(collatInfoIB01.xFeeBurn[0], 1000000000); + assertEq(collatInfoIB01.xFeeBurn[1], 160000000); + assertEq(collatInfoIB01.xFeeBurn[2], 150000000); + assertEq(collatInfoIB01.yFeeBurn[0], 5000000); + assertEq(collatInfoIB01.yFeeBurn[1], 5000000); + assertEq(collatInfoIB01.yFeeBurn[2], 999000000); + } + } + + { + Storage.Collateral memory collatInfoSTEAK = transmuter.getCollateralInfo(address(STEAK_USDC)); + assertEq(collatInfoSTEAK.isManaged, 0); + assertEq(collatInfoSTEAK.isMintLive, 1); + assertEq(collatInfoSTEAK.isBurnLive, 1); + assertEq(collatInfoSTEAK.decimals, 18); + assertEq(collatInfoSTEAK.onlyWhitelisted, 0); + assertEq(collatInfoSTEAK.oracleConfig, oracleConfigSTEAK); + assertEq(collatInfoSTEAK.managerData.subCollaterals.length, 0); + assertEq(collatInfoSTEAK.managerData.config.length, 0); + + { + assertEq(collatInfoSTEAK.xFeeMint.length, 3); + assertEq(collatInfoSTEAK.yFeeMint.length, 3); + assertEq(collatInfoSTEAK.xFeeMint[0], 0); + assertEq(collatInfoSTEAK.xFeeMint[1], 790000000); + assertEq(collatInfoSTEAK.xFeeMint[2], 800000000); + assertEq(collatInfoSTEAK.yFeeMint[0], 0); + assertEq(collatInfoSTEAK.yFeeMint[1], 0); + assertEq(collatInfoSTEAK.yFeeMint[2], 999999999999); + } + { + assertEq(collatInfoSTEAK.xFeeBurn.length, 3); + assertEq(collatInfoSTEAK.yFeeBurn.length, 3); + assertEq(collatInfoSTEAK.xFeeBurn[0], 1000000000); + assertEq(collatInfoSTEAK.yFeeBurn[0], 0); + assertEq(collatInfoSTEAK.xFeeBurn[1], 310000000); + assertEq(collatInfoSTEAK.yFeeBurn[1], 0); + assertEq(collatInfoSTEAK.xFeeBurn[2], 300000000); + assertEq(collatInfoSTEAK.yFeeBurn[2], 999000000); + } + } + } + + function testUnit_UpgradeUSDA_GetCollateralDecimals() external { + assertEq(transmuter.getCollateralDecimals(address(USDC)), 6); + assertEq(transmuter.getCollateralDecimals(address(STEAK_USDC)), 18); + assertEq(transmuter.getCollateralDecimals(address(BIB01)), 18); + } + + function testUnit_UpgradeUSDA_GetCollateralRatio() external { + (uint64 collatRatio, uint256 stablecoinIssued) = transmuter.getCollateralRatio(); + + assertApproxEqRel(collatRatio, 1000173196, BPS * 100); + assertApproxEqRel(stablecoinIssued, 1199993347000000000000, 100 * BPS); + } + + function testUnit_UpgradeUSDA_isTrusted() external { + assertEq(transmuter.isTrusted(address(governor)), false); + assertEq(transmuter.isTrustedSeller(address(governor)), false); + assertEq(transmuter.isTrusted(DEPLOYER), false); + assertEq(transmuter.isTrustedSeller(DEPLOYER), false); + assertEq(transmuter.isTrusted(NEW_DEPLOYER), false); + assertEq(transmuter.isTrustedSeller(NEW_DEPLOYER), true); + assertEq(transmuter.isTrusted(KEEPER), false); + assertEq(transmuter.isTrustedSeller(KEEPER), false); + assertEq(transmuter.isTrusted(NEW_KEEPER), false); + assertEq(transmuter.isTrustedSeller(NEW_KEEPER), true); + } + + function testUnit_UpgradeUSDA_IsWhitelistedForCollateral() external { + assertEq(transmuter.isWhitelistedForCollateral(address(USDC), alice), true); + assertEq(transmuter.isWhitelistedForCollateral(address(BIB01), alice), false); + assertEq(transmuter.isWhitelistedForCollateral(address(STEAK_USDC), alice), true); + vm.startPrank(governor); + transmuter.toggleWhitelist(Storage.WhitelistType.BACKED, alice); + vm.stopPrank(); + assertEq(transmuter.isWhitelistedForCollateral(address(BIB01), alice), true); + assertEq(transmuter.isWhitelistedForCollateral(address(BIB01), WHALE_USDA), true); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ORACLE + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testUnit_UpgradeUSDA_getOracleValues_Success() external { + _checkOracleValues(address(USDC), BASE_18, USER_PROTECTION_USDC, FIREWALL_BURN_RATIO_USDC); + _checkOracleValues(address(BIB01), 109480000000000000000, USER_PROTECTION_IB01, FIREWALL_BURN_RATIO_IB01); + _checkOracleValues( + address(STEAK_USDC), + 1.015 ether, + USER_PROTECTION_STEAK_USDC, + FIREWALL_BURN_RATIO_STEAK_USDC + ); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + MINT + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testFuzz_UpgradeUSDA_QuoteMintExactInput_Reflexivity(uint256 amountIn, uint256 fromToken) public { + fromToken = bound(fromToken, 0, transmuter.getCollateralList().length - 1); + address collateral = transmuter.getCollateralList()[fromToken]; + amountIn = bound(amountIn, BASE_6, collateral != USDC ? 1000 * BASE_18 : BASE_6 * 1e6); + + uint256 amountStable = transmuter.quoteIn(amountIn, collateral, address(USDA)); + uint256 amountInReflexive = transmuter.quoteOut(amountStable, collateral, address(USDA)); + assertApproxEqRel(amountIn, amountInReflexive, BPS * 10); + } + + function testFuzz_UpgradeUSDA_QuoteMintExactInput_Independent( + uint256 amountIn, + uint256 splitProportion, + uint256 fromToken + ) public { + fromToken = bound(fromToken, 0, transmuter.getCollateralList().length - 1); + address collateral = transmuter.getCollateralList()[fromToken]; + amountIn = bound(amountIn, BASE_6, collateral != USDC ? 1000 * BASE_18 : BASE_6 * 1e6); + splitProportion = bound(splitProportion, 0, BASE_9); + + uint256 amountStable = transmuter.quoteIn(amountIn, collateral, address(USDA)); + uint256 amountInSplit1 = (amountIn * splitProportion) / BASE_9; + amountInSplit1 = amountInSplit1 == 0 ? 1 : amountInSplit1; + uint256 amountStableSplit1 = transmuter.quoteIn(amountInSplit1, collateral, address(USDA)); + // do the swap to update the system + _mintExactInput(alice, collateral, amountInSplit1, amountStableSplit1); + uint256 amountStableSplit2 = transmuter.quoteIn(amountIn - amountInSplit1, collateral, address(USDA)); + assertApproxEqRel(amountStableSplit1 + amountStableSplit2, amountStable, BPS * 10); + } + + function testFuzz_UpgradeUSDA_MintExactOutput(uint256 stableAmount, uint256 fromToken) public { + fromToken = bound(fromToken, 0, transmuter.getCollateralList().length - 1); + address collateral = transmuter.getCollateralList()[fromToken]; + stableAmount = bound(stableAmount, BASE_18, BASE_6 * 1e18); + + uint256 prevBalanceStable = USDA.balanceOf(alice); + uint256 prevTransmuterCollat = IERC20(collateral).balanceOf(address(transmuter)); + uint256 prevAgTokenSupply = IERC20(USDA).totalSupply(); + (uint256 prevStableAmountCollat, uint256 prevStableAmount) = transmuter.getIssuedByCollateral(collateral); + + uint256 amountIn = transmuter.quoteOut(stableAmount, collateral, address(USDA)); + if (amountIn == 0 || stableAmount == 0) return; + _mintExactOutput(alice, collateral, stableAmount, amountIn); + + uint256 balanceStable = USDA.balanceOf(alice); + + assertEq(balanceStable, prevBalanceStable + stableAmount); + assertEq(USDA.totalSupply(), prevAgTokenSupply + stableAmount); + assertEq(IERC20(collateral).balanceOf(alice), 0); + assertEq(IERC20(collateral).balanceOf(address(transmuter)), prevTransmuterCollat + amountIn); + + (uint256 newStableAmountCollat, uint256 newStableAmount) = transmuter.getIssuedByCollateral(collateral); + + assertApproxEqAbs(newStableAmountCollat, prevStableAmountCollat + stableAmount, 1 wei); + assertApproxEqAbs(newStableAmount, prevStableAmount + stableAmount, 1 wei); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + BURN + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function testFuzz_UpgradeUSDA_QuoteBurnExactInput_Independent( + uint256 amountStable, + uint256 splitProportion + ) public { + amountStable = bound(amountStable, 1, BASE_6); + splitProportion = bound(splitProportion, 0, BASE_9); + + uint256 amountOut = transmuter.quoteIn(amountStable, address(USDA), USDC); + uint256 amountStableSplit1 = (amountStable * splitProportion) / BASE_9; + amountStableSplit1 = amountStableSplit1 == 0 ? 1 : amountStableSplit1; + uint256 amountOutSplit1 = transmuter.quoteIn(amountStableSplit1, address(USDA), USDC); + // do the swap to update the system + _burnExactInput(WHALE_USDA, USDC, amountStableSplit1, amountOutSplit1); + uint256 amountOutSplit2 = transmuter.quoteIn(amountStable - amountStableSplit1, address(USDA), USDC); + assertApproxEqRel(amountOutSplit1 + amountOutSplit2, amountOut, BPS * 10); + } + + function testFuzz_UpgradeUSDA_BurnExactOutput(uint256 amountOut) public { + amountOut = bound(amountOut, 1, BASE_6); + + uint256 prevBalanceStable = USDA.balanceOf(WHALE_USDA); + uint256 prevBalanceUSDC = IERC20(USDC).balanceOf(WHALE_USDA); + uint256 prevTransmuterCollat = IERC20(USDC).balanceOf(address(transmuter)); + uint256 prevAgTokenSupply = IERC20(USDA).totalSupply(); + (uint256 prevStableAmountCollat, uint256 prevStableAmount) = transmuter.getIssuedByCollateral(USDC); + + uint256 stableAmount = transmuter.quoteOut(amountOut, address(USDA), USDC); + if (amountOut == 0 || stableAmount == 0) return; + _burnExactOutput(WHALE_USDA, USDC, amountOut, stableAmount); + + uint256 balanceStable = USDA.balanceOf(WHALE_USDA); + + assertEq(balanceStable, prevBalanceStable - stableAmount); + assertEq(USDA.totalSupply(), prevAgTokenSupply - stableAmount); + assertEq(IERC20(USDC).balanceOf(WHALE_USDA), amountOut + prevBalanceUSDC); + assertEq(IERC20(USDC).balanceOf(address(transmuter)), prevTransmuterCollat - amountOut); + + (uint256 newStableAmountCollat, uint256 newStableAmount) = transmuter.getIssuedByCollateral(USDC); + + assertApproxEqAbs(newStableAmountCollat, prevStableAmountCollat - stableAmount, 1 wei); + assertApproxEqAbs(newStableAmount, prevStableAmount - stableAmount, 1 wei); + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + UTILS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function _mintExactOutput( + address owner, + address tokenIn, + uint256 amountStable, + uint256 estimatedAmountIn + ) internal { + vm.startPrank(owner); + deal(tokenIn, owner, estimatedAmountIn); + IERC20(tokenIn).approve(address(transmuter), type(uint256).max); + transmuter.swapExactOutput(amountStable, estimatedAmountIn, tokenIn, address(USDA), owner, block.timestamp * 2); + vm.stopPrank(); + } + + function _mintExactInput(address owner, address tokenIn, uint256 amountIn, uint256 estimatedStable) internal { + vm.startPrank(owner); + deal(tokenIn, owner, amountIn); + IERC20(tokenIn).approve(address(transmuter), type(uint256).max); + transmuter.swapExactInput(amountIn, estimatedStable, tokenIn, address(USDA), owner, block.timestamp * 2); + vm.stopPrank(); + } + + function _burnExactInput( + address owner, + address tokenOut, + uint256 amountStable, + uint256 estimatedAmountOut + ) internal returns (bool burnMoreThanHad) { + // we need to increase the balance because fees are negative and we need to transfer + // more than what we received with the mint + if (IERC20(tokenOut).balanceOf(address(transmuter)) < estimatedAmountOut) { + deal(tokenOut, address(transmuter), estimatedAmountOut); + burnMoreThanHad = true; + } + + vm.startPrank(owner); + transmuter.swapExactInput(amountStable, estimatedAmountOut, address(USDA), tokenOut, owner, 0); + vm.stopPrank(); + } + + function _burnExactOutput( + address owner, + address tokenOut, + uint256 amountOut, + uint256 estimatedStable + ) internal returns (bool) { + // _logIssuedCollateral(); + vm.startPrank(owner); + (uint256 maxAmount, ) = transmuter.getIssuedByCollateral(tokenOut); + uint256 balanceStableOwner = USDA.balanceOf(owner); + if (estimatedStable > maxAmount) vm.expectRevert(); + else if (estimatedStable > balanceStableOwner) vm.expectRevert("ERC20: burn amount exceeds balance"); + transmuter.swapExactOutput(amountOut, estimatedStable, address(USDA), tokenOut, owner, block.timestamp * 2); + if (amountOut > maxAmount) return false; + vm.stopPrank(); + return true; + } + + /*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + CHECKS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ + + function _checkOracleValues( + address collateral, + uint256 targetValue, + uint128 userProtection, + uint128 firewallBurn + ) internal { + (uint256 mint, uint256 burn, uint256 ratio, uint256 minRatio, uint256 redemption) = transmuter.getOracleValues( + collateral + ); + assertApproxEqRel(targetValue, redemption, 200 * BPS); + + if ( + targetValue * (BASE_18 - userProtection) < redemption * BASE_18 && + redemption * BASE_18 < targetValue * (BASE_18 + userProtection) + ) assertEq(burn, targetValue); + else assertEq(burn, redemption); + + if ( + targetValue * (BASE_18 - userProtection) < redemption * BASE_18 && + redemption * BASE_18 < targetValue * (BASE_18 + userProtection) + ) { + assertEq(mint, targetValue); + assertEq(ratio, BASE_18); + } else if (redemption * BASE_18 < targetValue * (BASE_18 - firewallBurn)) { + assertEq(mint, redemption); + assertEq(ratio, (redemption * BASE_18) / targetValue); + } else { + assertEq(mint, redemption); + assertEq(ratio, BASE_18); + } + } +} diff --git a/yarn.lock b/yarn.lock index d29e0b5c..e0e16f7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,11 +2,12 @@ # yarn lockfile v1 -"@angleprotocol/sdk@^3.0.129": - version "3.0.129" - resolved "https://registry.yarnpkg.com/@angleprotocol/sdk/-/sdk-3.0.129.tgz#bf63925e90a3479d1a7fbc229ba174af273e9086" - integrity sha512-OeEx8Tfmk3oHRAbpw9HM27a39b/cdAoj1GccU8y2pYttj6KELq3inIPBxjpdT4lXCoP7meYGpJTVu0Odllq9sw== +"@angleprotocol/sdk@^0.37.1": + version "0.37.1" + resolved "https://npm.pkg.github.com/download/@angleprotocol/sdk/0.37.1/3c2510ecd61d08649d758be4e4e27517bffa3f61#3c2510ecd61d08649d758be4e4e27517bffa3f61" + integrity sha512-KkKa14U2AafSiVbigcJrWexH3uTJ2hmcRh8Bp7SPoYf8CJSOaXzT+HbX0KC0LWDWjZcts+0YOhmjFmUFmy/Rtg== dependencies: + "@apollo/client" "^3.7.17" "@typechain/ethers-v5" "^10.0.0" "@types/lodash" "^4.14.180" ethers "^5.6.4" @@ -17,7 +18,27 @@ lodash "^4.17.21" merkletreejs "^0.3.10" tiny-invariant "^1.1.0" - typechain "^8.0.0" + typechain "^8.3.2" + +"@apollo/client@^3.7.17": + version "3.9.5" + resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.9.5.tgz#502ec191756a7f44788b5f08cbe7b8de594a7656" + integrity sha512-7y+c8MTPU+hhTwvcGVtMMGIgWduzrvG1mz5yJMRyqYbheBkkky3Lki6ADWVSBXG1lZoOtPYvB2zDgVfKb2HSsw== + dependencies: + "@graphql-typed-document-node/core" "^3.1.1" + "@wry/caches" "^1.0.0" + "@wry/equality" "^0.5.6" + "@wry/trie" "^0.5.0" + graphql-tag "^2.12.6" + hoist-non-react-statics "^3.3.2" + optimism "^0.18.0" + prop-types "^15.7.2" + rehackt "0.0.5" + response-iterator "^0.2.6" + symbol-observable "^4.0.0" + ts-invariant "^0.10.3" + tslib "^2.3.0" + zen-observable-ts "^1.2.5" "@babel/code-frame@^7.0.0": version "7.16.7" @@ -396,6 +417,11 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" +"@graphql-typed-document-node/core@^3.1.1": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" + integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== + "@noble/curves@1.3.0", "@noble/curves@~1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.3.0.tgz#01be46da4fd195822dab821e72f71bf4aeec635e" @@ -455,6 +481,41 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" integrity sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA== +"@wry/caches@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@wry/caches/-/caches-1.0.1.tgz#8641fd3b6e09230b86ce8b93558d44cf1ece7e52" + integrity sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA== + dependencies: + tslib "^2.3.0" + +"@wry/context@^0.7.0": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.7.4.tgz#e32d750fa075955c4ab2cfb8c48095e1d42d5990" + integrity sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ== + dependencies: + tslib "^2.3.0" + +"@wry/equality@^0.5.6": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.5.7.tgz#72ec1a73760943d439d56b7b1e9985aec5d497bb" + integrity sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw== + dependencies: + tslib "^2.3.0" + +"@wry/trie@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@wry/trie/-/trie-0.4.3.tgz#077d52c22365871bf3ffcbab8e95cb8bc5689af4" + integrity sha512-I6bHwH0fSf6RqQcnnXLJKhkSXG45MFral3GxPaY4uAl0LYDZM+YDVDAiU9bYwjTuysy1S0IeecWtmq1SZA3M1w== + dependencies: + tslib "^2.3.0" + +"@wry/trie@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@wry/trie/-/trie-0.5.0.tgz#11e783f3a53f6e4cd1d42d2d1323f5bc3fa99c94" + integrity sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA== + dependencies: + tslib "^2.3.0" + aes-js@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" @@ -907,6 +968,13 @@ graphql-request@^3.6.1: extract-files "^9.0.0" form-data "^3.0.0" +graphql-tag@^2.12.6: + version "2.12.6" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1" + integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg== + dependencies: + tslib "^2.1.0" + graphql@^15.7.1: version "15.8.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" @@ -939,6 +1007,13 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -990,9 +1065,9 @@ js-sha3@0.8.0, js-sha3@^0.8.0: resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== -js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^4.1.0: @@ -1067,6 +1142,13 @@ lodash@^4.17.15, lodash@^4.17.21: resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" @@ -1161,6 +1243,11 @@ number-to-bn@1.7.0: bn.js "4.11.6" strip-hex-prefix "1.0.0" +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + once@^1.3.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" @@ -1168,6 +1255,16 @@ once@^1.3.0: dependencies: wrappy "1" +optimism@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.18.0.tgz#e7bb38b24715f3fdad8a9a7fc18e999144bbfa63" + integrity sha512-tGn8+REwLRNFnb9WmcY5IfpOqeX2kpaYJ1s6Ae3mn12AeydLkR3j+jSCmVQFoXqU8D41PAJ1RG1rCRNWmNZVmQ== + dependencies: + "@wry/caches" "^1.0.0" + "@wry/context" "^0.7.0" + "@wry/trie" "^0.4.3" + tslib "^2.3.0" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -1221,6 +1318,15 @@ prettier@^2.0.0, prettier@^2.3.1, prettier@^2.8.3: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== +prop-types@^15.7.2: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + punycode@^2.1.0: version "2.1.1" resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" @@ -1233,6 +1339,11 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" +react-is@^16.13.1, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" @@ -1247,6 +1358,11 @@ reduce-flatten@^2.0.0: resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== +rehackt@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/rehackt/-/rehackt-0.0.5.tgz#184c82ea369d5b0b989ede0593ebea8b2bcfb1d6" + integrity sha512-BI1rV+miEkaHj8zd2n+gaMgzu/fKz7BGlb4zZ6HAiY9adDmJMkaDcmuXlJFv0eyKUob+oszs3/2gdnXUrzx2Tg== + require-from-string@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" @@ -1257,6 +1373,11 @@ resolve-from@^4.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +response-iterator@^0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/response-iterator/-/response-iterator-0.2.6.tgz#249005fb14d2e4eeb478a3f735a28fd8b4c9f3da" + integrity sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw== + safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -1369,6 +1490,11 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +symbol-observable@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" + integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== + table-layout@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04" @@ -1425,7 +1551,19 @@ ts-essentials@^7.0.1: resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38" integrity sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ== -typechain@^8.0.0: +ts-invariant@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.10.3.tgz#3e048ff96e91459ffca01304dbc7f61c1f642f6c" + integrity sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ== + dependencies: + tslib "^2.1.0" + +tslib@^2.1.0, tslib@^2.3.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + +typechain@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/typechain/-/typechain-8.3.2.tgz#1090dd8d9c57b6ef2aed3640a516bdbf01b00d73" integrity sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q== @@ -1522,3 +1660,15 @@ yallist@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +zen-observable-ts@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz#6c6d9ea3d3a842812c6e9519209365a122ba8b58" + integrity sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg== + dependencies: + zen-observable "0.8.15" + +zen-observable@0.8.15: + version "0.8.15" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" + integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==