Repository: aave/gho-core Branch: main Commit: c6335a0bb9cb Files: 307 Total size: 1.5 MB Directory structure: gitextract_qacr2h46/ ├── .github/ │ ├── CODEOWNERS │ └── workflows/ │ ├── certora-gho-505.yml │ ├── certora-gho.yml │ ├── certora-gsm-4626.yml │ ├── certora-gsm.yml │ ├── certora-steward.yml │ ├── dependency-review.yml │ ├── node.js.yml │ └── sync-issue.yml ├── .gitignore ├── .gitmodules ├── .husky/ │ ├── .gitignore │ └── pre-commit ├── .prettierignore ├── .prettierrc ├── .solcover.js ├── .vscode/ │ └── settings.json ├── LICENSE ├── README.md ├── certora/ │ ├── gho/ │ │ ├── Makefile │ │ ├── applyHarness.patch │ │ ├── conf/ │ │ │ ├── verifyFlashMinter.conf │ │ │ ├── verifyGhoAToken.conf │ │ │ ├── verifyGhoDiscountRateStrategy.conf │ │ │ ├── verifyGhoToken.conf │ │ │ ├── verifyGhoVariableDebtToken-rayMulDiv-summarization.conf │ │ │ ├── verifyGhoVariableDebtToken.conf │ │ │ ├── verifyGhoVariableDebtTokenInternal.conf │ │ │ ├── verifyGhoVariableDebtToken_specialBranch.conf │ │ │ ├── verifyGhoVariableDebtToken_summarized.conf │ │ │ └── verifyUpgradeableGhoToken.conf │ │ ├── harness/ │ │ │ ├── DummyERC20A.sol │ │ │ ├── DummyERC20B.sol │ │ │ ├── DummyERC20Impl.sol │ │ │ ├── DummyERC20WithTimedBalanceOf.sol │ │ │ ├── DummyPool.sol │ │ │ ├── GhoDiscountRateStrategyHarness.sol │ │ │ ├── GhoTokenHarness.sol │ │ │ ├── MockFlashBorrower.sol │ │ │ ├── UpgradeableGhoTokenHarness.sol │ │ │ ├── ghoVariableDebtTokenHarness.sol │ │ │ └── ghoVariableDebtTokenHarnessInternal.sol │ │ ├── munged/ │ │ │ └── .gitignore │ │ └── specs/ │ │ ├── VariableDebtToken.spec │ │ ├── erc20.spec │ │ ├── flashMinter.spec │ │ ├── ghoAToken.spec │ │ ├── ghoDiscountRateStrategy.spec │ │ ├── ghoToken.spec │ │ ├── ghoVariableDebtToken-rayMulDiv-summarization.spec │ │ ├── ghoVariableDebtToken.spec │ │ ├── ghoVariableDebtTokenInternal.spec │ │ ├── ghoVariableDebtToken_summarized.spec │ │ ├── set-natspec.json │ │ ├── set.spec │ │ └── summarizations.spec │ ├── gsm/ │ │ ├── conf/ │ │ │ ├── gsm/ │ │ │ │ ├── FixedFeeStrategy.conf │ │ │ │ ├── OracleSwapFreezer.conf │ │ │ │ ├── balances-buy.conf │ │ │ │ ├── balances-sell.conf │ │ │ │ ├── fees-buy.conf │ │ │ │ ├── fees-sell.conf │ │ │ │ ├── finishedRules.conf │ │ │ │ ├── getAmount_properties.conf │ │ │ │ ├── gho-assetToGhoInvertibility.conf │ │ │ │ ├── gho-fixedPriceStrategy.conf │ │ │ │ ├── gho-gsm-2.conf │ │ │ │ ├── gho-gsm.conf │ │ │ │ ├── gho-gsm_inverse.conf │ │ │ │ └── optimality.conf │ │ │ └── gsm4626/ │ │ │ ├── balances-buy-4626.conf │ │ │ ├── balances-sell-4626.conf │ │ │ ├── fees-buy-4626.conf │ │ │ ├── fees-sell-4626.conf │ │ │ ├── finishedRules4626.conf │ │ │ ├── getAmount_4626_properties.conf │ │ │ ├── gho-assetToGhoInvertibility4626.conf │ │ │ ├── gho-fixedPriceStrategy4626.conf │ │ │ ├── gho-gsm4626-2.conf │ │ │ ├── gho-gsm4626.conf │ │ │ ├── gho-gsm_4626_inverse.conf │ │ │ └── optimality4626.conf │ │ ├── harness/ │ │ │ ├── DiffHelper.sol │ │ │ ├── DummyERC20A.sol │ │ │ ├── DummyERC20B.sol │ │ │ ├── DummyERC20Impl.sol │ │ │ ├── ERC20Helper.sol │ │ │ ├── FixedFeeStrategyHarness.sol │ │ │ ├── FixedPriceStrategy4626Harness.sol │ │ │ ├── FixedPriceStrategyHarness.sol │ │ │ ├── Gsm4626Harness.sol │ │ │ ├── GsmHarness.sol │ │ │ └── OracleSwapFreezerHarness.sol │ │ ├── munged/ │ │ │ └── .gitignore │ │ └── specs/ │ │ ├── GsmMethods/ │ │ │ ├── aave_fee_limits.spec │ │ │ ├── aave_price_fee_limits.spec │ │ │ ├── aave_price_fee_limits_strict.spec │ │ │ ├── aave_price_limits.spec │ │ │ ├── erc20.spec │ │ │ ├── erc4626.spec │ │ │ ├── methods4626_base.spec │ │ │ ├── methods_base-Martin.spec │ │ │ ├── methods_base.spec │ │ │ ├── methods_divint_summary.spec │ │ │ └── shared.spec │ │ ├── gsm/ │ │ │ ├── AssetToGhoInvertibility.spec │ │ │ ├── FixedFeeStrategy.spec │ │ │ ├── FixedPriceStrategy.spec │ │ │ ├── OracleSwapFreezer.spec │ │ │ ├── balances-buy.spec │ │ │ ├── balances-sell.spec │ │ │ ├── fees-buy.spec │ │ │ ├── fees-sell.spec │ │ │ ├── getAmount_properties.spec │ │ │ ├── gho-gsm-2.spec │ │ │ ├── gho-gsm-Buy.spec │ │ │ ├── gho-gsm-finishedRules.spec │ │ │ ├── gho-gsm.spec │ │ │ ├── gho-gsm_inverse.spec │ │ │ └── optimality.spec │ │ └── gsm4626/ │ │ ├── AssetToGhoInvertibility4626.spec │ │ ├── FixedPriceStrategy4626.spec │ │ ├── balances-buy-4626.spec │ │ ├── balances-sell-4626.spec │ │ ├── fees-buy-4626.spec │ │ ├── fees-sell-4626.spec │ │ ├── getAmount_4626_properties.spec │ │ ├── gho-gsm-finishedRules4626.spec │ │ ├── gho-gsm4626-2.spec │ │ ├── gho-gsm4626.spec │ │ ├── gho-gsm_4626_inverse.spec │ │ └── optimality4626.spec │ └── steward/ │ ├── Makefile │ ├── applyHarness.patch │ ├── conf/ │ │ ├── GhoAaveSteward.conf │ │ ├── GhoBucketSteward.conf │ │ ├── GhoCcipSteward.conf │ │ └── GhoGsmSteward.conf │ ├── harness/ │ │ ├── GhoAaveSteward_Harness.sol │ │ ├── GhoCcipSteward_Harness.sol │ │ ├── GhoGsmSteward_Harness.sol │ │ └── GhoStewardV2_Harness.sol │ ├── munged/ │ │ └── .gitignore │ └── specs/ │ ├── GhoAaveSteward.spec │ ├── GhoBucketSteward.spec │ ├── GhoCcipSteward.spec │ └── GhoGsmSteward.spec ├── deploy/ │ ├── 00_deploy_gho_token.ts │ ├── 01_deploy_gho_oracle.ts │ ├── 02_deploy_gho_atoken.ts │ ├── 03_deploy_gho_stable_debt.ts │ ├── 04_deploy_gho_variable_debt.ts │ ├── 05_deploy_gho_interest_rate.ts │ ├── 06_deploy_gho_discount_rate.ts │ ├── 07_deploy_stakedAave_upgrade.ts │ ├── 08_deploy_gho_flashminter.ts │ └── 09_deploy_uighodataprovider.ts ├── docs/ │ └── gho-stewards.md ├── foundry.toml ├── hardhat.config.ts ├── helpers/ │ ├── config.ts │ ├── constants.ts │ ├── contract-getters.ts │ ├── hardhat-config.ts │ ├── misc-utils.ts │ └── types.ts ├── package.json ├── remappings.txt ├── setup-test-env.sh ├── slither.config.json ├── src/ │ ├── contracts/ │ │ ├── facilitators/ │ │ │ ├── aave/ │ │ │ │ ├── interestStrategy/ │ │ │ │ │ ├── FixedRateStrategyFactory.sol │ │ │ │ │ ├── GhoDiscountRateStrategy.sol │ │ │ │ │ ├── GhoInterestRateStrategy.sol │ │ │ │ │ ├── ZeroDiscountRateStrategy.sol │ │ │ │ │ └── interfaces/ │ │ │ │ │ ├── IFixedRateStrategyFactory.sol │ │ │ │ │ └── IGhoDiscountRateStrategy.sol │ │ │ │ ├── misc/ │ │ │ │ │ ├── UiGhoDataProvider.sol │ │ │ │ │ └── interfaces/ │ │ │ │ │ └── IUiGhoDataProvider.sol │ │ │ │ ├── oracle/ │ │ │ │ │ └── GhoOracle.sol │ │ │ │ └── tokens/ │ │ │ │ ├── GhoAToken.sol │ │ │ │ ├── GhoStableDebtToken.sol │ │ │ │ ├── GhoVariableDebtToken.sol │ │ │ │ ├── base/ │ │ │ │ │ └── ScaledBalanceTokenBase.sol │ │ │ │ └── interfaces/ │ │ │ │ ├── IGhoAToken.sol │ │ │ │ └── IGhoVariableDebtToken.sol │ │ │ ├── flashMinter/ │ │ │ │ ├── GhoFlashMinter.sol │ │ │ │ └── interfaces/ │ │ │ │ └── IGhoFlashMinter.sol │ │ │ └── gsm/ │ │ │ ├── Gsm.sol │ │ │ ├── Gsm4626.sol │ │ │ ├── dependencies/ │ │ │ │ └── chainlink/ │ │ │ │ └── AutomationCompatibleInterface.sol │ │ │ ├── feeStrategy/ │ │ │ │ ├── FixedFeeStrategy.sol │ │ │ │ ├── FixedFeeStrategyFactory.sol │ │ │ │ └── interfaces/ │ │ │ │ ├── IFixedFeeStrategyFactory.sol │ │ │ │ └── IGsmFeeStrategy.sol │ │ │ ├── interfaces/ │ │ │ │ ├── IGsm.sol │ │ │ │ └── IGsm4626.sol │ │ │ ├── misc/ │ │ │ │ ├── GsmRegistry.sol │ │ │ │ ├── IGsmRegistry.sol │ │ │ │ ├── SampleLiquidator.sol │ │ │ │ └── SampleSwapFreezer.sol │ │ │ ├── priceStrategy/ │ │ │ │ ├── FixedPriceStrategy.sol │ │ │ │ ├── FixedPriceStrategy4626.sol │ │ │ │ └── interfaces/ │ │ │ │ └── IGsmPriceStrategy.sol │ │ │ └── swapFreezer/ │ │ │ └── OracleSwapFreezer.sol │ │ ├── gho/ │ │ │ ├── ERC20.sol │ │ │ ├── GhoToken.sol │ │ │ ├── UpgradeableERC20.sol │ │ │ ├── UpgradeableGhoToken.sol │ │ │ └── interfaces/ │ │ │ ├── IGhoFacilitator.sol │ │ │ └── IGhoToken.sol │ │ └── misc/ │ │ ├── GhoAaveSteward.sol │ │ ├── GhoBucketSteward.sol │ │ ├── GhoCcipSteward.sol │ │ ├── GhoGsmSteward.sol │ │ ├── RiskCouncilControlled.sol │ │ ├── dependencies/ │ │ │ ├── AaveV3-1.sol │ │ │ └── Ccip.sol │ │ └── interfaces/ │ │ ├── IGhoAaveSteward.sol │ │ ├── IGhoBucketSteward.sol │ │ ├── IGhoCcipSteward.sol │ │ └── IGhoGsmSteward.sol │ ├── script/ │ │ ├── DeployGsmLaunch.s.sol │ │ └── ExternalDependencyCompiler.s.sol │ └── test/ │ ├── TestFixedRateStrategyFactory.t.sol │ ├── TestGhoAToken.t.sol │ ├── TestGhoAaveSteward.t.sol │ ├── TestGhoBase.t.sol │ ├── TestGhoBucketSteward.t.sol │ ├── TestGhoCcipSteward.t.sol │ ├── TestGhoDiscountRateStrategy.t.sol │ ├── TestGhoFlashMinter.t.sol │ ├── TestGhoGsmSteward.t.sol │ ├── TestGhoInterestRateStrategy.t.sol │ ├── TestGhoOracle.t.sol │ ├── TestGhoStableDebtToken.t.sol │ ├── TestGhoStewardsForkEthereum.t.sol │ ├── TestGhoStewardsForkRemote.t.sol │ ├── TestGhoToken.t.sol │ ├── TestGhoVariableDebtToken.t.sol │ ├── TestGhoVariableDebtTokenForked.t.sol │ ├── TestGsm.t.sol │ ├── TestGsm4626.t.sol │ ├── TestGsm4626Edge.t.sol │ ├── TestGsmFixedFeeStrategy.sol │ ├── TestGsmFixedPriceStrategy.t.sol │ ├── TestGsmFixedPriceStrategy4626.t.sol │ ├── TestGsmOracleSwapFreezer.t.sol │ ├── TestGsmRegistry.t.sol │ ├── TestGsmSampleLiquidator.t.sol │ ├── TestGsmSampleSwapFreezer.t.sol │ ├── TestGsmSwapEdge.t.sol │ ├── TestGsmSwapFuzz.t.sol │ ├── TestGsmUpgrade.t.sol │ ├── TestUiGhoDataProvider.t.sol │ ├── TestUpgradeableGhoToken.t.sol │ ├── TestZeroDiscountRateStrategy.t.sol │ ├── helpers/ │ │ ├── Constants.sol │ │ ├── DebtUtils.sol │ │ ├── ErrorsLib.sol │ │ └── Events.sol │ └── mocks/ │ ├── MockAclManager.sol │ ├── MockAddressesProvider.sol │ ├── MockConfigurator.sol │ ├── MockERC4626.sol │ ├── MockFlashBorrower.sol │ ├── MockGsmV2.sol │ ├── MockPool.sol │ ├── MockPoolDataProvider.sol │ ├── MockUpgradeable.sol │ ├── MockUpgradeableBurnMintTokenPool.sol │ └── MockUpgradeableLockReleaseTokenPool.sol ├── tasks/ │ ├── main/ │ │ ├── deploy-and-setup.ts │ │ └── gho-testnet-setup.ts │ ├── misc/ │ │ ├── network-check.ts │ │ └── print-all-deployments.ts │ ├── roles/ │ │ └── 00_gho-transfer-ownership.ts │ └── testnet-setup/ │ ├── 00_initialize-gho-reserve.ts │ ├── 01_enable-gho-borrowing.ts │ ├── 02_set-gho-oracle.ts │ ├── 03_add-gho-as-entity.ts │ ├── 04_add-gho-flashminter-as-entity.ts │ ├── 05_set-gho-addresses.ts │ └── 06_upgrade-stkAave.ts ├── test/ │ ├── __setup.test.ts │ ├── basic-borrow.test.ts │ ├── borrow-onBehalf.test.ts │ ├── discount-borrow.test.ts │ ├── discount-rebalance.test.ts │ ├── flashmint.test.ts │ ├── gho-atoken.test.ts │ ├── gho-oracle.test.ts │ ├── gho-stable-debt.test.ts │ ├── gho-token-permit.test.ts │ ├── gho-token-unit.test.ts │ ├── gho-variable-debt.test.ts │ ├── helpers/ │ │ ├── constants.ts │ │ ├── helpers.ts │ │ ├── make-suite.ts │ │ ├── math/ │ │ │ ├── calculations.ts │ │ │ └── wadraymath.ts │ │ ├── tokenization-events.ts │ │ └── user-setup.ts │ ├── initial-entitiy-configuration.test.ts │ ├── initial-reserve-configuration.test.ts │ ├── stkAave-upgrade.test.ts │ └── transfer-stkAave.test.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODEOWNERS ================================================ * @miguelmtzinf @foodaka ================================================ FILE: .github/workflows/certora-gho-505.yml ================================================ name: certora-gho-5.0.5 on: push: branches: - main pull_request: branches: - main workflow_dispatch: jobs: verify: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: recursive - name: Install python uses: actions/setup-python@v5 with: { python-version: 3.9 } - name: Install java uses: actions/setup-java@v4 with: { distribution: 'zulu', java-version: '11', java-package: jre } - name: Install certora cli run: pip install certora-cli==5.0.5 - name: Install solc run: | wget https://github.com/ethereum/solidity/releases/download/v0.8.10/solc-static-linux chmod +x solc-static-linux sudo mv solc-static-linux /usr/local/bin/solc8.10 - name: Verify rule ${{ matrix.rule }} run: | cd certora/gho touch applyHarness.patch make munged cd ../.. certoraRun certora/gho/conf/${{ matrix.rule }} env: CERTORAKEY: ${{ secrets.CERTORAKEY }} strategy: fail-fast: false max-parallel: 16 matrix: rule: - verifyGhoVariableDebtToken_specialBranch.conf --rule sendersDiscountPercentCannotIncrease ================================================ FILE: .github/workflows/certora-gho.yml ================================================ name: certora-gho on: push: branches: - main pull_request: branches: - main workflow_dispatch: jobs: verify: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: recursive - name: Install python uses: actions/setup-python@v5 with: { python-version: 3.9 } - name: Install java uses: actions/setup-java@v4 with: { distribution: 'zulu', java-version: '11', java-package: jre } - name: Install certora cli run: pip install certora-cli==4.13.1 - name: Install solc run: | wget https://github.com/ethereum/solidity/releases/download/v0.8.10/solc-static-linux chmod +x solc-static-linux sudo mv solc-static-linux /usr/local/bin/solc8.10 - name: Verify rule ${{ matrix.rule }} run: | cd certora/gho touch applyHarness.patch make munged cd ../.. certoraRun certora/gho/conf/${{ matrix.rule }} env: CERTORAKEY: ${{ secrets.CERTORAKEY }} strategy: fail-fast: false max-parallel: 16 matrix: rule: - verifyUpgradeableGhoToken.conf - verifyGhoToken.conf - verifyGhoAToken.conf --rule noMint noBurn noTransfer transferUnderlyingToCantExceedCapacity totalSupplyAlwaysZero userBalanceAlwaysZero level_does_not_decrease_after_transferUnderlyingTo_followed_by_handleRepayment - verifyGhoDiscountRateStrategy.conf --rule equivalenceOfWadMulCVLAndWadMulSol maxDiscountForHighDiscountTokenBalance zeroDiscountForSmallDiscountTokenBalance partialDiscountForIntermediateTokenBalance limitOnDiscountRate - verifyFlashMinter.conf --rule balanceOfFlashMinterGrows integrityOfTreasurySet integrityOfFeeSet availableLiquidityDoesntChange integrityOfDistributeFeesToTreasury feeSimulationEqualsActualFee - verifyGhoVariableDebtToken.conf --rule user_index_after_mint user_index_ge_one_ray nonzeroNewDiscountToken - verifyGhoVariableDebtToken.conf --rule accumulated_interest_increase_after_mint - verifyGhoVariableDebtToken.conf --rule userCantNullifyItsDebt - verifyGhoVariableDebtToken.conf --rule discountCantExceedDiscountRate - verifyGhoVariableDebtToken.conf --rule onlyMintForUserCanIncreaseUsersBalance - verifyGhoVariableDebtToken.conf --rule discountCantExceed100Percent - verifyGhoVariableDebtToken.conf --rule disallowedFunctionalities nonMintFunctionCantIncreaseBalance nonMintFunctionCantIncreaseScaledBalance debtTokenIsNotTransferable onlyCertainFunctionsCanModifyScaledBalance userAccumulatedDebtInterestWontDecrease integrityOfMint_updateDiscountRate integrityOfMint_updateIndex integrityOfMint_updateScaledBalance_fixedIndex integrityOfMint_userIsolation integrityMint_atoken integrityOfBurn_updateDiscountRate integrityOfBurn_updateIndex burnZeroDoesntChangeBalance integrityOfBurn_fullRepay_concrete integrityOfBurn_userIsolation integrityOfUpdateDiscountDistribution_updateIndex integrityOfUpdateDiscountDistribution_userIsolation integrityOfRebalanceUserDiscountPercent_updateDiscountRate integrityOfRebalanceUserDiscountPercent_updateIndex integrityOfRebalanceUserDiscountPercent_userIsolation integrityOfBalanceOf_fullDiscount integrityOfBalanceOf_noDiscount integrityOfBalanceOf_zeroScaledBalance burnAllDebtReturnsZeroDebt integrityOfUpdateDiscountRateStrategy user_index_up_to_date - verifyGhoVariableDebtToken_summarized.conf --rule accrueAlwaysCalleldBeforeRefresh - verifyGhoVariableDebtTokenInternal.conf - verifyGhoVariableDebtToken-rayMulDiv-summarization.conf ================================================ FILE: .github/workflows/certora-gsm-4626.yml ================================================ name: certora-gsm-4626 on: push: branches: - main pull_request: branches: - main workflow_dispatch: jobs: verify: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: recursive - name: Install python uses: actions/setup-python@v5 with: { python-version: 3.9 } - name: Install java uses: actions/setup-java@v4 with: { distribution: 'zulu', java-version: '11', java-package: jre } - name: Install certora cli run: pip install certora-cli==7.14.2 - name: Install solc run: | wget https://github.com/ethereum/solidity/releases/download/v0.8.10/solc-static-linux chmod +x solc-static-linux sudo mv solc-static-linux /usr/local/bin/solc8.10 - name: Verify rule ${{ matrix.rule }} run: | certoraRun certora/gsm/conf/gsm4626/${{ matrix.rule }} env: CERTORAKEY: ${{ secrets.CERTORAKEY }} strategy: fail-fast: false max-parallel: 16 matrix: rule: - gho-gsm_4626_inverse.conf --rule buySellInverse27 buySellInverse26 buySellInverse25 buySellInverse24 buySellInverse23 buySellInverse22 buySellInverse21 buySellInverse20 buySellInverse19 - gho-gsm4626.conf --rule enoughULtoBackGhoNonBuySell NonZeroFeeCheckSellAsset NonZeroFeeCheckBuyAsset - balances-buy-4626.conf - balances-sell-4626.conf --rule R1_getAssetAmountForSellAsset_arg_vs_return R1a_buyGhoUpdatesGhoBalanceCorrectly1 R2_getAssetAmountForSellAsset_sellAsset_eq - balances-sell-4626.conf --rule R3a_sellAssetUpdatesAssetBalanceCorrectly - balances-sell-4626.conf --rule R4_buyGhoUpdatesGhoBalanceCorrectly R4a_buyGhoAmountGtGhoBalanceChange - fees-buy-4626.conf - fees-sell-4626.conf --rule R3a_estimatedSellFeeCanBeLowerThanActualSellFee R2_getAssetAmountForSellAssetVsActualSellFee R4a_getSellFeeVsgetAssetAmountForSellAsset R4_getSellFeeVsgetAssetAmountForSellAsset R1a_getAssetAmountForSellAssetFeeNeGetSellFee R2a_getAssetAmountForSellAssetNeActualSellFee R4b_getSellFeeVsgetAssetAmountForSellAsset R1_getAssetAmountForSellAssetFeeGeGetSellFee R3b_estimatedSellFeeEqActualSellFee - gho-gsm4626-2.conf --rule accruedFeesLEGhoBalanceOfThis accruedFeesNeverDecrease systemBalanceStabilitySell systemBalanceStabilitySell - optimality4626.conf --rule R5a_externalOptimalityOfSellAsset R6a_externalOptimalityOfBuyAsset - optimality4626.conf --rule R1_optimalityOfBuyAsset_v1 - optimality4626.conf --rule R3_optimalityOfSellAsset_v1 - getAmount_4626_properties.conf --rule getAssetAmountForBuyAsset_correctness_bound1 getAssetAmountForBuyAsset_correctness_bound2 getGhoAmountForBuyAsset_correctness_bound1 getAssetAmountForSellAsset_correctness getAssetAmountForBuyAsset_optimality getAssetAmountForBuyAsset_correctness - getAmount_4626_properties.conf --rule getGhoAmountForBuyAsset_optimality - getAmount_4626_properties.conf --rule getGhoAmountForBuyAsset_correctness - getAmount_4626_properties.conf --rule getAssetAmountForSellAsset_optimality getAssetAmountForBuyAsset_funcProperty - finishedRules4626.conf --rule cantBuyOrSellWhenSeized cantBuyOrSellWhenFrozen sellAssetIncreasesExposure buyAssetDecreasesExposure rescuingAssetKeepsAccruedFees rescuingGhoKeepsAccruedFees giftingGhoDoesntAffectStorageSIMPLE correctnessOfBuyAsset giftingUnderlyingDoesntAffectStorageSIMPLE sellAssetSameAsGetGhoAmountForSellAsset correctnessOfSellAsset giftingGhoDoesntCreateExcessOrDearth backWithGhoDoesntCreateExcess getAssetAmountForSellAsset_correctness collectedSellFeeIsAtLeastAsRequired collectedBuyFeePlus2IsAtLeastAsRequired collectedBuyFeePlus1IsAtLeastAsRequired collectedBuyFeeIsAtLeastAsRequired sellingDoesntExceedExposureCap whoCanChangeAccruedFees whoCanChangeExposure - finishedRules4626.conf --rule giftingUnderlyingDoesntCreateExcessOrDearth ================================================ FILE: .github/workflows/certora-gsm.yml ================================================ name: certora-gsm on: push: branches: - main pull_request: branches: - main workflow_dispatch: jobs: verify: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: recursive - name: Install python uses: actions/setup-python@v5 with: { python-version: 3.9 } - name: Install java uses: actions/setup-java@v4 with: { distribution: 'zulu', java-version: '11', java-package: jre } - name: Install certora cli run: pip install certora-cli==6.1.3 - name: Install solc run: | wget https://github.com/ethereum/solidity/releases/download/v0.8.10/solc-static-linux chmod +x solc-static-linux sudo mv solc-static-linux /usr/local/bin/solc8.10 - name: Verify rule ${{ matrix.rule }} run: | certoraRun certora/gsm/conf/gsm/${{ matrix.rule }} env: CERTORAKEY: ${{ secrets.CERTORAKEY }} strategy: fail-fast: false max-parallel: 16 matrix: rule: - gho-gsm_inverse.conf - gho-gsm.conf - balances-buy.conf - balances-sell.conf - gho-assetToGhoInvertibility.conf --rule basicProperty_getAssetAmountForBuyAsset sellAssetInverse_all buyAssetInverse_all basicProperty_getGhoAmountForSellAsset basicProperty_getAssetAmountForSellAsset basicProperty_getGhoAmountForBuyAsset - gho-assetToGhoInvertibility.conf --rule basicProperty2_getAssetAmountForBuyAsset - gho-fixedPriceStrategy.conf - fees-buy.conf - fees-sell.conf - FixedFeeStrategy.conf - gho-gsm.conf - optimality.conf --rule R3_optimalityOfSellAsset_v1 R1_optimalityOfBuyAsset_v1 R6a_externalOptimalityOfBuyAsset R5a_externalOptimalityOfSellAsset R2_optimalityOfBuyAsset_v2 - getAmount_properties.conf --rule getAssetAmountForBuyAsset_funcProperty_LR getAssetAmountForBuyAsset_funcProperty_RL - finishedRules.conf --rule whoCanChangeExposure whoCanChangeAccruedFees sellingDoesntExceedExposureCap cantBuyOrSellWhenSeized giftingGhoDoesntAffectStorageSIMPLE giftingUnderlyingDoesntAffectStorageSIMPLE collectedBuyFeePlus1IsAtLeastAsRequired sellAssetSameAsGetGhoAmountForSellAsset collectedSellFeeIsAtLeastAsRequired collectedBuyFeeIsAtLeastAsRequired correctnessOfBuyAsset collectedBuyFeePlus2IsAtLeastAsRequired getAssetAmountForSellAsset_correctness cantBuyOrSellWhenFrozen whoCanChangeExposureCap cantSellIfExposureTooHigh sellAssetIncreasesExposure buyAssetDecreasesExposure rescuingGhoKeepsAccruedFees rescuingAssetKeepsAccruedFees - OracleSwapFreezer.conf ================================================ FILE: .github/workflows/certora-steward.yml ================================================ name: certora-steward on: push: branches: - main pull_request: branches: - main workflow_dispatch: jobs: verify: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: recursive - name: Install python uses: actions/setup-python@v5 with: { python-version: 3.9 } - name: Install java uses: actions/setup-java@v4 with: { distribution: 'zulu', java-version: '11', java-package: jre } - name: Install certora cli run: pip install certora-cli - name: Install solc run: | cd certora/steward/ touch applyHarness.patch make munged cd ../.. wget https://github.com/ethereum/solidity/releases/download/v0.8.10/solc-static-linux chmod +x solc-static-linux sudo mv solc-static-linux /usr/local/bin/solc8.10 - name: Verify rule ${{ matrix.rule }} run: | certoraRun certora/steward/conf/${{ matrix.rule }} env: CERTORAKEY: ${{ secrets.CERTORAKEY }} strategy: fail-fast: false max-parallel: 16 matrix: rule: - GhoAaveSteward.conf - GhoBucketSteward.conf - GhoCcipSteward.conf - GhoGsmSteward.conf ================================================ FILE: .github/workflows/dependency-review.yml ================================================ name: Dependency Review on: - pull_request permissions: contents: read pull-requests: write jobs: dependency-review: runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v5 - name: Dependency Review uses: actions/dependency-review-action@v4 with: comment-summary-in-pr: on-failure fail-on-severity: moderate license-check: false ================================================ FILE: .github/workflows/node.js.yml ================================================ name: Build on: push jobs: build: runs-on: group: larger env: ALCHEMY_KEY: '${{secrets.ALCHEMY_KEY}}' ETH_RPC_URL: 'https://eth-mainnet.g.alchemy.com/v2/${{secrets.ALCHEMY_KEY}}' RPC_MAINNET: 'https://eth-mainnet.g.alchemy.com/v2/${{secrets.ALCHEMY_KEY}}' RPC_ARBITRUM: 'https://arb-mainnet.g.alchemy.com/v2/${{secrets.ALCHEMY_KEY}}' strategy: matrix: node-version: - 16.x steps: - name: Checkout uses: actions/checkout@v3 with: submodules: recursive - name: Setup node uses: actions/setup-node@v3 with: node-version: 18.x.x - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: version: nightly - name: Install Dependencies run: npm ci - name: Compilation run: 'npm run compile' - name: Lint check run: 'npm run prettier:check' - name: Test env: NODE_OPTIONS: '--max_old_space_size=4096' run: 'npm run test' ================================================ FILE: .github/workflows/sync-issue.yml ================================================ name: Sync Issue to Height on: issues: types: [opened] jobs: sync: runs-on: ubuntu-latest steps: - name: Sync issue uses: fjogeleit/http-request-action@v1 with: url: 'https://api.height.app/tasks' method: 'POST' customHeaders: '{ "Content-Type": "application/json", "Authorization": "api-key ${{ secrets.HEIGHT_SECRET_TOKEN }}" }' data: '{ "name": "${{ github.event.issue.title }}", "listIds": ["64fa570a-a252-4521-b801-9b0eb18113bc"], "status": "359d42b4-5e7b-46fa-8465-d179b5a097ef" }' ================================================ FILE: .gitignore ================================================ /cache /cache_forge /out /artifacts /.env /node_modules /deployments /types /coverage /coverage.json /temp-artifacts /src/contracts/hardhat-dependency-compiler .DS_Store /report lcov.info combined-lcov.info /broadcast ================================================ FILE: .gitmodules ================================================ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std branch = v1.3.0 [submodule "lib/aave-stk-v1-5"] path = lib/aave-stk-v1-5 url = https://github.com/bgd-labs/aave-stk-v1-5 [submodule "lib/aave-address-book"] path = lib/aave-address-book url = https://github.com/bgd-labs/aave-address-book [submodule "lib/solidity-utils"] path = lib/solidity-utils url = https://github.com/bgd-labs/solidity-utils [submodule "lib/aave-v3-core"] path = lib/aave-v3-core url = https://github.com/aave/aave-v3-core [submodule "lib/aave-token"] path = lib/aave-token url = https://github.com/aave/aave-token [submodule "lib/aave-v3-periphery"] path = lib/aave-v3-periphery url = https://github.com/aave/aave-v3-periphery [submodule "lib/safety-module"] path = lib/safety-module url = https://github.com/aave/safety-module [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts ================================================ FILE: .husky/.gitignore ================================================ _ ================================================ FILE: .husky/pre-commit ================================================ #!/bin/sh . "$(dirname "$0")/_/husky.sh" npx lint-staged ================================================ FILE: .prettierignore ================================================ artifacts cache node_modules types out deployments lib coverage cache_forge ================================================ FILE: .prettierrc ================================================ { "printWidth": 100, "trailingComma": "es5", "semi": true, "singleQuote": true, "tabWidth": 2, "overrides": [ { "files": "*.sol", "options": { "semi": true, "printWidth": 100 } } ] } ================================================ FILE: .solcover.js ================================================ module.exports = { skipFiles: ['./script', './test'], mocha: { enableTimeouts: false, }, configureYulOptimizer: true, }; ================================================ FILE: .vscode/settings.json ================================================ { "solidity.formatter": "none", "editor.defaultFormatter": "esbenp.prettier-vscode", "prettier.configPath": ".prettierrc", "editor.formatOnSave": true } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Aave Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # ⚠️ This repository is DEPRECATED and no longer maintained ⚠️ For the latest GHO code visit the GHO Origin Repository [here](https://github.com/aave-dao/gho-origin). [![Build pass](https://github.com/aave/gho/actions/workflows/node.js.yml/badge.svg)](https://github.com/aave/gho/actions/workflows/node.js.yml) ``` .///. .///. //. .// `/////////////- `++:++` .++:++` :++` `++: `++:......---.` `/+: -+/` `++- :+/` /+/ `/+/ `++. /+/ :+/ /+: /+/ `/+/ /+/` `++. -::/++::` /+: -::/++::` `/+: `++: :++` `++/:::::::::. -:+++::-` `/+: --++/---` `++- .++- -++. `++/:::::::::. -++. .++- -++` .++. .++. .++- `++. .++- -++. .++. -++. -++``++- `++. `++: :++` .++- :++` :+//+: `++:----------` -/: :/- -/: :/. ://: `/////////////- ``` # Gho This repository contains the source code, tests and deployments for both GHO itself and the first facilitator integrating Aave. The repository uses [Hardhat](https://hardhat.org/) development framework. ## Description GHO is a decentralized, protocol-agnostic crypto-asset intended to maintain a stable value. GHO is minted and burned by approved entities named Facilitators. The first facilitator is the Aave V3 Ethereum Pool, which allows users to mint GHO against their collateral assets, based on the interest rate set by the Aave Governance. In addition, there is a FlashMint module as a second facilitator, which facilitates arbitrage and liquidations, providing instant liquidity. Furthermore, the Aave Governance has the ability to approve entities as Facilitators and manage the total amount of GHO they can generate (also known as bucket's capacity). ## Documentation See the link to the technical paper - [Technical Paper](./techpaper/GHO_Technical_Paper.pdf) - [Developer Documentation](https://docs.gho.xyz/) ## Audits and Formal Verification You can find all audit reports under the [audits](./audits/) folder - [2022-08-12 - OpenZeppelin](./audits/2022-08-12_Openzeppelin-v1.pdf) - [2022-11-10 - OpenZeppelin](./audits/2022-11-10_Openzeppelin-v2.pdf) - [2023-03-01 - ABDK](./audits/2023-03-01_ABDK.pdf) - [2023-02-28 - Certora Formal Verification](./certora/reports/Aave_Gho_Formal_Verification_Report.pdf) - [2023-07-06 - Sigma Prime](./audits/2023-07-06_SigmaPrime.pdf) - [2023-06-13 - Sigma Prime (GhoSteward)](./audits/2023-06-13_GhoSteward_SigmaPrime.pdf) - [2023-09-20 - Emanuele Ricci @Stermi (GHO Stability Module)](./audits/2023-09-20_GSM_Stermi.pdf) - [2023-10-23 - Sigma Prime (GHO Stability Module)](./audits/2023-10-23_GSM_SigmaPrime.pdf) - [2023-12-07 - Certora Formal Verification (GHO Stability Module)](./certora/reports/Formal_Verification_Report_of_GHO_Stability_Module.pdf) - [2024-03-14 - Certora Formal Verification (GhoStewardV2)](./audits/2024-03-14_GhoStewardV2_Certora.pdf) - [2024-06-11 - Certora Formal Verification (UpgradeableGHO)](./audits/2024-06-11_UpgradeableGHO_Certora.pdf) - [2024-06-11 - Certora Formal Verification (Modular Gho Stewards)](./audits/2024-09-15_ModularGhoStewards_Certora.pdf) ## Getting Started Clone the repository and run the following command to install dependencies: ```sh npm i forge i ``` If you need to interact with GHO in the Goerli testnet, provide your Alchemy API key and mnemonic in the `.env` file: ```sh cp .env.example .env # Fill ALCHEMY_KEY and MNEMONIC in the .env file with your editor code .env ``` Compile contracts: ```sh npm run compile ``` Run the test suite: ```sh npm run test ``` Deploy and setup GHO in a local Hardhat network: ```sh npm run deploy-testnet ``` Deploy and setup GHO in Goerli testnet: ```sh npm run deploy-testnet:goerli ``` ## Connect with the community You can join the [Discord](http://aave.com/discord) channel or the [Governance Forum](https://governance.aave.com/) to ask questions about the protocol or talk about Gho with other peers. ================================================ FILE: certora/gho/Makefile ================================================ default: help PATCH = applyHarness.patch CONTRACTS_DIR = ../../src MUNGED_DIR = munged help: @echo "usage:" @echo " make clean: remove all generated files (those ignored by git)" @echo " make $(MUNGED_DIR): create $(MUNGED_DIR) directory by applying the patch file to $(CONTRACTS_DIR)" @echo " make record: record a new patch file capturing the differences between $(CONTRACTS_DIR) and $(MUNGED_DIR)" munged: $(wildcard $(CONTRACTS_DIR)/*.sol) $(PATCH) rm -rf $@ cp -r $(CONTRACTS_DIR) $@ patch -p0 -d $@ < $(PATCH) record: diff -ruN $(CONTRACTS_DIR) $(MUNGED_DIR) | sed 's+\.\./$(CONTRACTS_DIR)/++g' | sed 's+$(MUNGED_DIR)/++g' > $(PATCH) clean: git clean -fdX touch $(PATCH) ================================================ FILE: certora/gho/applyHarness.patch ================================================ diff -ruN ../../src/contracts/gho/GhoToken.sol contracts/gho/GhoToken.sol --- ../../src/contracts/gho/GhoToken.sol 2024-05-21 10:57:52.000000000 +0300 +++ contracts/gho/GhoToken.sol 2024-05-27 12:55:24.588859419 +0300 @@ -66,11 +66,16 @@ uint128 bucketCapacity ) external onlyRole(FACILITATOR_MANAGER_ROLE) { Facilitator storage facilitator = _facilitators[facilitatorAddress]; + require( + !facilitator.isLabelNonempty, //TODO: remove workaroun when CERT-977 is resolved + 'FACILITATOR_ALREADY_EXISTS' + ); require(bytes(facilitator.label).length == 0, 'FACILITATOR_ALREADY_EXISTS'); require(bytes(facilitatorLabel).length > 0, 'INVALID_LABEL'); facilitator.label = facilitatorLabel; facilitator.bucketCapacity = bucketCapacity; + facilitator.isLabelNonempty = true; _facilitatorsList.add(facilitatorAddress); @@ -86,6 +91,10 @@ address facilitatorAddress ) external onlyRole(FACILITATOR_MANAGER_ROLE) { require( + _facilitators[facilitatorAddress].isLabelNonempty, //TODO: remove workaroun when CERT-977 is resolved + 'FACILITATOR_DOES_NOT_EXIST' + ); + require( bytes(_facilitators[facilitatorAddress].label).length > 0, 'FACILITATOR_DOES_NOT_EXIST' ); @@ -105,6 +114,10 @@ address facilitator, uint128 newCapacity ) external onlyRole(BUCKET_MANAGER_ROLE) { + require( + _facilitators[facilitator].isLabelNonempty, //TODO: remove workaroun when CERT-977 is resolved + 'FACILITATOR_DOES_NOT_EXIST' + ); require(bytes(_facilitators[facilitator].label).length > 0, 'FACILITATOR_DOES_NOT_EXIST'); uint256 oldCapacity = _facilitators[facilitator].bucketCapacity; @@ -119,12 +132,12 @@ } /// @inheritdoc IGhoToken - function getFacilitatorBucket(address facilitator) external view returns (uint256, uint256) { + function getFacilitatorBucket(address facilitator) public view returns (uint256, uint256) { return (_facilitators[facilitator].bucketCapacity, _facilitators[facilitator].bucketLevel); } /// @inheritdoc IGhoToken - function getFacilitatorsList() external view returns (address[] memory) { + function getFacilitatorsList() public view returns (address[] memory) { return _facilitatorsList.values(); } } diff -ruN ../../src/contracts/gho/interfaces/IGhoToken.sol contracts/gho/interfaces/IGhoToken.sol --- ../../src/contracts/gho/interfaces/IGhoToken.sol 2024-05-21 10:57:52.000000000 +0300 +++ contracts/gho/interfaces/IGhoToken.sol 2024-05-27 12:55:24.588859419 +0300 @@ -13,6 +13,7 @@ uint128 bucketCapacity; uint128 bucketLevel; string label; + bool isLabelNonempty; //TODO: remove workaroun when https://certora.atlassian.net/browse/CERT-977 is resolved } /** diff -ruN ../../src/contracts/gho/UpgradeableGhoToken.sol contracts/gho/UpgradeableGhoToken.sol --- ../../src/contracts/gho/UpgradeableGhoToken.sol 2024-05-21 11:57:23.000000000 +0300 +++ contracts/gho/UpgradeableGhoToken.sol 2024-05-27 15:04:16.458997293 +0300 @@ -76,11 +76,16 @@ uint128 bucketCapacity ) external onlyRole(FACILITATOR_MANAGER_ROLE) { Facilitator storage facilitator = _facilitators[facilitatorAddress]; + require( + !facilitator.isLabelNonempty, //TODO: remove workaroun when CERT-977 is resolved + 'FACILITATOR_ALREADY_EXISTS' + ); require(bytes(facilitator.label).length == 0, 'FACILITATOR_ALREADY_EXISTS'); require(bytes(facilitatorLabel).length > 0, 'INVALID_LABEL'); facilitator.label = facilitatorLabel; facilitator.bucketCapacity = bucketCapacity; + facilitator.isLabelNonempty = true; _facilitatorsList.add(facilitatorAddress); @@ -96,6 +101,10 @@ address facilitatorAddress ) external onlyRole(FACILITATOR_MANAGER_ROLE) { require( + _facilitators[facilitatorAddress].isLabelNonempty, //TODO: remove workaroun when CERT-977 is resolved + 'FACILITATOR_DOES_NOT_EXIST' + ); + require( bytes(_facilitators[facilitatorAddress].label).length > 0, 'FACILITATOR_DOES_NOT_EXIST' ); @@ -115,6 +124,10 @@ address facilitator, uint128 newCapacity ) external onlyRole(BUCKET_MANAGER_ROLE) { + require( + _facilitators[facilitator].isLabelNonempty, //TODO: remove workaroun when CERT-977 is resolved + 'FACILITATOR_DOES_NOT_EXIST' + ); require(bytes(_facilitators[facilitator].label).length > 0, 'FACILITATOR_DOES_NOT_EXIST'); uint256 oldCapacity = _facilitators[facilitator].bucketCapacity; @@ -129,12 +142,12 @@ } /// @inheritdoc IGhoToken - function getFacilitatorBucket(address facilitator) external view returns (uint256, uint256) { + function getFacilitatorBucket(address facilitator) public view returns (uint256, uint256) { return (_facilitators[facilitator].bucketCapacity, _facilitators[facilitator].bucketLevel); } /// @inheritdoc IGhoToken - function getFacilitatorsList() external view returns (address[] memory) { + function getFacilitatorsList() public view returns (address[] memory) { return _facilitatorsList.values(); } diff -ruN ../../src/.gitignore .gitignore --- ../../src/.gitignore 1970-01-01 02:00:00.000000000 +0200 +++ .gitignore 2024-05-27 12:55:24.588859419 +0300 @@ -0,0 +1,2 @@ +* +!.gitignore ================================================ FILE: certora/gho/conf/verifyFlashMinter.conf ================================================ { "files": [ "certora/gho/munged/contracts/facilitators/flashMinter/GhoFlashMinter.sol:GhoFlashMinter", "certora/gho/munged/contracts/facilitators/aave/tokens/GhoAToken.sol:GhoAToken", "certora/gho/munged/contracts/gho/GhoToken.sol", "certora/gho/harness/MockFlashBorrower.sol" ], "link": [ "MockFlashBorrower:Gho=GhoToken", "MockFlashBorrower:AGho=GhoAToken", "GhoFlashMinter:GHO_TOKEN=GhoToken", "MockFlashBorrower:minter=GhoFlashMinter" ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "msg": "flashMinter check, all rules", "optimistic_loop": true, "process": "emv", "prover_args": [ " -contractRecursionLimit 1" ], "solc": "solc8.10", "verify": "GhoFlashMinter:certora/gho/specs/flashMinter.spec" } ================================================ FILE: certora/gho/conf/verifyGhoAToken.conf ================================================ { "files": [ "certora/gho/munged/contracts/facilitators/aave/tokens/GhoAToken.sol", "certora/gho/munged/contracts/facilitators/aave/tokens/GhoVariableDebtToken.sol", "certora/gho/munged/contracts/gho/GhoToken.sol", "certora/gho/harness/GhoTokenHarness.sol", "certora/gho/harness/DummyERC20A.sol", "certora/gho/harness/DummyERC20B.sol" ], "link": [ "GhoAToken:_ghoVariableDebtToken=GhoVariableDebtToken", "GhoVariableDebtToken:_ghoAToken=GhoAToken", "GhoAToken:_underlyingAsset=GhoTokenHarness" ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "msg": "GhoAToken, all rules", "optimistic_loop": true, "process": "emv", "solc": "solc8.10", "verify": "GhoAToken:certora/gho/specs/ghoAToken.spec" } ================================================ FILE: certora/gho/conf/verifyGhoDiscountRateStrategy.conf ================================================ { "files": [ "certora/gho/harness/GhoDiscountRateStrategyHarness.sol:GhoDiscountRateStrategyHarness" ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "loop_iter": "2", "msg": "GhoDiscountRateStrategy, all rules.", "optimistic_loop": true, "process": "emv", "prover_args": [ " -mediumTimeout 20 -depth 10" ], "smt_timeout": "500", "solc": "solc8.10", "verify": "GhoDiscountRateStrategyHarness:certora/gho/specs/ghoDiscountRateStrategy.spec" } ================================================ FILE: certora/gho/conf/verifyGhoToken.conf ================================================ { "files": [ "certora/gho/harness/GhoTokenHarness.sol:GhoTokenHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "loop_iter": "3", "msg": "GhoToken, all rules.", "optimistic_loop": true, "process": "emv", "solc": "solc8.10", "verify": "GhoTokenHarness:certora/gho/specs/ghoToken.spec" } ================================================ FILE: certora/gho/conf/verifyGhoVariableDebtToken-rayMulDiv-summarization.conf ================================================ { "files": [ "certora/gho/harness/ghoVariableDebtTokenHarness.sol:GhoVariableDebtTokenHarness", "certora/gho/harness/DummyPool.sol", "certora/gho/harness/DummyERC20WithTimedBalanceOf.sol", "certora/gho/munged/contracts/facilitators/aave/interestStrategy/GhoDiscountRateStrategy.sol", "certora/gho/harness/DummyERC20A.sol", "certora/gho/harness/DummyERC20B.sol" ], "link": [ "GhoVariableDebtTokenHarness:_discountToken=DummyERC20WithTimedBalanceOf", "GhoVariableDebtTokenHarness:POOL=DummyPool", "GhoVariableDebtTokenHarness:_discountRateStrategy=GhoDiscountRateStrategy" ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "loop_iter": "2", "msg": " ", "multi_assert_check": true, "optimistic_loop": true, "process": "emv", "prover_args": [ " -mediumTimeout 30 -depth 15" ], "smt_timeout": "900", "solc": "solc8.10", "verify": "GhoVariableDebtTokenHarness:certora/gho/specs/ghoVariableDebtToken-rayMulDiv-summarization.spec" } ================================================ FILE: certora/gho/conf/verifyGhoVariableDebtToken.conf ================================================ { "files": [ "certora/gho/harness/ghoVariableDebtTokenHarness.sol:GhoVariableDebtTokenHarness", "certora/gho/harness/DummyPool.sol", "certora/gho/harness/DummyERC20WithTimedBalanceOf.sol", "certora/gho/munged/contracts/facilitators/aave/interestStrategy/GhoDiscountRateStrategy.sol", "certora/gho/harness/DummyERC20A.sol", "certora/gho/harness/DummyERC20B.sol" ], "link": [ "GhoVariableDebtTokenHarness:_discountRateStrategy=GhoDiscountRateStrategy", "GhoVariableDebtTokenHarness:POOL=DummyPool", "GhoVariableDebtTokenHarness:_discountToken=DummyERC20WithTimedBalanceOf" ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "loop_iter": "2", "msg": "GhoVariableDebtToken", "optimistic_loop": true, "process": "emv", "prover_args": [ " -mediumTimeout 30 -depth 15" ], "smt_timeout": "900", "solc": "solc8.10", "verify": "GhoVariableDebtTokenHarness:certora/gho/specs/ghoVariableDebtToken.spec" } ================================================ FILE: certora/gho/conf/verifyGhoVariableDebtTokenInternal.conf ================================================ { "files": [ "certora/gho/harness/ghoVariableDebtTokenHarnessInternal.sol:GhoVariableDebtTokenHarnessInternal", "certora/gho/munged/contracts/facilitators/aave/interestStrategy/GhoDiscountRateStrategy.sol" ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "loop_iter": "2", "msg": "GhoVariableDebtToken internal functions", "optimistic_loop": true, "process": "emv", "prover_args": [ " -mediumTimeout 30 -depth 15" ], "smt_timeout": "900", "solc": "solc8.10", "verify": "GhoVariableDebtTokenHarnessInternal:certora/gho/specs/ghoVariableDebtTokenInternal.spec" } ================================================ FILE: certora/gho/conf/verifyGhoVariableDebtToken_specialBranch.conf ================================================ { "files": [ "certora/gho/harness/ghoVariableDebtTokenHarness.sol:GhoVariableDebtTokenHarness", "certora/gho/harness/DummyPool.sol", "certora/gho/harness/DummyERC20WithTimedBalanceOf.sol", "certora/gho/munged/contracts/facilitators/aave/interestStrategy/GhoDiscountRateStrategy.sol", "certora/gho/harness/DummyERC20A.sol", "certora/gho/harness/DummyERC20B.sol" ], "link": [ "GhoVariableDebtTokenHarness:POOL=DummyPool", "GhoVariableDebtTokenHarness:_discountToken=DummyERC20WithTimedBalanceOf", "GhoVariableDebtTokenHarness:_discountRateStrategy=GhoDiscountRateStrategy" ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "loop_iter": "2", "msg": "GhoVariableDebtToken", "optimistic_loop": true, "process": "emv", "prover_args": [ " -depth 0 -adaptiveSolverConfig false -smt_nonLinearArithmetic true" ], "prover_version": "shelly/z3-4-12-3-build", "solc": "solc8.10", "verify": "GhoVariableDebtTokenHarness:certora/gho/specs/ghoVariableDebtToken.spec" } ================================================ FILE: certora/gho/conf/verifyGhoVariableDebtToken_summarized.conf ================================================ { "files": [ "certora/gho/harness/ghoVariableDebtTokenHarness.sol:GhoVariableDebtTokenHarness", "certora/gho/harness/DummyPool.sol", "certora/gho/harness/DummyERC20WithTimedBalanceOf.sol", "certora/gho/munged/contracts/facilitators/aave/interestStrategy/GhoDiscountRateStrategy.sol", "certora/gho/harness/DummyERC20A.sol", "certora/gho/harness/DummyERC20B.sol" ], "link": [ "GhoVariableDebtTokenHarness:POOL=DummyPool", "GhoVariableDebtTokenHarness:_discountToken=DummyERC20WithTimedBalanceOf", "GhoVariableDebtTokenHarness:_discountRateStrategy=GhoDiscountRateStrategy" ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "loop_iter": "2", "msg": "GhoVariableDebtToken", "optimistic_loop": true, "process": "emv", "prover_args": [ " -mediumTimeout 30 -depth 15" ], "smt_timeout": "900", "solc": "solc8.10", "verify": "GhoVariableDebtTokenHarness:certora/gho/specs/ghoVariableDebtToken_summarized.spec" } ================================================ FILE: certora/gho/conf/verifyUpgradeableGhoToken.conf ================================================ { "files": [ "certora/gho/harness/UpgradeableGhoTokenHarness.sol:UpgradeableGhoTokenHarness", ], "packages": [ "@openzeppelin/=lib/openzeppelin-contracts", "solidity-utils/=lib/solidity-utils/src/", ], "loop_iter": "3", "msg": "GhoToken, all rules.", "optimistic_loop": true, "process": "emv", "solc": "solc8.10", "verify": "UpgradeableGhoTokenHarness:certora/gho/specs/ghoToken.spec" } ================================================ FILE: certora/gho/harness/DummyERC20A.sol ================================================ pragma solidity ^0.8.0; import './DummyERC20Impl.sol'; contract DummyERC20A is DummyERC20Impl {} ================================================ FILE: certora/gho/harness/DummyERC20B.sol ================================================ pragma solidity ^0.8.0; import './DummyERC20Impl.sol'; contract DummyERC20B is DummyERC20Impl {} ================================================ FILE: certora/gho/harness/DummyERC20Impl.sol ================================================ // SPDX-License-Identifier: agpl-3.0 pragma solidity ^0.8.0; // with mint contract DummyERC20Impl { uint256 t; mapping(address => uint256) b; mapping(address => mapping(address => uint256)) a; string public name; string public symbol; uint public decimals; function myAddress() public returns (address) { return address(this); } function add(uint a, uint b) internal pure returns (uint256) { uint c = a + b; require(c >= a); return c; } function sub(uint a, uint b) internal pure returns (uint256) { require(a >= b); return a - b; } function totalSupply() external view returns (uint256) { return t; } function balanceOf(address account) external view returns (uint256) { return b[account]; } function transfer(address recipient, uint256 amount) external returns (bool) { b[msg.sender] = sub(b[msg.sender], amount); b[recipient] = add(b[recipient], amount); return true; } function allowance(address owner, address spender) external view returns (uint256) { return a[owner][spender]; } function approve(address spender, uint256 amount) external returns (bool) { a[msg.sender][spender] = amount; return true; } function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { b[sender] = sub(b[sender], amount); b[recipient] = add(b[recipient], amount); a[sender][msg.sender] = sub(a[sender][msg.sender], amount); return true; } } ================================================ FILE: certora/gho/harness/DummyERC20WithTimedBalanceOf.sol ================================================ contract DummyERC20WithTimedBalanceOf { function balanceOf(address user) public view virtual returns (uint256) { return _balanceOfWithBlockTimestamp(user, block.timestamp); } function _balanceOfWithBlockTimestamp( address user, uint256 blockTs ) internal view returns (uint256) { return 0; // STUB! Should be summarized } } ================================================ FILE: certora/gho/harness/DummyPool.sol ================================================ contract DummyPool { function getReserveNormalizedVariableDebt(address asset) external view returns (uint256) { return _getReserveNormalizedVariableDebtWithBlockTimestamp(asset, block.timestamp); } function _getReserveNormalizedVariableDebtWithBlockTimestamp( address asset, uint256 blockTs ) internal view returns (uint256) { return 0; // will be replaced by a summary in the spec file } } ================================================ FILE: certora/gho/harness/GhoDiscountRateStrategyHarness.sol ================================================ import {GhoDiscountRateStrategy} from '../munged/contracts/facilitators/aave/interestStrategy/GhoDiscountRateStrategy.sol'; import {WadRayMath} from '@aave/core-v3/contracts/protocol/libraries/math/WadRayMath.sol'; contract GhoDiscountRateStrategyHarness is GhoDiscountRateStrategy { using WadRayMath for uint256; function wadMul(uint256 x, uint256 y) external view returns (uint256) { return x.wadMul(y); } } ================================================ FILE: certora/gho/harness/GhoTokenHarness.sol ================================================ pragma solidity ^0.8.0; import {IGhoToken} from '../munged/contracts/gho/interfaces/IGhoToken.sol'; import '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; import {GhoToken} from '../munged/contracts/gho/GhoToken.sol'; contract GhoTokenHarness is GhoToken { using EnumerableSet for EnumerableSet.AddressSet; constructor() GhoToken(msg.sender) {} /** * @notice Returns the bucket capacity * @param facilitator The address of the facilitator * @return The facilitator bucket capacity */ function getFacilitatorBucketCapacity(address facilitator) public view returns (uint256) { (uint256 bucketCapacity, ) = getFacilitatorBucket(facilitator); return bucketCapacity; } /** * @notice Returns the bucket level * @param facilitator The address of the facilitator * @return The facilitator bucket level */ function getFacilitatorBucketLevel(address facilitator) public view returns (uint256) { (, uint256 bucketLevel) = getFacilitatorBucket(facilitator); return bucketLevel; } /** * @notice Returns the length of the facilitator list * @return The length of the facilitator list */ function getFacilitatorsListLen() external view returns (uint256) { address[] memory flist = getFacilitatorsList(); return flist.length; } /** * @notice Indicator of GhoToken mapping * @param addr An address of a facilitator * @return True of facilitator is in GhoToken mapping */ function is_in_facilitator_mapping(address addr) external view returns (bool) { Facilitator memory facilitator = _facilitators[addr]; return facilitator.isLabelNonempty; //TODO: remove workaround when CERT-977 is resolved // return (bytes(facilitator.label).length > 0); } /** * @notice Indicator of AddressSet mapping * @param addr An address of a facilitator * @return True of facilitator is in AddressSet mapping */ function is_in_facilitator_set_map(address addr) external view returns (bool) { return _facilitatorsList.contains(addr); } /** * @notice Indicator of AddressSet list * @param addr An address of a facilitator * @return True of facilitator is in AddressSet array */ function is_in_facilitator_set_array(address addr) external view returns (bool) { address[] memory flist = getFacilitatorsList(); for (uint256 i = 0; i < flist.length; ++i) { if (address(flist[i]) == addr) { return true; } } return false; } /** * @notice Converts address to bytes32 * @param value Some address * @return b the value as bytes32 */ function to_bytes32(address value) external pure returns (bytes32 b) { b = bytes32(uint256(uint160(value))); } } ================================================ FILE: certora/gho/harness/MockFlashBorrower.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {IERC3156FlashBorrower} from '@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol'; import {IERC3156FlashLender} from '@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol'; import {IGhoFlashMinter} from '../munged/contracts/facilitators/flashMinter/interfaces/IGhoFlashMinter.sol'; import {IGhoToken} from '../munged/contracts/gho/interfaces/IGhoToken.sol'; import {IGhoAToken} from '../munged/contracts/facilitators/aave/tokens/interfaces/IGhoAToken.sol'; contract MockFlashBorrower is IERC3156FlashBorrower { enum Action { FLASH_LOAN, DISTRIBUTE_FEES, UPDATE_FEES, UPDATE_TREASURY, MINT, BURN, ADD_FACILITATOR, REMOVE_FACILITATOR, SET_FACILITATOR, APPROVE, TRANSFER, TRANSFER_FROM, TRANSFER_UNDERLYING_TO, HANDLE_REPAYMENT, ATOKEN_DISTRIBUTE_FEES, RESCUE_TOKENS, SET_VAR_DEBT_TOKEN, ATOKEN_UPDATE_TREASURY, OTHER } struct Facilitator { uint128 bucketCapacity; uint128 bucketLevel; string label; } Action public action; uint8 public counter; uint8 public repeat_on_count; IGhoFlashMinter public minter; IGhoToken public Gho; IGhoAToken public AGho; address public _transferTo; IERC3156FlashLender private _lender; bool allowRepayment; constructor(IERC3156FlashLender lender) { _lender = lender; allowRepayment = true; } /// @dev ERC-3156 Flash loan callback function onFlashLoan( address initiator, address token, uint256 amount, uint256 fee, bytes calldata data ) external override returns (bytes32) { require(msg.sender == address(_lender), 'FlashBorrower: Untrusted lender'); require(initiator == address(this), 'FlashBorrower: Untrusted loan initiator'); counter++; if (action == Action.FLASH_LOAN && counter < repeat_on_count) { uint256 amount_reenter; bytes calldata data_reenter; minter.flashLoan(IERC3156FlashBorrower(address(this)), token, amount, data); } else if (action == Action.DISTRIBUTE_FEES) { minter.distributeFeesToTreasury(); } else if (action == Action.UPDATE_FEES) { uint256 new_fee; minter.updateFee(new_fee); } else if (action == Action.UPDATE_TREASURY) { address newGhoTreasury; minter.updateGhoTreasury(newGhoTreasury); } else if (action == Action.MINT) { address account; uint256 amt; Gho.mint(account, amt); } else if (action == Action.BURN) { uint256 amt; Gho.burn(amt); // } else if (action == Action.ADD_FACILITATOR) { // address facilitatorAddress; Facilitator memory facilitatorConfig; // Gho.addFacilitator(facilitatorAddress, facilitatorConfig); } else if (action == Action.REMOVE_FACILITATOR) { address facilitatorAddress; Gho.removeFacilitator(facilitatorAddress); } else if (action == Action.SET_FACILITATOR) { address facilitator; uint128 newCapacity; Gho.setFacilitatorBucketCapacity(facilitator, newCapacity); } else if (action == Action.APPROVE) { address spender; uint256 amt; AGho.approve(spender, amt); } else if (action == Action.TRANSFER) { uint256 amt; AGho.transfer(_transferTo, amt); } else if (action == Action.TRANSFER_FROM) { address from; uint256 amt; AGho.transferFrom(from, _transferTo, amt); } else if (action == Action.TRANSFER_UNDERLYING_TO) { address target; uint256 amt; AGho.transferUnderlyingTo(target, amt); } else if (action == Action.HANDLE_REPAYMENT) { address user; address onBehalfOf; uint256 amt; AGho.handleRepayment(user, onBehalfOf, amt); } else if (action == Action.ATOKEN_DISTRIBUTE_FEES) { AGho.distributeFeesToTreasury(); } else if (action == Action.RESCUE_TOKENS) { address token; address to; uint256 amt; AGho.rescueTokens(token, to, amt); } else if (action == Action.SET_VAR_DEBT_TOKEN) { address ghoVariableDebtToken; AGho.setVariableDebtToken(ghoVariableDebtToken); } else if (action == Action.ATOKEN_UPDATE_TREASURY) { address newGhoTreasury; AGho.updateGhoTreasury(newGhoTreasury); } else if (action == Action.OTHER) { require(true); } return keccak256('ERC3156FlashBorrower.onFlashLoan'); } /// @dev Initiate a flash loan function flashBorrow(address token, uint256 amount) public { bytes memory data = abi.encode(Action.FLASH_LOAN); if (allowRepayment) { uint256 allowance = IERC20(token).allowance(address(this), address(_lender)); uint256 fee = _lender.flashFee(token, amount); uint256 repayment = amount + fee; IERC20(token).approve(address(_lender), allowance + repayment); } _lender.flashLoan(this, token, amount, data); } function setAllowRepayment(bool active) public { allowRepayment = active; } } ================================================ FILE: certora/gho/harness/UpgradeableGhoTokenHarness.sol ================================================ pragma solidity ^0.8.0; import {IGhoToken} from '../munged/contracts/gho/interfaces/IGhoToken.sol'; import '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; import {UpgradeableGhoToken} from '../munged/contracts/gho/UpgradeableGhoToken.sol'; contract UpgradeableGhoTokenHarness is UpgradeableGhoToken { using EnumerableSet for EnumerableSet.AddressSet; constructor() UpgradeableGhoToken() {} /** * @notice Returns the bucket capacity * @param facilitator The address of the facilitator * @return The facilitator bucket capacity */ function getFacilitatorBucketCapacity(address facilitator) public view returns (uint256) { (uint256 bucketCapacity, ) = getFacilitatorBucket(facilitator); return bucketCapacity; } /** * @notice Returns the bucket level * @param facilitator The address of the facilitator * @return The facilitator bucket level */ function getFacilitatorBucketLevel(address facilitator) public view returns (uint256) { (, uint256 bucketLevel) = getFacilitatorBucket(facilitator); return bucketLevel; } /** * @notice Returns the length of the facilitator list * @return The length of the facilitator list */ function getFacilitatorsListLen() external view returns (uint256) { address[] memory flist = getFacilitatorsList(); return flist.length; } /** * @notice Indicator of GhoToken mapping * @param addr An address of a facilitator * @return True of facilitator is in GhoToken mapping */ function is_in_facilitator_mapping(address addr) external view returns (bool) { Facilitator memory facilitator = _facilitators[addr]; return facilitator.isLabelNonempty; //TODO: remove workaround when CERT-977 is resolved // return (bytes(facilitator.label).length > 0); } /** * @notice Indicator of AddressSet mapping * @param addr An address of a facilitator * @return True of facilitator is in AddressSet mapping */ function is_in_facilitator_set_map(address addr) external view returns (bool) { return _facilitatorsList.contains(addr); } /** * @notice Indicator of AddressSet list * @param addr An address of a facilitator * @return True of facilitator is in AddressSet array */ function is_in_facilitator_set_array(address addr) external view returns (bool) { address[] memory flist = getFacilitatorsList(); for (uint256 i = 0; i < flist.length; ++i) { if (address(flist[i]) == addr) { return true; } } return false; } /** * @notice Converts address to bytes32 * @param value Some address * @return b the value as bytes32 */ function to_bytes32(address value) external pure returns (bytes32 b) { b = bytes32(uint256(uint160(value))); } } ================================================ FILE: certora/gho/harness/ghoVariableDebtTokenHarness.sol ================================================ pragma solidity 0.8.10; import {GhoVariableDebtToken} from '../munged/contracts/facilitators/aave/tokens/GhoVariableDebtToken.sol'; import {WadRayMath} from '@aave/core-v3/contracts/protocol/libraries/math/WadRayMath.sol'; import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; contract GhoVariableDebtTokenHarness is GhoVariableDebtToken { using WadRayMath for uint256; constructor(IPool pool) public GhoVariableDebtToken(pool) { //nop } function getUserCurrentIndex(address user) external view returns (uint256) { return _userState[user].additionalData; } function getUserDiscountRate(address user) external view returns (uint256) { return _ghoUserState[user].discountPercent; } function getUserAccumulatedDebtInterest(address user) external view returns (uint256) { return _ghoUserState[user].accumulatedDebtInterest; } function scaledBalanceOfToBalanceOf(uint256 bal) public view returns (uint256) { return bal.rayMul(POOL.getReserveNormalizedVariableDebt(_underlyingAsset)); } function getBalanceOfDiscountToken(address user) external returns (uint256) { return _discountToken.balanceOf(user); } function rayMul(uint256 x, uint256 y) external view returns (uint256) { return x.rayMul(y); } function rayDiv(uint256 x, uint256 y) external view returns (uint256) { return x.rayDiv(y); } function get_ghoAToken() external returns (address) { return _ghoAToken; } } ================================================ FILE: certora/gho/harness/ghoVariableDebtTokenHarnessInternal.sol ================================================ pragma solidity 0.8.10; import {GhoVariableDebtTokenHarness} from './ghoVariableDebtTokenHarness.sol'; import {GhoVariableDebtToken} from '../munged/contracts/facilitators/aave/tokens/GhoVariableDebtToken.sol'; import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; contract GhoVariableDebtTokenHarnessInternal is GhoVariableDebtTokenHarness { constructor(IPool pool) public GhoVariableDebtTokenHarness(pool) { //nop } function accrueDebtOnAction( address user, uint256 previousScaledBalance, uint256 discountPercent, uint256 index ) external returns (uint256, uint256) { return _accrueDebtOnAction(user, previousScaledBalance, discountPercent, index); } } ================================================ FILE: certora/gho/munged/.gitignore ================================================ * !.gitignore ================================================ FILE: certora/gho/specs/VariableDebtToken.spec ================================================ methods { // summarization for elimination the raymul operation in balance of and totalSupply. //getReserveNormalizedVariableDebt(address asset) returns (uint256) => indexAtTimestamp(e.block.timestamp) //setAdditionalData(address user, uint128 data) envfree function _.handleAction(address, uint256, uint256) external => NONDET; function scaledBalanceOfToBalanceOf(uint256) external returns (uint256) envfree; //balanceOf(address) returns (uint256) envfree } definition ray() returns uint256 = 1000000000000000000000000000; // 10^27 definition wad() returns uint256 = 1000000000000000000; // 10^18 definition bound(uint256 index) returns mathint = ((index / ray()) + 1 ) / 2; // summarization for scaledBalanaceOf -> regularBalanceOf + 0.5 (canceling the rayMul) // ghost gRNVB() returns uint256 { // axiom gRNVB() == 7 * ray(); // } /* Due to rayDiv and RayMul Rounding (+ 0.5) - balance could increase by (gRNI() / Ray() + 1) / 2. */ definition bounded_error_eq(uint x, uint y, uint scale, uint256 index) returns bool = to_mathint(x) <= y + (bound(index) * scale) && x + (bound(index) * scale) >= to_mathint(y); definition disAllowedFunctions(method f) returns bool = f.selector == sig:transfer(address, uint256).selector || f.selector == sig:allowance(address, address).selector || f.selector == sig:approve(address, uint256).selector || f.selector == sig:transferFrom(address, address, uint256).selector || f.selector == sig:increaseAllowance(address, uint256).selector || f.selector == sig:decreaseAllowance(address, uint256).selector; ghost sumAllBalance() returns mathint { init_state axiom sumAllBalance() == 0; } hook Sstore _userState[KEY address a].balance uint128 balance (uint128 old_balance) STORAGE { havoc sumAllBalance assuming sumAllBalance@new() == sumAllBalance@old() + balance - old_balance; } invariant totalSupplyEqualsSumAllBalance(env e) totalSupply() == scaledBalanceOfToBalanceOf(sumAllBalance()) filtered { f -> !f.isView && !disAllowedFunctions(f) } { preserved mint(address user, address onBehalfOf, uint256 amount, uint256 index) with (env e2) { require index == indexAtTimestamp(e.block.timestamp); } preserved burn(address from, uint256 amount, uint256 index) with (env e3) { require index == indexAtTimestamp(e.block.timestamp); } } // Only the pool with burn or mint operation can change the total supply. (assuming the getReserveNormalizedVariableDebt is not changed) rule whoChangeTotalSupply(method f) filtered { f -> !f.isView && !disAllowedFunctions(f) } { env e; uint256 oldTotalSupply = totalSupply(); calldataarg args; f(e, args); uint256 newTotalSupply = totalSupply(); assert oldTotalSupply != newTotalSupply => (e.msg.sender == POOL(e) && (f.selector == sig:burn(address, uint256, uint256).selector || f.selector == sig:mint(address, address, uint256, uint256).selector)); } /* Each operation of Variable Debt Token can change at most one user's balance. */ rule balanceOfChange(address a, address b, method f) filtered { f -> !f.isView && !disAllowedFunctions(f) } { env e; require a != b; uint256 balanceABefore = balanceOf(e, a); uint256 balanceBBefore = balanceOf(e, b); calldataarg arg; f(e, arg); uint256 balanceAAfter = balanceOf(e, a); uint256 balanceBAfter = balanceOf(e, b); assert (balanceABefore == balanceAAfter || balanceBBefore == balanceBAfter); } /* Each operation of Variable Debt Token can change at most two user's balance. */ rule balanceOfAtMost3Change(address a, address b, address c, method f) filtered { f -> !f.isView && !disAllowedFunctions(f) } { env e; require a != b; require a != c; require b != c; uint256 balanceABefore = balanceOf(e, a); uint256 balanceBBefore = balanceOf(e, b); uint256 balanceCBefore = balanceOf(e, c); calldataarg arg; f(e, arg); uint256 balanceAAfter = balanceOf(e, a); uint256 balanceBAfter = balanceOf(e, b); uint256 balanceCAfter = balanceOf(e, c); assert !(balanceABefore != balanceAAfter && balanceBBefore != balanceBAfter && balanceCBefore != balanceCAfter); } // only delegationWithSig operation can change the nonce. rule nonceChangePermits(method f) filtered { f -> !f.isView && !disAllowedFunctions(f) } { env e; address user; uint256 oldNonce = nonces(e, user); calldataarg args; f(e, args); uint256 newNonce = nonces(e, user); assert oldNonce != newNonce => f.selector == sig:delegationWithSig(address, address, uint256, uint256, uint8, bytes32, bytes32).selector; } // minting and then burning Variable Debt Token should have no effect on the users balance rule inverseMintBurn(address a, address delegatedUser, uint256 amount, uint256 index) { env e; uint256 balancebefore = balanceOf(e, a); requireInvariant discountCantExceed100Percent(a); mint(e, delegatedUser, a, amount, index); burn(e, a, amount, index); uint256 balanceAfter = balanceOf(e, a); assert balancebefore == balanceAfter, "burn is not the inverse of mint"; } rule integrityDelegationWithSig(address delegator, address delegatee, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) { env e; uint256 oldNonce = nonces(e, delegator); delegationWithSig(e, delegator, delegatee, value, deadline, v, r, s); assert nonces(e, delegator) == oldNonce + 1 && borrowAllowance(e, delegator, delegatee) == value; } /** Burning user u amount of amount tokens, decreases his balanceOf the user by amount. (balance is decreased by amount and not scaled amount because of the summarization to one ray) */ rule integrityOfBurn(address u, uint256 amount) { env e; uint256 index = indexAtTimestamp(e.block.timestamp); uint256 balanceBeforeUser = balanceOf(e, u); uint256 totalSupplyBefore = totalSupply(); burn(e, u, amount, index); uint256 balanceAfterUser = balanceOf(e, u); uint256 totalSupplyAfter = totalSupply(); assert bounded_error_eq(totalSupplyAfter, totalSupplyBefore - amount, 1, index), "total supply integrity"; // total supply reduced assert bounded_error_eq(balanceAfterUser, balanceBeforeUser - amount, 1, index), "integrity break"; // user burns ATokens to receive underlying } rule integrityOfBurn_exact_supply_should_fail(address u, uint256 amount) { env e; uint256 index = indexAtTimestamp(e.block.timestamp); uint256 balanceBeforeUser = balanceOf(e, u); uint256 totalSupplyBefore = totalSupply(); burn(e, u, amount, index); uint256 balanceAfterUser = balanceOf(e, u); uint256 totalSupplyAfter = totalSupply(); assert totalSupplyAfter == totalSupplyBefore - amount, "total supply integrity"; // total supply reduced } rule integrityOfBurn_exact_balance_should_fail(address u, uint256 amount) { env e; uint256 index = indexAtTimestamp(e.block.timestamp); uint256 balanceBeforeUser = balanceOf(e, u); uint256 totalSupplyBefore = totalSupply(); burn(e, u, amount, index); uint256 balanceAfterUser = balanceOf(e, u); uint256 totalSupplyAfter = totalSupply(); assert totalSupplyAfter == totalSupplyBefore - amount, "total supply integrity"; // total supply reduced } /* Burn is additive, can performed either all at once or gradually burn(from,to,x,index); burn(from,to,y,index) ~ burn(from,to,x+y,index) at the same initial state */ rule additiveBurn(address user1, address user2, uint256 x, uint256 y) { env e; uint256 index = indexAtTimestamp(e.block.timestamp); require (user1 != user2 && balanceOf(e, user1) == balanceOf(e, user2)); require user1 != currentContract && user2 != currentContract; burn(e, user1, x, index); burn(e, user1, y, index); uint256 balanceScenario1 = balanceOf(e, user1); burn(e, user2, x+y, index); uint256 balanceScenario2 = balanceOf(e, user2); assert bounded_error_eq(balanceScenario1, balanceScenario2, 3, index), "burn is not additive"; // assert balanceScenario1 == balanceScenario2, "burn is not additive"; } // using too tight bound rule additiveBurn_should_fail(address user1, address user2, uint256 x, uint256 y) { env e; uint256 index = indexAtTimestamp(e.block.timestamp); require (user1 != user2 && balanceOf(e, user1) == balanceOf(e, user2)); require user1 != currentContract && user2 != currentContract; burn(e, user1, x, index); burn(e, user1, y, index); uint256 balanceScenario1 = balanceOf(e, user1); burn(e, user2, x+y, index); uint256 balanceScenario2 = balanceOf(e, user2); assert bounded_error_eq(balanceScenario1, balanceScenario2, 2, index), "burn is not additive"; //assert balanceScenario1 == balanceScenario2, "burn is not additive"; } /* Mint is additive, can performed either all at once or gradually mint(from,to,x,index); mint(from,to,y,index) ~ mint(from,to,x+y,index) at the same initial state */ rule additiveMint(address user1, address user2, address user3, uint256 x, uint256 y) { env e; uint256 index = indexAtTimestamp(e.block.timestamp); require (user1 != user2 && balanceOf(e, user1) == balanceOf(e, user2)); mint(e, user3, user1, x, index); mint(e, user3, user1, y, index); uint256 balanceScenario1 = balanceOf(e, user1); mint(e, user3, user2, x+y, index); uint256 balanceScenario2 = balanceOf(e, user2); assert bounded_error_eq(balanceScenario1, balanceScenario2, 3, index), "burn is not additive"; // assert balanceScenario1 == balanceScenario2, "burn is not additive"; } //using exact comparison rule additiveMint_exact_should_fail(address user1, address user2, address user3, uint256 x, uint256 y) { env e; uint256 index = indexAtTimestamp(e.block.timestamp); require (user1 != user2 && balanceOf(e, user1) == balanceOf(e, user2)); mint(e, user3, user1, x, index); mint(e, user3, user1, y, index); uint256 balanceScenario1 = balanceOf(e, user1); mint(e, user3, user2, x+y, index); uint256 balanceScenario2 = balanceOf(e, user2); //assert bounded_error_eq(balanceScenario1, balanceScenario2, 3, index), "burn is not additive"; assert balanceScenario1 == balanceScenario2, "burn is not additive"; } /** Mint to user u amount of x tokens, increases his balanceOf the user by x. (balance is increased by x and not scaled x because of the summarization to one ray) */ rule integrityMint(address a, uint256 x) { env e; address delegatedUser; uint256 index = indexAtTimestamp(e.block.timestamp); uint256 underlyingBalanceBefore = balanceOf(e, a); uint256 atokenBalanceBefore = scaledBalanceOf(a); uint256 totalATokenSupplyBefore = scaledTotalSupply(e); mint(e, delegatedUser, a, x, index); uint256 underlyingBalanceAfter = balanceOf(e, a); uint256 atokenBalanceAfter = scaledBalanceOf(a); uint256 totalATokenSupplyAfter = scaledTotalSupply(e); assert atokenBalanceAfter - atokenBalanceBefore == totalATokenSupplyAfter - totalATokenSupplyBefore; assert totalATokenSupplyAfter > totalATokenSupplyBefore; assert bounded_error_eq(underlyingBalanceAfter, underlyingBalanceBefore+x, 1, index); // assert balanceAfter == balancebefore+x; } //split rule to three - checking underlying alone rule integrityMint_underlying(address a, uint256 x) { env e; address delegatedUser; uint256 index = indexAtTimestamp(e.block.timestamp); uint256 underlyingBalanceBefore = balanceOf(e, a); uint256 atokenBalanceBefore = scaledBalanceOf(a); uint256 totalATokenSupplyBefore = scaledTotalSupply(e); mint(e, delegatedUser, a, x, index); uint256 underlyingBalanceAfter = balanceOf(e, a); uint256 atokenBalanceAfter = scaledBalanceOf(a); uint256 totalATokenSupplyAfter = scaledTotalSupply(e); //assert atokenBalanceAfter - atokenBalanceBefore == totalATokenSupplyAfter - totalATokenSupplyBefore; //assert totalATokenSupplyAfter > totalATokenSupplyBefore; assert bounded_error_eq(underlyingBalanceAfter, underlyingBalanceBefore+x, 1, index); // assert balanceAfter == balancebefore+x; } //checking atoken alone rule integrityMint_atoken(address a, uint256 x) { env e; address delegatedUser; uint256 index = indexAtTimestamp(e.block.timestamp); uint256 underlyingBalanceBefore = balanceOf(e, a); uint256 atokenBalanceBefore = scaledBalanceOf(a); uint256 totalATokenSupplyBefore = scaledTotalSupply(e); mint(e, delegatedUser, a, x, index); uint256 underlyingBalanceAfter = balanceOf(e, a); uint256 atokenBalanceAfter = scaledBalanceOf(a); uint256 totalATokenSupplyAfter = scaledTotalSupply(e); assert atokenBalanceAfter - atokenBalanceBefore == totalATokenSupplyAfter - totalATokenSupplyBefore; //assert totalATokenSupplyAfter > totalATokenSupplyBefore; //assert bounded_error_eq(underlyingBalanceAfter, underlyingBalanceBefore+x, 1, index); // assert balanceAfter == balancebefore+x; } rule integrityMint_exact_should_fail(address a, uint256 x) { env e; address delegatedUser; uint256 index = indexAtTimestamp(e.block.timestamp); uint256 underlyingBalanceBefore = balanceOf(e, a); uint256 atokenBalanceBefore = scaledBalanceOf(a); uint256 totalATokenSupplyBefore = scaledTotalSupply(e); mint(e, delegatedUser, a, x, index); uint256 underlyingBalanceAfter = balanceOf(e, a); uint256 atokenBalanceAfter = scaledBalanceOf(a); uint256 totalATokenSupplyAfter = scaledTotalSupply(e); assert atokenBalanceAfter - atokenBalanceBefore == totalATokenSupplyAfter - totalATokenSupplyBefore; assert totalATokenSupplyAfter > totalATokenSupplyBefore; assert underlyingBalanceAfter == underlyingBalanceBefore+x; } // Burning zero amount of tokens should have no effect. rule burnZeroDoesntChangeBalance(address u, uint256 index) { env e; uint256 balanceBefore = balanceOf(e, u); burn@withrevert(e, u, 0, index); uint256 balanceAfter = balanceOf(e, u); assert balanceBefore == balanceAfter; } /* Burning one user atokens should have no effect on other users that are not involved in the action. */ rule burnNoChangeToOther(address user, uint256 amount, uint256 index, address other) { require other != user; env e; uint256 otherBalanceBefore = balanceOf(e, other); burn(e, user, amount, index); uint256 otherBalanceAfter = balanceOf(e, other); assert otherBalanceBefore == otherBalanceAfter; } /* Minting ATokens for a user should have no effect on other users that are not involved in the action. */ rule mintNoChangeToOther(address user, address onBehalfOf, uint256 amount, uint256 index, address other) { require other != user && other != onBehalfOf; env e; uint256 userBalanceBefore = balanceOf(e, user); uint256 otherBalanceBefore = balanceOf(e, other); mint(e, user, onBehalfOf, amount, index); uint256 userBalanceAfter = balanceOf(e, user); uint256 otherBalanceAfter = balanceOf(e, other); if (user != onBehalfOf) { assert userBalanceBefore == userBalanceAfter ; } assert otherBalanceBefore == otherBalanceAfter ; } /* Ensuring that the defined disallowed functions revert in any case. */ rule disallowedFunctionalities(method f) filtered { f -> disAllowedFunctions(f) } { env e; calldataarg args; f@withrevert(e, args); assert lastReverted; } ================================================ FILE: certora/gho/specs/erc20.spec ================================================ // erc20 methods methods { function _.name() external => DISPATCHER(true); function _.symbol() external => DISPATCHER(true); function _.decimals() external => DISPATCHER(true); function _.totalSupply() external => DISPATCHER(true); function _.balanceOf(address) external => DISPATCHER(true); function _.allowance(address,address) external => DISPATCHER(true); function _.approve(address,uint256) external => DISPATCHER(true); function _.transfer(address,uint256) external => DISPATCHER(true); function _.transferFrom(address,address,uint256) external => DISPATCHER(true); } ================================================ FILE: certora/gho/specs/flashMinter.spec ================================================ using GhoToken as gho; using GhoAToken as atoken; using MockFlashBorrower as flashBorrower; methods{ function _.isPoolAdmin(address user) external => retrievePoolAdminFromGhost(user) expect bool ALL; function _.isFlashBorrower(address user) external => retrieveFlashBorrowerFromGhost(user) expect bool ALL; function _.onFlashLoan(address, address, uint256, uint256, bytes) external => DISPATCHER(true); function _.getACLManager() external => NONDET; // FlashBorrower function flashBorrower.action() external returns (MockFlashBorrower.Action) envfree; function flashBorrower._transferTo() external returns (address) envfree; function gho.allowance(address, address) external returns (uint256) envfree; function _.burn(uint256) external=> DISPATCHER(true); function _.mint(address, uint256) external=> DISPATCHER(true); function _.transfer(address, uint256) external => DISPATCHER(true); function _.balanceOf(address) external => DISPATCHER(true); function _.decreaseBalanceFromInterest(address, uint256) external => NONDET; function _.getBalanceFromInterest(address) external => NONDET; function gho.totalSupply() external returns (uint256) envfree; function gho.balanceOf(address) external returns (uint256) envfree; function atoken.getGhoTreasury() external returns (address) envfree; } // keeps track of users with pool admin permissions in order to return a consistent value per user ghost mapping(address => bool) poolAdmin_ghost; // keeps track of users with flash borrower permissions in order to return a consistent value per user ghost mapping(address => bool) flashBorrower_ghost; // returns whether the user is a pool admin function retrievePoolAdminFromGhost(address user) returns bool{ return poolAdmin_ghost[user]; } // returns whether the user is a flash borrower function retrieveFlashBorrowerFromGhost(address user) returns bool{ return flashBorrower_ghost[user]; } // a set of assumptions needed for rules that call flashloan function flashLoanReqs(env e){ require e.msg.sender != currentContract; require gho.allowance(currentContract, e.msg.sender) == 0; } // an assumption that demands the sum of balances of 3 given users is no more than the total supply function ghoBalanceOfTwoUsersLETotalSupply(address user1, address user2, address user3){ require gho.balanceOf(user1) + gho.balanceOf(user2) + gho.balanceOf(user3) <= to_mathint(gho.totalSupply()); } /** * @title The GHO balance of the flash minter should grow when calling any function, excluding distributeFees */ rule balanceOfFlashMinterGrows(method f, env e, calldataarg args) filtered { f -> f.selector != sig:distributeFeesToTreasury().selector }{ // No overflow of gho is possible ghoBalanceOfTwoUsersLETotalSupply(currentContract, e.msg.sender, atoken); flashLoanReqs(e); // excluding calls to distribute fees mathint action = assert_uint256(flashBorrower.action()); require action != 1; uint256 _facilitatorBalance = gho.balanceOf(currentContract); f(e, args); uint256 facilitatorBalance_ = gho.balanceOf(currentContract); assert facilitatorBalance_ >= _facilitatorBalance; } /** * @title Checks the integrity of updateGhoTreasury - after update the given address is set */ rule integrityOfTreasurySet(address token){ env e; updateGhoTreasury(e, token); address treasury_ = getGhoTreasury(e); assert treasury_ == token; } /** * @title Checks the integrity of updateFee - after update the given value is set */ rule integrityOfFeeSet(uint256 new_fee){ env e; updateFee(e, new_fee); uint256 fee_ = getFee(e); assert fee_ == new_fee; } /** * @title Checks that the available liquidity, retrieved by maxFlashLoan, stays the same after any action */ rule availableLiquidityDoesntChange(method f, address token){ env e; calldataarg args; uint256 _liquidity = maxFlashLoan(e, token); f(e, args); uint256 liquidity_ = maxFlashLoan(e, token); assert liquidity_ == _liquidity; } /** * @title Checks the integrity of distributeFees: * 1. As long as the treasury contract itself isn't acting as a flashloan minter, the flashloan facilitator's GHO balance should be empty after distribution * 2. The change in balances of the receiver (treasury) and the sender (flash minter) is the same. i.e. no money is being generated out of thin air */ rule integrityOfDistributeFeesToTreasury(){ env e; address treasury = getGhoTreasury(e); uint256 _facilitatorBalance = gho.balanceOf(currentContract); uint256 _treasuryBalance = gho.balanceOf(treasury); // No overflow of gho is possible ghoBalanceOfTwoUsersLETotalSupply(currentContract, treasury, atoken); distributeFeesToTreasury(e); uint256 facilitatorBalance_ = gho.balanceOf(currentContract); uint256 treasuryBalance_ = gho.balanceOf(treasury); assert treasury != currentContract => facilitatorBalance_ == 0; assert treasuryBalance_ - _treasuryBalance == _facilitatorBalance - facilitatorBalance_; } /** * @title Checks that the fee amount reported by flashFee is the the same as the actual fee that is taken by flashloaning */ rule feeSimulationEqualsActualFee(address receiver, address token, uint256 amount, bytes data){ env e; mathint feeSimulationResult = flashFee(e, token, amount); uint256 _facilitatorBalance = gho.balanceOf(currentContract); flashLoanReqs(e); require atoken.getGhoTreasury() != currentContract; // No overflow of gho is possible ghoBalanceOfTwoUsersLETotalSupply(currentContract, e.msg.sender, atoken); // Excluding call to distributeFeesToTreasury & calling another flashloan (which will generate another fee in recursion) mathint borrower_action = assert_uint256(flashBorrower.action()); require borrower_action != 1 && borrower_action != 0; // Because we calculate the actual fee by balance difference of the minter, we assume no extra money is being sent to the minter. require flashBorrower._transferTo() != currentContract; flashLoan(e, receiver, token, amount, data); uint256 facilitatorBalance_ = gho.balanceOf(currentContract); mathint actualFee = facilitatorBalance_ - _facilitatorBalance; assert feeSimulationResult == actualFee; } rule sanity { env e; calldataarg arg; method f; f(e, arg); satisfy true; } ================================================ FILE: certora/gho/specs/ghoAToken.spec ================================================ import "erc20.spec"; using GhoTokenHarness as _ghoTokenHarness; methods{ function totalSupply() external returns (uint256) envfree; function RESERVE_TREASURY_ADDRESS() external returns (address) envfree; function UNDERLYING_ASSET_ADDRESS() external returns (address) envfree; function transferUnderlyingTo(address,uint256) external; function handleRepayment(address,address,uint256) external; function distributeFeesToTreasury() external envfree ; function rescueTokens(address,address,uint256) external; function setVariableDebtToken(address) external; function getVariableDebtToken() external returns (address) envfree; function updateGhoTreasury(address) external ; function getGhoTreasury() external returns (address) envfree; function _ghoTokenHarness.getFacilitatorBucketCapacity(address) external returns (uint256) envfree; function _ghoTokenHarness.getFacilitatorBucketLevel(address) external returns (uint256) envfree; function _ghoTokenHarness.balanceOf(address) external returns (uint256) envfree; function scaledBalanceOf(address) external returns (uint256) envfree; /******************* * Pool.sol * ********************/ function _.getReserveNormalizedIncome(address) external => CONSTANT; /*********************************** * PoolAddressesProvider.sol * ************************************/ function _.getACLManager() external => CONSTANT; /************************ * ACLManager.sol * *************************/ function _.isPoolAdmin(address) external => CONSTANT; } /** * @title Proves that ghoAToken::mint always reverts **/ rule noMint() { env e; calldataarg args; mint(e, args); assert(false); } /** * @title Proves that ghoAToken::burn always reverts **/ rule noBurn() { env e; calldataarg args; burn(e, args); assert(false); } /** * @title Proves that ghoAToken::transfer always reverts **/ rule noTransfer() { env e; calldataarg args; transfer(e, args); assert(false); } /** * @title Proves that calling ghoAToken::transferUnderlyingTo will revert if the amount exceeds the excess capacity * @notice A user can’t borrow more than the facilitator’s remaining capacity. **/ rule transferUnderlyingToCantExceedCapacity() { address target; uint256 amount; env e; mathint facilitatorLevel = _ghoTokenHarness.getFacilitatorBucketLevel(currentContract); mathint facilitatorCapacity = _ghoTokenHarness.getFacilitatorBucketCapacity(currentContract); transferUnderlyingTo@withrevert(e, target, amount); assert(to_mathint(amount) > (facilitatorCapacity - facilitatorLevel) => lastReverted); } /** * @title Proves that the total supply of GhoAToken is always zero **/ rule totalSupplyAlwaysZero() { assert(totalSupply() == 0); } /** * @title Proves that any user's balance of GhoAToken is always zero **/ invariant userBalanceAlwaysZero(address user) scaledBalanceOf(user) == 0; // /** // * @title first handleRepayment(amount) after transferUnderlyingTo(amount) succeeds. // * @dev assumption of sufficient balanceOf(msg.sender) is justified because BorrowLogic.executeRepay() // * @dev executes: IERC20(params.asset).safeTransferFrom(msg.sender, reserveCache.aTokenAddress, paybackAmount); // * @dev before invocation of handleRepayment() // * OBSOLETE - GhoToken has other rules to validate the behavior of the facilitator level maintenance // */ // rule handleRepayment_after_transferUnderlyingTo() // { // env e; // calldataarg arg; // uint256 amount; // address target; // address user; // address onBehalfOf; // transferUnderlyingTo(e, target, amount); // require _ghoTokenHarness.balanceOf(e.msg.sender) >= amount; //underlying asset // require e.msg.sender == currentContract; // handleRepayment@withrevert(e, user, onBehalfOf, amount); // assert !lastReverted, "handleRepayment failed"; // } /** * @title BucketLevel decreases after transferUnderlyingTo() followed by handleRepayment() * @dev repayment funds are, partially or fully, transferred to the treasury */ rule level_does_not_decrease_after_transferUnderlyingTo_followed_by_handleRepayment() { env e; calldataarg arg; uint256 amount; address target; address user; address onBehalfOf; uint256 levelBefore = _ghoTokenHarness.getFacilitatorBucketLevel(currentContract); transferUnderlyingTo(e, target, amount); handleRepayment(e, user, onBehalfOf, amount); uint256 levelAfter = _ghoTokenHarness.getFacilitatorBucketLevel(currentContract); assert levelBefore <= levelAfter; } ================================================ FILE: certora/gho/specs/ghoDiscountRateStrategy.spec ================================================ methods { function calculateDiscountRate(uint256, uint256) external returns (uint256) envfree; function MIN_DISCOUNT_TOKEN_BALANCE() external returns (uint256) envfree; function MIN_DEBT_TOKEN_BALANCE() external returns (uint256) envfree; function DISCOUNT_RATE() external returns (uint256) envfree; function GHO_DISCOUNTED_PER_DISCOUNT_TOKEN() external returns (uint256) envfree; function wadMul(uint256, uint256) external returns (uint256) envfree; } function wad() returns uint256 { return 10^18; } function wadMulCVL(uint256 a, uint256 b) returns mathint { return ((a * b + (wad() / 2)) / wad()); } /** * @title sanity rule, checks that all contract functions are executables, should fail. **/ // rule sanity(method f) { // env e; // calldataarg args; // f(e, args); // assert(false); // } /** * @title prove the equivalence between wadMulCVL and the solidity implementation of wadMul **/ rule equivalenceOfWadMulCVLAndWadMulSol() { uint256 x; uint256 y; mathint wadMulCvl = wadMulCVL(x, y); uint256 wadMulSol = wadMul(x, y); assert(wadMulCvl == to_mathint(wadMulSol)); } /** * @title proves that if the account's entitled balance for discount is above its current debt balance than the discount rate is the maximal rate **/ rule maxDiscountForHighDiscountTokenBalance() { uint256 debtBalance; uint256 discountTokenBalance; mathint discountedBalance = wadMulCVL(GHO_DISCOUNTED_PER_DISCOUNT_TOKEN(), discountTokenBalance); uint256 rate = calculateDiscountRate(debtBalance, discountTokenBalance); // forcing the debt/discount token balance to be above the minimal value allowed in order to get a non-zero rate require(debtBalance >= MIN_DEBT_TOKEN_BALANCE() && discountTokenBalance >= MIN_DISCOUNT_TOKEN_BALANCE()); assert(discountedBalance >= to_mathint(debtBalance) => rate == DISCOUNT_RATE()); } /** * @title proves that the discount balance below the threshold leads to zero discount rate **/ rule zeroDiscountForSmallDiscountTokenBalance() { uint256 debtBalance; uint256 discountTokenBalance; uint256 rate = calculateDiscountRate(debtBalance, discountTokenBalance); mathint discountedBalance = wadMulCVL(GHO_DISCOUNTED_PER_DISCOUNT_TOKEN(), discountTokenBalance); // there are three conditions that can result in a zero rate: // 1,2 - if the debt balance or the discount token balance are below some threshold. // 3 - if debtBalance is much larger than discountBalance (since the return value is the max rate multiplied // by the ratio between debtBalance and discountBalance) assert( (debtBalance < MIN_DEBT_TOKEN_BALANCE() || discountTokenBalance < MIN_DISCOUNT_TOKEN_BALANCE() || discountedBalance*DISCOUNT_RATE() < to_mathint(debtBalance)) <=> rate == 0); } /** * @title if the discounted balance is above the threshold and below the current debt, the discount rate will be according to the ratio * between the debt balance and the discounted balance **/ rule partialDiscountForIntermediateTokenBalance() { uint256 debtBalance; uint256 discountTokenBalance; mathint discountedBalance = wadMulCVL(GHO_DISCOUNTED_PER_DISCOUNT_TOKEN(), discountTokenBalance); uint256 rate = calculateDiscountRate(debtBalance, discountTokenBalance); require(debtBalance >= MIN_DEBT_TOKEN_BALANCE() && discountTokenBalance >= MIN_DISCOUNT_TOKEN_BALANCE()); assert(discountedBalance < to_mathint(debtBalance) => (to_mathint(rate) == (discountedBalance * DISCOUNT_RATE()) / debtBalance)); } /** * @title proves that the discount rate is caped by the maximal discount rate value **/ rule limitOnDiscountRate() { uint256 debtBalance; uint256 discountTokenBalance; uint256 discountRate = calculateDiscountRate(debtBalance, discountTokenBalance); assert(discountRate <= DISCOUNT_RATE()); } rule sanity { env e; calldataarg arg; method f; f(e, arg); satisfy true; } ================================================ FILE: certora/gho/specs/ghoToken.spec ================================================ import "set.spec"; methods{ function mint(address,uint256) external; function burn(uint256) external; function removeFacilitator(address) external; function setFacilitatorBucketCapacity(address,uint128) external; function totalSupply() external returns uint256 envfree; function balanceOf(address) external returns (uint256) envfree; function getFacilitatorBucketLevel(address) external returns uint256 envfree; function getFacilitatorBucketCapacity(address) external returns uint256 envfree; function is_in_facilitator_mapping(address) external returns bool envfree; function is_in_facilitator_set_map(address) external returns bool envfree; function is_in_facilitator_set_array(address) external returns bool envfree; } ghost sumAllBalance() returns mathint { init_state axiom sumAllBalance() == 0; } hook Sstore balanceOf[KEY address a] uint256 balance (uint256 old_balance) STORAGE { havoc sumAllBalance assuming sumAllBalance@new() == sumAllBalance@old() + balance - old_balance; } hook Sload uint256 balance balanceOf[KEY address a] STORAGE { require to_mathint(balance) <= sumAllBalance(); } ghost sumAllLevel() returns mathint { init_state axiom sumAllLevel() == 0; } /** * @title Sum of facilitators' bucket levels * @dev Sample stores to _facilitators[*].bucketLevel * @dev first field of struct Facilitator is uint128 so offset 16 is used **/ hook Sstore _facilitators[KEY address a].(offset 16) uint128 level (uint128 old_level) STORAGE { havoc sumAllLevel assuming sumAllLevel@new() == sumAllLevel@old() + level - old_level; } // // Invariants // // INV #1 /** * @title Length of AddressSet is less than 2^160 * @dev the assumption is safe because there are at most 2^160 unique addresses * @dev the proof of the assumption is vacuous because length > loop_iter */ invariant length_leq_max_uint160() getFacilitatorsListLen() < TWO_TO_160(); // INV #2 /** * @title User's balance not greater than totalSupply() */ invariant inv_balanceOf_leq_totalSupply(address user) balanceOf(user) <= totalSupply() { preserved { requireInvariant sumAllBalance_eq_totalSupply(); } } // INV #3 /** * @title Sum of bucket levels is equals to GhoToken::totalSupply() **/ invariant total_supply_eq_sumAllLevel() sumAllLevel() == to_mathint(totalSupply()) { preserved burn(uint256 amount) with (env e){ requireInvariant inv_balanceOf_leq_totalSupply(e.msg.sender); } } // INV #4 /** * @title Sum of balances is GhoToke::totalSupply() * @dev EITHER requireInvariant sumAllLevel_eq_sumAllBalance() OR requireInvariant total_supply_eq_sumAllLevel() suffices. **/ //todo: replace preserve invariant sumAllBalance_eq_totalSupply() sumAllBalance() == to_mathint(totalSupply()) { preserved { requireInvariant sumAllLevel_eq_sumAllBalance(); } } // INV #5 /** * @title The sum of bucket level is equal to the sum of GhoToken balances * @dev This invariant can be deduced from sumAllBalance_eq_totalSupply and total_supply_eq_sumAllLevel * @dev requireInvariant of EITHER sumAllBalance_eq_totalSupply() OR total_supply_eq_sumAllLevel() suffuces for the proof **/ invariant sumAllLevel_eq_sumAllBalance() sumAllLevel() == sumAllBalance() { preserved { requireInvariant sumAllBalance_eq_totalSupply(); } } // INV #6 /** * @title A facilitator with a positive bucket capacity exists in the _facilitators mapping */ invariant inv_valid_capacity(address facilitator) ((getFacilitatorBucketCapacity(facilitator)>0) => is_in_facilitator_mapping(facilitator) ); // INV #7 /** * @title A facilitator with a positive bucket level exists in the _facilitators mapping */ invariant inv_valid_level(address facilitator) ((getFacilitatorBucketLevel(facilitator)>0) => is_in_facilitator_mapping(facilitator) ) { preserved{ requireInvariant inv_valid_capacity(facilitator); } } // INV #8 /** * @title AddressSet internal coherency * @dev A facilitator address exists in AddressSet list (GhoToken._facilitatorsList._values) * @dev if and only if it exists in AddressSet mapping (GhoToken._facilitatorsList._indexes) */ invariant address_in_set_values_iff_in_set_indexes(address facilitator) is_in_facilitator_set_array(facilitator) <=> is_in_facilitator_set_map(facilitator) {preserved{ requireInvariant addressSetInvariant(); requireInvariant length_leq_max_uint160(); } } // INV #9 /** * @title GhoToken mapping-AddressSet coherency (1) * @dev A facilitator address that exists in GhoToken Facilitator mapping (GhoToken._facilitators) * @dev if and only if it exists in GhoToken AddressSet (GhoToken._facilitatorsList._indexes) */ invariant addr_in_set_iff_in_map(address facilitator) is_in_facilitator_mapping(facilitator) <=> is_in_facilitator_set_map(facilitator) {preserved{ requireInvariant addressSetInvariant(); } } // INV #10 /** * @title GhoToken mapping-AddressSet coherency (2) * @dev A facilitator address exists in GhoToken Facilitator mapping (GhoToken._facilitators) * @dev iff it exists in GhoToken AddressSet list (GhoToken._facilitatorsList._values) */ invariant addr_in_set_list_iff_in_map(address facilitator) is_in_facilitator_mapping(facilitator) <=> is_in_facilitator_set_array(facilitator) {preserved{ requireInvariant addressSetInvariant(); requireInvariant length_leq_max_uint160(); } } // // Rules // /** * @title Bucket level <= bucket capacity unless setFacilitatorBucketCapacity() lowered it */ rule level_leq_capacity(address facilitator, method f) filtered {f -> !f.isView}{ env e; calldataarg arg; requireInvariant inv_valid_capacity(facilitator); require getFacilitatorBucketLevel(facilitator) <= getFacilitatorBucketCapacity(facilitator); f(e, arg); assert ((f.selector != sig:setFacilitatorBucketCapacity(address,uint128).selector) => (getFacilitatorBucketLevel(facilitator) <= getFacilitatorBucketCapacity(facilitator))); } /** * @notice If Bucket level < bucket capacity then the first invocation of mint() succeeds after burn * @notice unless setFacilitatorBucketCapacity() lowered bucket capacity or removeFacilitator() was called */ rule mint_after_burn(method f) filtered {f -> !f.isView} { env e; calldataarg arg; uint256 amount_burn; uint256 amount_mint; address account; require getFacilitatorBucketLevel(e.msg.sender) <= getFacilitatorBucketCapacity(e.msg.sender); require amount_mint > 0; requireInvariant addressSetInvariant(); requireInvariant inv_balanceOf_leq_totalSupply(e.msg.sender); requireInvariant inv_valid_capacity(e.msg.sender); burn(e, amount_burn); f(e, arg); mint@withrevert(e, account, amount_mint); assert (((amount_mint <= amount_burn) && f.selector != sig:mint(address,uint256).selector && f.selector != sig:setFacilitatorBucketCapacity(address,uint128).selector && f.selector != sig:removeFacilitator(address).selector ) => !lastReverted), "mint failed"; } /** * @title Burn after mint succeeds * @dev BorrowLogic::executeRepa() executes the following code before invocation of handleRepayment() * @dev safeTransferFrom(msg.sender, reserveCache.aTokenAddress, paybackAmount); */ rule burn_after_mint(method f) filtered {f -> !f.isView} { env e; uint256 amount; address account; requireInvariant inv_balanceOf_leq_totalSupply(e.msg.sender); require e.msg.value == 0; require amount > 0; mint(e, account, amount); transferFrom(e, account, e.msg.sender, amount); burn@withrevert(e, amount); assert !lastReverted, "burn failed"; } /** * @title BucketLevel remains unchanged after mint() followed by burn() */ rule level_unchanged_after_mint_followed_by_burn() { env e; calldataarg arg; uint256 amount; address account; uint256 levelBefore = getFacilitatorBucketLevel(e.msg.sender); mint(e, account, amount); burn(e, amount); uint256 leveAfter = getFacilitatorBucketLevel(e.msg.sender); assert levelBefore == leveAfter; } rule level_after_mint() { env e; calldataarg arg; uint256 amount; address account; uint256 levelBefore = getFacilitatorBucketLevel(e.msg.sender); mint(e, account, amount); uint256 leveAfter = getFacilitatorBucketLevel(e.msg.sender); assert levelBefore + amount == to_mathint(leveAfter); } rule level_after_burn() { env e; calldataarg arg; uint256 amount; uint256 levelBefore = getFacilitatorBucketLevel(e.msg.sender); burn(e, amount); uint256 leveAfter = getFacilitatorBucketLevel(e.msg.sender); assert to_mathint(levelBefore) == leveAfter + amount; } /** * @title Facilitator is valid after successful call to setFacilitatorBucketCapacity() */ rule facilitator_in_list_after_setFacilitatorBucketCapacity(){ env e; address facilitator; uint128 newCapacity; requireInvariant addr_in_set_iff_in_map(facilitator); requireInvariant addr_in_set_list_iff_in_map(facilitator); setFacilitatorBucketCapacity(e, facilitator, newCapacity); assert is_in_facilitator_set_map(facilitator); assert is_in_facilitator_set_array(facilitator); } /** * @title getFacilitatorBucketCapacity() called after setFacilitatorBucketCapacity() return the assign bucket capacity */ rule getFacilitatorBucketCapacity_after_setFacilitatorBucketCapacity(){ env e; address facilitator; uint128 newCapacity; setFacilitatorBucketCapacity(e, facilitator, newCapacity); assert getFacilitatorBucketCapacity(facilitator) == require_uint256(newCapacity); } /** * @title Facilitator is valid after successful call to addFacilitator() */ rule facilitator_in_list_after_addFacilitator(){ env e; address facilitator; string label; uint128 capacity; requireInvariant addr_in_set_iff_in_map(facilitator); addFacilitator(e,facilitator, label, capacity); assert is_in_facilitator_set_map(facilitator); assert is_in_facilitator_set_array(facilitator); } /** * @title Facilitator is valid after successful call to mint() or burn() */ rule facilitator_in_list_after_mint_and_burn(method f){ env e; calldataarg args; requireInvariant inv_valid_capacity(e.msg.sender); requireInvariant inv_valid_level(e.msg.sender); requireInvariant addr_in_set_iff_in_map(e.msg.sender); requireInvariant addr_in_set_list_iff_in_map(e.msg.sender); f(e,args); assert (((f.selector == sig:mint(address,uint256).selector) || (f.selector == sig:burn(uint256).selector)) => is_in_facilitator_mapping(e.msg.sender)); assert (((f.selector == sig:mint(address,uint256).selector) || (f.selector == sig:burn(uint256).selector)) => is_in_facilitator_set_map(e.msg.sender)); assert (((f.selector == sig:mint(address,uint256).selector) || (f.selector == sig:burn(uint256).selector)) => is_in_facilitator_set_array(e.msg.sender)); } /** * @title Facilitator address is removed from list (GhoToken._facilitatorsList._values) after calling removeFacilitator() **/ rule address_not_in_list_after_removeFacilitator(address facilitator){ env e; requireInvariant addressSetInvariant(); requireInvariant length_leq_max_uint160(); requireInvariant addr_in_set_iff_in_map(facilitator); removeFacilitator(e, facilitator); assert !is_in_facilitator_set_array(facilitator); } /** * @title Proves that mint(a + b) == mint(a) + mint(b) **/ // rule mintIsAdditive() { // address user1; // address user2; // require (user1 != user2); // uint256 initBalance1 = balanceOf(user1); // uint256 initBalance2 = balanceOf(user2); // require (sumAllBalance() >= initBalance1 + initBalance2); // requireInvariant sumAllBalance_eq_totalSupply(); // uint256 amount1; // uint256 amount2; // uint256 sum = amount1 + amount2; // env e; // mint(e, user1, amount1); // mint(e, user1, amount2); // mint(e, user2, sum); // uint256 finBalance1 = balanceOf(user1); // uint256 finBalance2 = balanceOf(user2); // mathint diff1 = finBalance1 - initBalance1; // mathint diff2 = finBalance2 - initBalance2; // assert diff1 == diff2; // } rule balance_after_mint() { env e; address user; uint256 initBalance = balanceOf(user); uint256 initSupply = totalSupply(); uint256 amount; requireInvariant sumAllBalance_eq_totalSupply(); mint(e, user, amount); uint256 finBalance = balanceOf(user); uint256 finSupply = totalSupply(); assert initBalance + amount == to_mathint(finBalance); assert initSupply + amount == to_mathint(finSupply); } rule balance_after_burn() { env e; requireInvariant inv_balanceOf_leq_totalSupply(e.msg.sender); uint256 initBalance = balanceOf(e.msg.sender); uint256 initSupply = totalSupply(); uint256 amount; burn(e, amount); uint256 finBalance = balanceOf(e.msg.sender); uint256 finSupply = totalSupply(); assert to_mathint(initBalance) == finBalance + amount; assert to_mathint(initSupply) == finSupply + amount ; } /** * @title Proves that burn(a + b) == burn(a) + burn(b) **/ // rule burnIsAdditive() { // env e; // uint256 senderBalance = balanceOf(e.msg.sender); // require(senderBalance <= sumAllBalance()); // requireInvariant sumAllBalance_eq_totalSupply(); // uint256 amount1; // uint256 amount2; // uint256 sum = amount1 + amount2; // uint256 initSupply = totalSupply(); // burn(e, amount1); // burn(e, amount2); // uint256 midSupply = totalSupply(); // burn(e, sum); // uint256 finSupply = totalSupply(); // mathint diff1 = finSupply - midSupply; // mathint diff2 = midSupply - initSupply; // assert diff1 == diff2; // } /** * @title Proves that you can't mint more than the facilitator's remaining capacity **/ rule mintLimitedByFacilitatorRemainingCapacity() { env e; require(getFacilitatorBucketCapacity(e.msg.sender) > getFacilitatorBucketLevel(e.msg.sender)); uint256 amount; require(to_mathint(amount) > (getFacilitatorBucketCapacity(e.msg.sender) - getFacilitatorBucketLevel(e.msg.sender))); address user; mint@withrevert(e, user, amount); assert lastReverted; } /** * @title Proves that you can't burn more than the facilitator's current level **/ rule burnLimitedByFacilitatorLevel() { env e; require(getFacilitatorBucketCapacity(e.msg.sender) > getFacilitatorBucketLevel(e.msg.sender)); uint256 amount; require(amount > getFacilitatorBucketLevel(e.msg.sender)); burn@withrevert(e, amount); assert lastReverted; } // // Additional rules // //keep these rules for development team - resolve timeouts, fix bugs //pass with workaround for https://certora.atlassian.net/browse/CERT-1060 invariant ARRAY_IS_INVERSE_OF_MAP_Invariant() ARRAY_IS_INVERSE_OF_MAP() { preserved{ require ADDRESS_SET_INVARIANT(); requireInvariant length_leq_max_uint160(); } } //pass with workaround for https://certora.atlassian.net/browse/CERT-1060 invariant addressSetInvariant() ADDRESS_SET_INVARIANT() { preserved{ requireInvariant length_leq_max_uint160(); } } //Debugging https://certora.atlassian.net/browse/CERT-1060 //timeout with staging //fail with yoav/grounding //pass with axiom mirrorArrayLen < TWO_TO_160() - 1 rule address_not_in_list_after_removeFacilitator_CASE_SPLIT_zero_address(address facilitator){ env e; requireInvariant addressSetInvariant(); require facilitator == 0; requireInvariant addr_in_set_iff_in_map(facilitator); removeFacilitator(e, facilitator); assert !is_in_facilitator_set_array(facilitator); } ================================================ FILE: certora/gho/specs/ghoVariableDebtToken-rayMulDiv-summarization.spec ================================================ //import "erc20.spec" import "VariableDebtToken.spec"; import "summarizations.spec"; using GhoDiscountRateStrategy as discStrategy; methods{ /********************; * WadRayMath.sol *; *********************/ function _.rayMul(uint256 a,uint256 b) internal => rayMul_gst(a,b) expect uint256 ALL; function _.rayDiv(uint256 a,uint256 b) internal => rayDiv_gst(a,b) expect uint256 ALL; function getDiscountPercent(address user) external returns (uint256) envfree; function get_ghoAToken() external returns (address) envfree; /***********************************; * PoolAddressesProvider.sol *; ************************************/ function _.getACLManager() external => CONSTANT; /************************; * ACLManager.sol *; *************************/ function _.isPoolAdmin(address) external => CONSTANT; /******************************************************************; * DummyERC20WithTimedBalanceOf.sol (linked to _discountToken) *; *******************************************************************/ function _._balanceOfWithBlockTimestamp(address user, uint256 ts) internal => balanceOfDiscountTokenAtTimestamp(user, ts) expect uint256; /************************************; * DummyPool.sol (linked to POOL) *; *************************************/ function _._getReserveNormalizedVariableDebtWithBlockTimestamp(address asset, uint256 timestamp) internal => indexAtTimestamp(timestamp) expect uint256; /************************************; * GhoVariableDebtTokenHarness.sol *; *************************************/ function discStrategy.calculateDiscountRate(uint256, uint256) external returns (uint256) envfree; /************************************; * GhoVariableDebtTokenHarness.sol *; *************************************/ function getUserCurrentIndex(address) external returns (uint256) envfree; function getUserDiscountRate(address) external returns (uint256) envfree; function getUserAccumulatedDebtInterest(address) external returns (uint256) envfree; function getBalanceOfDiscountToken(address) external returns (uint256); /********************************; * GhoVariableDebtToken.sol *; *********************************/ function totalSupply() external returns(uint256) envfree; function balanceOf(address) external returns (uint256); function mint(address, address, uint256, uint256) external returns (bool, uint256); function burn(address ,uint256 ,uint256) external returns (uint256); function scaledBalanceOf(address) external returns (uint256) envfree; function getBalanceFromInterest(address) external returns (uint256) envfree; function rebalanceUserDiscountPercent(address) external; function updateDiscountDistribution(address ,address ,uint256 ,uint256 ,uint256) external; } ghost rayMul_gst(mathint , mathint) returns uint256 { //axiom 1==1; axiom forall mathint x. forall mathint y. //rayMul_gst(x,y)+0 == x; ( ((x==0||y==0) => rayMul_gst(x,y)==0) && x <= to_mathint(rayMul_gst(x,y)) && to_mathint(rayMul_gst(x,y)) <= 2*x ) ; } ghost rayDiv_gst(mathint , mathint) returns uint256 { // axiom 1==1; axiom forall mathint x. forall mathint y. //rayDiv_gst(x,y)+0 == x; ( x/2 <= to_mathint(rayDiv_gst(x,y)) && to_mathint(rayDiv_gst(x,y)) <= x ); } definition MAX_DISCOUNT() returns uint256 = 10000; // equals to 100% discount, in points ghost mapping(address => mapping (uint256 => uint256)) discount_ghost; ghost mapping(uint256 => uint256) index_ghost; /** * Query index_ghost for the index value at the input timestamp **/ function indexAtTimestamp(uint256 timestamp) returns uint256 { require index_ghost[timestamp] >= ray(); return index_ghost[timestamp]; } /** * Query discount_ghost for the [user]'s balance of discount token at [timestamp] **/ function balanceOfDiscountTokenAtTimestamp(address user, uint256 timestamp) returns uint256 { return discount_ghost[user][timestamp]; } function get_discount_scaled(env e, address user, uint256 current_index) returns uint256 { uint256 user_scaledBal_prev = scaledBalanceOf(user); // assert (user_scaledBal_after <= user_scaledBal_prev); // uint256 current_index = indexAtTimestamp(e.block.timestamp); uint256 user_index = getUserCurrentIndex(user); require user_index <= current_index; //uint256 bal_increase = (current_index-sender_index) * previousScaledBalance_of_sender; mathint bal_increase = rayMul_gst(user_scaledBal_prev, current_index) - rayMul_gst(user_scaledBal_prev, user_index); //uint256 discountScaled = bal_increase * sender_precentage / index; uint256 discountPercent = getDiscountPercent(e, user); uint256 discount = require_uint256(bal_increase * discountPercent / MAX_DISCOUNT()); uint256 discountScaled = rayDiv_gst(discount, current_index); return discountScaled; } /*================================================================================ Calling to mint(...amount) can't increase the scaled balance by more than scaled-amount. (This catches mutant 11) =================================================================================*/ rule mint_cant_increase_bal_by_more_than_amountScaled() { env e; address user; address onBehalfOf; uint256 amount; uint256 index; require getUserCurrentIndex(onBehalfOf) <= index; uint256 amountScaled = rayDiv_gst(amount,index); uint256 prev_bal = scaledBalanceOf(e, onBehalfOf); mint(e,user,onBehalfOf,amount,index); mathint after_bal = scaledBalanceOf(e, onBehalfOf); assert (after_bal <= prev_bal + amountScaled); } /*================================================================================ When calling updateDiscountDistribution, if discountScaled>0 then the balance of the sender must decrease. (This catches mutant 5) =================================================================================*/ rule discount_takes_place_in_updateDiscountDistribution__sender() { env e; address sender; address recipient; uint256 senderDiscountTokenBalance; uint256 recipientDiscountTokenBalance; uint256 amount; uint256 sender_scaledBal_prev = scaledBalanceOf(sender); uint256 discountScaled = get_discount_scaled(e,sender,indexAtTimestamp(e.block.timestamp)); updateDiscountDistribution(e, sender, recipient, senderDiscountTokenBalance, recipientDiscountTokenBalance, amount); uint256 sender_scaledBal_after = scaledBalanceOf(sender); if (sender != recipient){ assert (discountScaled > 0 => sender_scaledBal_after0 then the balance of the recipient must decrease. (This catches mutant 13) =================================================================================*/ rule discount_takes_place_in_updateDiscountDistribution__recipient() { env e; address sender; address recipient; uint256 senderDiscountTokenBalance; uint256 recipientDiscountTokenBalance; uint256 amount; uint256 recipient_scaledBal_prev = scaledBalanceOf(recipient); uint256 discountScaled = get_discount_scaled(e,recipient,indexAtTimestamp(e.block.timestamp)); updateDiscountDistribution(e, sender, recipient, senderDiscountTokenBalance, recipientDiscountTokenBalance, amount); uint256 recipient_scaledBal_after = scaledBalanceOf(recipient); if (sender != recipient){ assert (discountScaled > 0 => recipient_scaledBal_after _after_scaledBal==0; assert (to_mathint(amount)!=_prev_bal && _discountScaled>0) => _after_scaledBal < _prev_scaledBal - _amountScaled; } ================================================ FILE: certora/gho/specs/ghoVariableDebtToken.spec ================================================ //import "erc20.spec" import "VariableDebtToken.spec"; import "summarizations.spec"; using GhoDiscountRateStrategy as discStrategy; methods{ /********************; * WadRayMath.sol *; *********************/ // function _.rayMul(uint256 x, uint256 y) internal => rayMulSummariztion(x, y) expect(uint256); function rayDiv(uint256 x, uint256 y) external returns uint256 envfree; function rayMul(uint256 x, uint256 y) external returns uint256 envfree; /***********************************; * PoolAddressesProvider.sol *; ************************************/ function _.getACLManager() external => CONSTANT; /************************; * ACLManager.sol *; *************************/ function _.isPoolAdmin(address) external => CONSTANT; /******************************************************************; * DummyERC20WithTimedBalanceOf.sol (linked to _discountToken) *; *******************************************************************/ // Internal function in DummyERC20WithTimedBalanceOf which exposes the block's timestamp and called by DummyERC20WithTimedBalanceOf::balanceOf function _._balanceOfWithBlockTimestamp(address user, uint256 ts) internal => balanceOfDiscountTokenAtTimestamp(user, ts) expect uint256; /************************************; * DummyPool.sol (linked to POOL) *; *************************************/ // Internal function in DummyPool which exposes the block's timestamp and called by Pool::getReserveNormalizedVariableDebt function _._getReserveNormalizedVariableDebtWithBlockTimestamp(address asset, uint256 timestamp) internal => indexAtTimestamp(timestamp) expect uint256; /************************************; * GhoVariableDebtTokenHarness.sol *; *************************************/ function discStrategy.calculateDiscountRate(uint256, uint256) external returns (uint256) envfree; /************************************; * GhoVariableDebtTokenHarness.sol *; *************************************/ function getUserCurrentIndex(address) external returns (uint256) envfree; function getUserDiscountRate(address) external returns (uint256) envfree; function getUserAccumulatedDebtInterest(address) external returns (uint256) envfree; function getBalanceOfDiscountToken(address) external returns (uint256); function getDiscountToken() external returns (address) envfree; /********************************; * GhoVariableDebtToken.sol *; *********************************/ function totalSupply() external returns(uint256) envfree; function balanceOf(address) external returns (uint256); function mint(address, address, uint256, uint256) external returns (bool, uint256); function burn(address ,uint256 ,uint256) external returns (uint256); function scaledBalanceOf(address) external returns (uint256) envfree; function getBalanceFromInterest(address) external returns (uint256) envfree; function rebalanceUserDiscountPercent(address) external; function updateDiscountDistribution(address ,address ,uint256 ,uint256 ,uint256) external; /********************************; * GhoDiscountRateStrategy.sol *; *********************************/ function discStrategy.DISCOUNT_RATE() external returns (uint256) envfree; } /** * CVL implementation of rayMul **/ function rayMulCVL(uint256 a, uint256 b) returns mathint { return ((a * b + (ray() / 2)) / ray()); } function rayDivCVL(uint256 a, uint256 b) returns mathint { return ((a * ray() + (b / 2)) / b); } function getReserveNormalizedVariableDebt_1ray() returns mathint { return ray(); } function getReserveNormalizedVariableDebt_1or2ray() returns uint256 { uint256 index; require (index==ray() || to_mathint(index)==2*ray()); return index; } function getReserveNormalizedVariableDebt_7ray() returns uint256 { uint256 index; require (to_mathint(index)==7*ray()); return index; } //todo: check balanceof after mint (stable index), burn after balanceof definition MAX_DISCOUNT() returns uint256 = 10000; // equals to 100% discount, in points ghost mapping(address => mapping (uint256 => uint256)) discount_ghost; ghost mapping(uint256 => uint256) index_ghost; /** * Query index_ghost for the index value at the input timestamp **/ function indexAtTimestamp(uint256 timestamp) returns uint256 { require index_ghost[timestamp] >= ray(); return index_ghost[timestamp]; // return 1001684385021630839436707910;//index_ghost[timestamp]; } /** * Query discount_ghost for the [user]'s balance of discount token at [timestamp] **/ function balanceOfDiscountTokenAtTimestamp(address user, uint256 timestamp) returns uint256 { return discount_ghost[user][timestamp]; } /** * Returns an env instance with [ts] as the block timestamp **/ function envAtTimestamp(uint256 ts) returns env { env e; require(e.block.timestamp == ts); return e; } /** * @title at any point in time, the user's discount rate isn't larger than 100% **/ invariant discountCantExceed100Percent(address user) getUserDiscountRate(user) <= MAX_DISCOUNT() { preserved updateDiscountDistribution(address sender,address recipient,uint256 senderDiscountTokenBalance,uint256 recipientDiscountTokenBalance,uint256 amount) with (env e) { require(indexAtTimestamp(e.block.timestamp) >= ray()); } } /** * @title at any point in time, the user's discount rate isn't larger than DISCOUNT_RATE **/ invariant discountCantExceedDiscountRate(address user) getUserDiscountRate(user) <= discStrategy.DISCOUNT_RATE() { preserved updateDiscountDistribution(address sender,address recipient,uint256 senderDiscountTokenBalance,uint256 recipientDiscountTokenBalance,uint256 amount) with (env e) { require(indexAtTimestamp(e.block.timestamp) >= ray()); } } // mutant 6 // A new discount token is not address zero rule nonzeroNewDiscountToken{ env e; address newDiscountToken; updateDiscountToken(e, newDiscountToken); assert newDiscountToken != 0; } // If a user's index has changed then it is assigned with the current pool index. // Assuming that the Pool calls mint() and burn() with its current index. invariant user_index_up_to_date(env e1, address user1) scaledBalanceOf(e1, user1) != 0 => getUserCurrentIndex(user1) == indexAtTimestamp(e1.block.timestamp) { preserved mint(address user2, address onBehalfOf, uint256 amount, uint256 index) with (env e2) { require index == indexAtTimestamp(e2.block.timestamp); require e1.block.timestamp == e2.block.timestamp; } preserved burn(address from, uint256 amount, uint256 index) with (env e3) { require index == indexAtTimestamp(e3.block.timestamp); require e1.block.timestamp == e3.block.timestamp; } preserved with (env e4) { require e1.block.timestamp == e4.block.timestamp; } } // check user index after mint() rule user_index_after_mint { env e; address user; address onBehalfOf; uint256 amount; uint256 index; uint256 user_index_before = getUserCurrentIndex(onBehalfOf); mint(e, user, onBehalfOf, amount, index); uint256 user_index_after = getUserCurrentIndex(onBehalfOf); assert index > user_index_before => user_index_after > user_index_before; assert user_index_after == index; } // check accumulated interest after mint() rule accumulated_interest_increase_after_mint { env e; address user; address onBehalfOf; uint256 amount; uint256 index; requireInvariant user_index_ge_one_ray(e, onBehalfOf); requireInvariant discountCantExceedDiscountRate(onBehalfOf); uint256 user_index_before = getUserCurrentIndex(onBehalfOf); uint256 balance_before = balanceOf(e, onBehalfOf); uint256 discount_before = getUserDiscountRate(onBehalfOf); uint256 accumulated_interest_before = getUserAccumulatedDebtInterest(onBehalfOf); mint(e, user, onBehalfOf, amount, index); uint256 accumulated_interest_after = getUserAccumulatedDebtInterest(onBehalfOf); assert balance_before > 0 && to_mathint(user_index_before + ray()) < to_mathint(index) => accumulated_interest_after > accumulated_interest_before; } // User index >= 1 ray for every user with positive balance invariant user_index_ge_one_ray(env e1, address user1) scaledBalanceOf(e1, user1) != 0 => ray() <= getUserCurrentIndex(user1) { preserved mint(address user2, address onBehalfOf, uint256 amount, uint256 index) with (env e2) { require index >= ray(); //TODO: verify - the Pool calls mint() with index >= 1 ray } preserved burn(address from, uint256 amount, uint256 index) with (env e3) { require index >= ray(); //TODO: verify - the Pool calls burn() with index >= 1 ray } } /** * Imported rules from VariableDebtToken.spec **/ //pass use rule disallowedFunctionalities; /** * @title proves that a user's discount rate can be updated only by calling rebalanceUserDiscountPercent * This rule fails since updateDiscountDistribution, mint and burn can recalculate and update the user discount rate **/ // rule onlyRebalanceCanUpdateUserDiscountRate(method f) { // address user; // uint256 discRateBefore = getUserDiscountRate(user); // requireInvariant discountCantExceed100Percent(user); // env e; // calldataarg args; // f(e,args); // uint256 discRateAfter = getUserDiscountRate(user); // assert(discRateAfter != discRateBefore => f.selector == sig:rebalanceUserDiscountPercent(address).selector); // } /** * @title proves that the user's balance of debt token (as reported by GhoVariableDebtToken::balanceOf) can't increase by calling any external non-mint function. **/ //pass rule nonMintFunctionCantIncreaseBalance(method f) filtered { f-> f.selector != sig:mint(address, address, uint256, uint256).selector } { address user; uint256 ts1; uint256 ts2; require(ts2 >= ts1); // Forcing the index to be fixed (otherwise the rule times out). For non-fixed index replace `==` with `>=` require((indexAtTimestamp(ts1) >= ray()) && (indexAtTimestamp(ts2) == indexAtTimestamp(ts1))); require(getUserCurrentIndex(user) == indexAtTimestamp(ts1)); requireInvariant discountCantExceed100Percent(user); env e = envAtTimestamp(ts2); uint256 balanceBeforeOp = balanceOf(e, user); calldataarg args; f(e,args); mathint balanceAfterOp = balanceOf(e, user); mathint allowedDiff = indexAtTimestamp(ts2) / ray(); // assert(balanceAfterOp != balanceBeforeOp + allowedDiff + 1); assert(balanceAfterOp <= balanceBeforeOp + allowedDiff); } /** * @title proves that a call to a non-mint operation won't increase the user's balance of the actual debt tokens (i.e. it's scaled balance) **/ // pass rule nonMintFunctionCantIncreaseScaledBalance(method f) filtered { f-> f.selector != sig:mint(address, address, uint256, uint256).selector } { address user; uint256 ts1; uint256 ts2; require(ts2 >= ts1); require((indexAtTimestamp(ts1) >= ray()) && (indexAtTimestamp(ts2) >= indexAtTimestamp(ts1))); require(getUserCurrentIndex(user) == indexAtTimestamp(ts1)); requireInvariant discountCantExceed100Percent(user); uint256 balanceBeforeOp = scaledBalanceOf(user); env e = envAtTimestamp(ts2); calldataarg args; f(e,args); uint256 balanceAfterOp = scaledBalanceOf(user); assert(balanceAfterOp <= balanceBeforeOp); } /** * @title proves that debt tokens aren't transferable **/ // pass rule debtTokenIsNotTransferable(method f) { address user1; address user2; require(user1 != user2); uint256 scaledBalanceBefore1 = scaledBalanceOf(user1); uint256 scaledBalanceBefore2 = scaledBalanceOf(user2); env e; calldataarg args; f(e,args); uint256 scaledBalanceAfter1 = scaledBalanceOf(user1); uint256 scaledBalanceAfter2 = scaledBalanceOf(user2); assert( scaledBalanceBefore1 + scaledBalanceBefore2 == scaledBalanceAfter1 + scaledBalanceAfter2 => (scaledBalanceBefore1 == scaledBalanceAfter1 && scaledBalanceBefore2 == scaledBalanceAfter2)); } /** * @title proves that only burn/mint/rebalanceUserDiscountPercent/updateDiscountDistribution can modify user's scaled balance **/ // pass rule onlyCertainFunctionsCanModifyScaledBalance(method f) { address user; uint256 ts1; uint256 ts2; require(ts2 >= ts1); require((indexAtTimestamp(ts1) >= ray()) && (indexAtTimestamp(ts2) >= indexAtTimestamp(ts1))); require(getUserCurrentIndex(user) == indexAtTimestamp(ts1)); requireInvariant discountCantExceed100Percent(user); uint256 balanceBeforeOp = scaledBalanceOf(user); env e = envAtTimestamp(ts2); calldataarg args; f(e,args); uint256 balanceAfterOp = scaledBalanceOf(user); assert(balanceAfterOp != balanceBeforeOp => ( (f.selector == sig:mint(address ,address ,uint256 ,uint256).selector) || (f.selector == sig:burn(address ,uint256 ,uint256).selector) || (f.selector == sig:updateDiscountDistribution(address ,address ,uint256 ,uint256 ,uint256).selector) || (f.selector == sig:rebalanceUserDiscountPercent(address).selector))); } /** * @title proves that only a call to decreaseBalanceFromInterest will decrease the user's accumulated interest listing. **/ // pass rule userAccumulatedDebtInterestWontDecrease(method f) { address user; uint256 ts1; uint256 ts2; require(ts2 >= ts1); require((indexAtTimestamp(ts1) >= ray()) && (indexAtTimestamp(ts2) >= indexAtTimestamp(ts1))); require(getUserCurrentIndex(user) == indexAtTimestamp(ts1)); requireInvariant discountCantExceed100Percent(user); uint256 initAccumulatedInterest = getUserAccumulatedDebtInterest(user); env e2 = envAtTimestamp(ts2); calldataarg args; f(e2,args); uint256 finAccumulatedInterest = getUserAccumulatedDebtInterest(user); assert(initAccumulatedInterest > finAccumulatedInterest => f.selector == sig:decreaseBalanceFromInterest(address, uint256).selector); } /** * @title proves that a user can't nullify its debt without calling burn **/ // pass rule userCantNullifyItsDebt(method f) { address user; env e; env e2; require(getUserCurrentIndex(user) == indexAtTimestamp(e.block.timestamp)); requireInvariant discountCantExceed100Percent(user); uint256 balanceBeforeOp = balanceOf(e, user); calldataarg args; require e2.block.timestamp == e.block.timestamp; f(e2,args); uint256 balanceAfterOp = balanceOf(e, user); assert((balanceBeforeOp > 0 && balanceAfterOp == 0) => (f.selector == sig:burn(address, uint256, uint256).selector)); } /*************************************************************** * Integrity of Mint ***************************************************************/ /** * @title proves that after calling mint, the user's discount rate is up to date **/ rule integrityOfMint_updateDiscountRate() { address user1; address user2; env e; uint256 amount; uint256 index = indexAtTimestamp(e.block.timestamp); mint(e, user1, user2, amount, index); uint256 debtBalance = balanceOf(e, user2); uint256 discountBalance = getBalanceOfDiscountToken(e, user2); uint256 discountRate = getUserDiscountRate(user2); assert(discStrategy.calculateDiscountRate(debtBalance, discountBalance) == discountRate); } /** * @title proves the after calling mint, the user's state is updated with the recent index value **/ rule integrityOfMint_updateIndex() { address user1; address user2; env e; uint256 amount; uint256 index; mint(e, user1, user2, amount, index); assert(getUserCurrentIndex(user2) == index); } /** * @title proves that on a fixed index calling mint(user, amount) will increase the user's scaled balance by amount. **/ // pass rule integrityOfMint_updateScaledBalance_fixedIndex() { address user; env e; uint256 balanceBefore = balanceOf(e, user); uint256 scaledBalanceBefore = scaledBalanceOf(user); address caller; uint256 amount; uint256 index = indexAtTimestamp(e.block.timestamp); require(getUserCurrentIndex(user) == index); mint(e, caller, user, amount, index); uint256 balanceAfter = balanceOf(e, user); mathint scaledBalanceAfter = scaledBalanceOf(user); mathint scaledAmount = rayDivCVL(amount, index); assert(scaledBalanceAfter == scaledBalanceBefore + scaledAmount); } /** * @title proves that mint can't effect other user's scaled balance **/ // pass rule integrityOfMint_userIsolation() { address otherUser; uint256 scaledBalanceBefore = scaledBalanceOf(otherUser); env e; uint256 amount; uint256 index; address targetUser; address caller; mint(e, caller, targetUser, amount, index); uint256 scaledBalanceAfter = scaledBalanceOf(otherUser); assert(scaledBalanceAfter != scaledBalanceBefore => otherUser == targetUser); } /** * @title proves that when calling mint, the user's balance (as reported by GhoVariableDebtToken::balanceOf) will increase if the call is made on bahalf of the user. **/ rule onlyMintForUserCanIncreaseUsersBalance() { address user1; env e; require(getUserCurrentIndex(user1) == indexAtTimestamp(e.block.timestamp)); uint256 finBalanceBeforeMint = balanceOf(e, user1); uint256 amount; mint(e,user1, user1, amount, indexAtTimestamp(e.block.timestamp)); uint256 finBalanceAfterMint = balanceOf(e, user1); assert(finBalanceAfterMint != finBalanceBeforeMint); } /** * @title proves that a user can't decrease the ovelall interest of his position by taking more loans, compared to another user with the same initial position. * This rule times out. **/ // rule integrityOfMint_cantDecreaseInterestWithMint() { // address user1; // uint256 ts1; // env e1 = envAtTimestamp(ts1); // uint256 ts2; // require(ts2 >= ts1); // env e2 = envAtTimestamp(ts2); // uint256 ts3; // require(ts3 >= ts2); // env e3 = envAtTimestamp(ts3); // // Forcing the index to be fixed (otherwise the rule times out). For non-fixed index replace `==` with `>=` // require((indexAtTimestamp(ts1) >= ray()) && // (indexAtTimestamp(ts2) >= indexAtTimestamp(ts1)) && // (indexAtTimestamp(ts3) >= indexAtTimestamp(ts2))); // require(getUserCurrentIndex(user1) == indexAtTimestamp(ts1)); // uint256 amount; // storage initialStorage = lastStorage; // mint(e2, user1, user1, amount, indexAtTimestamp(ts2)); // rebalanceUserDiscountPercent(e3, user1); // uint256 balanceFromInterestAfterMint = getBalanceFromInterest(user1); // rebalanceUserDiscountPercent(e3, user1) at initialStorage; // uint256 balanceFromInterestWithoutMint = getBalanceFromInterest(user1); // assert(balanceFromInterestAfterMint >= balanceFromInterestWithoutMint); // } //pass use rule integrityMint_atoken; /*************************************************************** * Integrity of Burn ***************************************************************/ /** * @title proves that after calling burn, the user's discount rate is up to date **/ rule integrityOfBurn_updateDiscountRate() { address user; env e; uint256 amount; uint256 index = indexAtTimestamp(e.block.timestamp); burn(e, user, amount, index); uint256 debtBalance = balanceOf(e, user); uint256 discountBalance = getBalanceOfDiscountToken(e, user); uint256 discountRate = getUserDiscountRate(user); assert(discStrategy.calculateDiscountRate(debtBalance, discountBalance) == discountRate); } /** * @title proves the after calling burn, the user's state is updated with the recent index value **/ rule integrityOfBurn_updateIndex() { address user; env e; uint256 amount; uint256 index; burn(e, user, amount, index); assert(getUserCurrentIndex(user) == index); } /** * @title proves that calling burn with 0 amount doesn't change the user's balance **/ use rule burnZeroDoesntChangeBalance; /** * @title proves a concrete case of repaying the full debt that ends with a zero balance **/ rule integrityOfBurn_fullRepay_concrete() { env e; address user; uint256 currentDebt = balanceOf(e, user); uint256 index = indexAtTimestamp(e.block.timestamp); require(getUserCurrentIndex(user) == ray()); require(to_mathint(index) == 2*ray()); require(to_mathint(scaledBalanceOf(user)) == 4*ray()); burn(e, user, currentDebt, index); uint256 scaled = scaledBalanceOf(user); assert(scaled == 0); } /** * @title proves that burn can't effect other user's scaled balance **/ // pass rule integrityOfBurn_userIsolation() { address otherUser; uint256 scaledBalanceBefore = scaledBalanceOf(otherUser); env e; uint256 amount; uint256 index; address targetUser; burn(e,targetUser, amount, index); uint256 scaledBalanceAfter = scaledBalanceOf(otherUser); assert(scaledBalanceAfter != scaledBalanceBefore => otherUser == targetUser); } /*************************************************************** * Integrity of updateDiscountDistribution ***************************************************************/ // /** // * @title proves that the discount rate is calculated correctly when calling updateDiscountDistribution // **/ // rule integrityOfUpdateDiscountDistribution_discountRate() { // address sender; // address recipient; // uint256 senderDiscountTokenBalanceBefore; // uint256 recipientDiscountTokenBalanceBefore; // uint256 amount; // uint256 senderDiscountTokenBalanceAfter = require_uint256(senderDiscountTokenBalanceBefore - amount); // uint256 recipientDiscountTokenBalanceAfter = require_uint256(recipientDiscountTokenBalanceBefore + amount); // env e0; // env e; // require(e.block.timestamp > e0.block.timestamp); // require(indexAtTimestamp(e.block.timestamp) >= indexAtTimestamp(e0.block.timestamp)); // require(indexAtTimestamp(e0.block.timestamp) == ray()); // reduces execution time // require(getUserCurrentIndex(sender) == indexAtTimestamp(e0.block.timestamp)); // require(getUserCurrentIndex(recipient) == indexAtTimestamp(e0.block.timestamp)); // require(getBalanceOfDiscountToken(e0, sender) == senderDiscountTokenBalanceBefore); // require(getBalanceOfDiscountToken(e0, recipient) == recipientDiscountTokenBalanceBefore); // require(discStrategy.calculateDiscountRate(balanceOf(e0, sender), senderDiscountTokenBalanceBefore) == getUserDiscountRate(sender)); // require(discStrategy.calculateDiscountRate(balanceOf(e0, recipient), recipientDiscountTokenBalanceBefore) == getUserDiscountRate(recipient)); // require(getBalanceOfDiscountToken(e, sender) == senderDiscountTokenBalanceAfter); // require(getBalanceOfDiscountToken(e, recipient) == recipientDiscountTokenBalanceAfter); // updateDiscountDistribution(e, sender, recipient, senderDiscountTokenBalanceBefore, recipientDiscountTokenBalanceBefore, amount); // uint256 senderBalance = balanceOf(e, sender); // uint256 recipientBalance = balanceOf(e, recipient); // assert(discStrategy.calculateDiscountRate(senderBalance, senderDiscountTokenBalanceAfter) == getUserDiscountRate(sender)); // assert(discStrategy.calculateDiscountRate(recipientBalance, recipientDiscountTokenBalanceAfter) == getUserDiscountRate(recipient)); // } rule sendersDiscountPercentCannotIncrease(){ env e1; address sender; address recipient; uint256 amount; uint256 _senderStkBalance = getBalanceOfDiscountToken(e1, sender); uint256 _recipientStkBalance = getBalanceOfDiscountToken(e1, recipient); uint256 indE1 = indexAtTimestamp(e1.block.timestamp); // require(indE1 >= ray()); // this is already enforced in the funciton's body require getUserCurrentIndex(sender) == indE1; uint256 _sender_debt = balanceOf(e1, sender); uint256 discount_sender = discStrategy.calculateDiscountRate(_sender_debt, _senderStkBalance); require(discount_sender == getDiscountPercent(e1, sender)); require discount_sender != 0; // this can be violated due to discontinuity of calculateDiscountRate env e2; require e1.block.timestamp <= e2.block.timestamp; uint256 indE2 = indexAtTimestamp(e2.block.timestamp); require(indE2 >= indE1); require _senderStkBalance == getBalanceOfDiscountToken(e2, sender); require _recipientStkBalance == getBalanceOfDiscountToken(e2, recipient); updateDiscountDistribution(e2, sender, recipient, _senderStkBalance, _recipientStkBalance, amount); uint256 discountPercent_ = getDiscountPercent(e2, sender); assert (discountPercent_ <= discount_sender); } /** * @title proves the after calling updateDiscountDistribution, the user's state is updated with the recent index value **/ rule integrityOfUpdateDiscountDistribution_updateIndex() { address sender; address recipient; uint256 senderDiscountTokenBalance; uint256 recipientDiscountTokenBalance; env e; uint256 amount; uint256 _senderInd = getUserCurrentIndex(sender); uint256 _recipientInd = getUserCurrentIndex(recipient); uint256 index = indexAtTimestamp(e.block.timestamp); updateDiscountDistribution(e, sender, recipient, senderDiscountTokenBalance, recipientDiscountTokenBalance, amount); if (sender != recipient){ assert(scaledBalanceOf(sender) > 0 => getUserCurrentIndex(sender) == index); assert(scaledBalanceOf(recipient) > 0 => getUserCurrentIndex(recipient) == index); } else{ assert(getUserCurrentIndex(sender) == _senderInd); assert(getUserCurrentIndex(recipient) == _recipientInd); // this is redundant, this is here for future changes in the code/rule } } /** * @title proves that updateDiscountDistribution can't effect other user's scaled balance **/ // pass rule integrityOfUpdateDiscountDistribution_userIsolation() { address otherUser; uint256 scaledBalanceBefore = scaledBalanceOf(otherUser); env e; uint256 amount; uint256 senderDiscountTokenBalance; uint256 recipientDiscountTokenBalance; address sender; address recipient; updateDiscountDistribution(e, sender, recipient, senderDiscountTokenBalance, recipientDiscountTokenBalance, amount); uint256 scaledBalanceAfter = scaledBalanceOf(otherUser); assert(scaledBalanceAfter != scaledBalanceBefore => (otherUser == sender || otherUser == recipient)); } /*************************************************************** * Integrity of rebalanceUserDiscountPercent ***************************************************************/ /** * @title proves that after calling rebalanceUserDiscountPercent, the user's discount rate is up to date **/ rule integrityOfRebalanceUserDiscountPercent_updateDiscountRate() { address user; env e; rebalanceUserDiscountPercent(e, user); assert(discStrategy.calculateDiscountRate(balanceOf(e, user), getBalanceOfDiscountToken(e, user)) == getUserDiscountRate(user)); } /** * @title proves the after calling rebalanceUserDiscountPercent, the user's state is updated with the recent index value **/ rule integrityOfRebalanceUserDiscountPercent_updateIndex() { address user; env e; rebalanceUserDiscountPercent(e, user); uint256 index = indexAtTimestamp(e.block.timestamp); assert(getUserCurrentIndex(user) == index); } /** * @title proves that rebalanceUserDiscountPercent can't effect other user's scaled balance **/ // pass rule integrityOfRebalanceUserDiscountPercent_userIsolation() { address otherUser; uint256 scaledBalanceBefore = scaledBalanceOf(otherUser); env e; address targetUser; rebalanceUserDiscountPercent(e, targetUser); uint256 scaledBalanceAfter = scaledBalanceOf(otherUser); assert(scaledBalanceAfter != scaledBalanceBefore => otherUser == targetUser); } /*************************************************************** * Integrity of balanceOf ***************************************************************/ /** * @title proves that a user with 100% discounts has a fixed balance over time **/ rule integrityOfBalanceOf_fullDiscount() { address user; uint256 fullDiscountRate = 10000; //100% require(getUserDiscountRate(user) == fullDiscountRate); env e1; env e2; uint256 index1 = indexAtTimestamp(e1.block.timestamp); uint256 index2 = indexAtTimestamp(e2.block.timestamp); assert(balanceOf(e1, user) == balanceOf(e2, user)); } /** * @title proves that a user's balance, with no discount, is equal to rayMul(scaledBalance, current index) **/ rule integrityOfBalanceOf_noDiscount() { address user; require(getUserDiscountRate(user) == 0); env e; uint256 scaledBalance = scaledBalanceOf(user); uint256 currentIndex = indexAtTimestamp(e.block.timestamp); mathint expectedBalance = rayMulCVL(scaledBalance, currentIndex); assert(to_mathint(balanceOf(e, user)) == expectedBalance); } /** * @title proves the a user with zero scaled balance has a zero balance **/ rule integrityOfBalanceOf_zeroScaledBalance() { address user; env e; uint256 scaledBalance = scaledBalanceOf(user); require(scaledBalance == 0); assert(balanceOf(e, user) == 0); } /** * @title burning amount of current debt nullifies the debt position **/ rule burnAllDebtReturnsZeroDebt(address user) { env e; uint256 _variableDebt = balanceOf(e, user); burn(e, user, _variableDebt, indexAtTimestamp(e.block.timestamp)); uint256 variableDebt_ = balanceOf(e, user); assert(variableDebt_ == 0); } /** * @title discount strategy setter is setting the corresponding storage slot to the passed value **/ rule integrityOfUpdateDiscountRateStrategy(address newDiscountRateStrategy) { env e; updateDiscountRateStrategy(e, newDiscountRateStrategy ); assert(getDiscountRateStrategy(e) == newDiscountRateStrategy); } ================================================ FILE: certora/gho/specs/ghoVariableDebtTokenInternal.spec ================================================ import "ghoVariableDebtToken.spec"; methods{ } // check a scenario that function _accrueDebtOnAction() returns non zero balance increase rule positive_balanceIncrease { env e; address user; uint256 previousScaledBalance; uint256 discountPercent; uint256 index; uint256 balanceIncrease; uint256 discountScaled; uint256 user_index_before = getUserCurrentIndex(user); uint256 accumulated_interest_before = getUserAccumulatedDebtInterest(user); balanceIncrease, discountScaled = accrueDebtOnAction(e, user,previousScaledBalance,discountPercent,index); uint256 accumulated_interest_after = getUserAccumulatedDebtInterest(user); uint256 user_index_after = getUserCurrentIndex(user); assert ray() <= user_index_before && to_mathint(user_index_before + ray()) < to_mathint(index) // user index increase by more than 1 ray && 0 < previousScaledBalance && discountPercent < discStrategy.DISCOUNT_RATE() // discount rate is less than 30% //(if user index increases by 1 ray discount percent could be as high as 50%) => balanceIncrease > 0; assert balanceIncrease > 0 => accumulated_interest_after > accumulated_interest_before; assert user_index_after == index; } ================================================ FILE: certora/gho/specs/ghoVariableDebtToken_summarized.spec ================================================ import "ghoVariableDebtToken.spec"; methods{ function GhoVariableDebtToken._accrueDebtOnAction(address user, uint256, uint256, uint256) internal returns (uint256, uint256) => flipAccrueCalled(user); function GhoVariableDebtToken._refreshDiscountPercent(address user, uint256, uint256, uint256) internal => flipRefreshCalled(user); } ghost mapping(address => mathint) accrue_called_counter { init_state axiom forall address user. accrue_called_counter[user] == 0; } ghost mapping(address => mathint) refresh_called_counter { init_state axiom forall address user. refresh_called_counter[user] == 0; } function flipAccrueCalled(address user) returns (uint256, uint256) { accrue_called_counter[user] = accrue_called_counter[user] + 1; return (0, 0); } function flipRefreshCalled(address user) { // before refreshing a user, accrue of the user should've been called exactly once // of course calling accrue twice is not a crucial mistake, but accruing the same user twice in a row before refreshing doesn't make sense, so a violation should be triggered assert(refresh_called_counter[user] + 1 == accrue_called_counter[user]); refresh_called_counter[user] = refresh_called_counter[user] + 1; } invariant allUsersRefreshAndAccrueCounterEqual() forall address user. accrue_called_counter[user] == refresh_called_counter[user]; // accrue is always called before refresh rule accrueAlwaysCalleldBeforeRefresh(env e, method f) { address user1; requireInvariant allUsersRefreshAndAccrueCounterEqual(); // require (forall address user. (accrue_called_counter[user] == refresh_called_counter[user])); calldataarg args; // see comment in flipRefreshCalled f(e, args); assert refresh_called_counter[user1] == accrue_called_counter[user1], "Remember, with great power comes great responsibility."; } // accrue is always called before refresh example // should pass only on updateDiscountDistribution rule accrueAlwaysCalledBeforeRefresh_witness(env e, method f) { address user1; mathint counter = accrue_called_counter[user1]; require accrue_called_counter[user1] == refresh_called_counter[user1]; calldataarg args; f(e, args); satisfy(refresh_called_counter[user1] == counter + 2); } ================================================ FILE: certora/gho/specs/set-natspec.json ================================================ [ { "content": "/**\n * @title get Set array length\n * @dev user should define getLen() in Solidity harness file.\n */\nmethods{\n getLen() returns (uint256) envfree\n}", "type": "methods", "title": "get Set array length", "dev": "user should define getLen() in Solidity harness file." }, { "content": "/**\n* @title max uint256\n* @return 2^256-1\n*/\ndefinition MAX_UINT256() returns uint256 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;", "type": "definition", "id": "MAX_UINT256", "title": "max uint256", "return": { "type": "uint256" } }, { "content": "/**\n* @title max address value + 1\n* @returns 2^160\n*/\ndefinition TWO_TO_160() returns uint256 = 0x10000000000000000000000000000000000000000;", "type": "definition", "id": "TWO_TO_160", "title": "max address value + 1", "return": { "type": "uint256" } }, { "content": "/**\n* @title Set map entries point to valid array entries\n* @notice an essential condition of the set, should hold for evert Set implementation \n* @return true if all map entries points to valid indexes of the array.\n*/\ndefinition MAP_POINTS_INSIDE_ARRAY() returns bool = forall bytes32 a. mirrorMap(a) <= mirrorArrayLen();", "type": "definition", "id": "MAP_POINTS_INSIDE_ARRAY", "title": "Set map entries point to valid array entries", "return": { "type": "bool", "comment": "true if all map entries points to valid indexes of the array." }, "notice": "an essential condition of the set, should hold for evert Set implementation" }, { "content": "/**\n* @title Set map is the inverse function of set array. \n* @notice an essential condition of the set, should hold for evert Set implementation \n* @notice this condition depends on the other set conditions, but the other conditions do not depend on this condition.\n* If this condition is omitted the rest of the conditions still hold, but the other conditions are required to prove this condition.\n* @return true if for every valid index of the array it holds that map(array(index)) == index + 1.\n*/\ndefinition MAP_IS_INVERSE_OF_ARRAY() returns bool = forall uint256 i. (i < mirrorArrayLen()) => (mirrorMap(mirrorArray(i))) == to_uint256(i + 1);", "type": "definition", "id": "MAP_IS_INVERSE_OF_ARRAY", "title": "Set map is the inverse function of set array.", "return": { "type": "bool" }, "notice": "this condition depends on the other set conditions, but the other conditions do not depend on this condition.\nIf this condition is omitted the rest of the conditions still hold, but the other conditions are required to prove this condition." }, { "content": "/**\n* @title Set array is the inverse function of set map\n* @notice an essential condition of the set, should hold for evert Set implementation \n* @return true if for every non-zero bytes32 value stored in in the set map it holds that array(map(value) - 1) == value\n*/\ndefinition ARRAY_IS_INVERSE_OF_MAP() returns bool = forall bytes32 a. (mirrorMap(a) != 0) => (mirrorArray(to_uint256(mirrorMap(a)-1)) == a);", "type": "definition", "id": "ARRAY_IS_INVERSE_OF_MAP", "title": "Set array is the inverse function of set map", "return": { "type": "bool", "comment": "true if for every non-zero bytes32 value stored in in the set map it holds that array(map(value) - 1) == value" }, "notice": "an essential condition of the set, should hold for evert Set implementation" }, { "content": "/**\n* @title load array length\n* @notice a dummy condition that forces load of array length, using it forces initialization of mirrorArrayLen()\n* @return always true\n*/\ndefinition CVL_LOAD_ARRAY_LENGTH() returns bool = (getLen() == getLen());", "type": "definition", "id": "CVL_LOAD_ARRAY_LENGTH", "title": "load array length", "return": { "type": "bool", "comment": "always true" }, "notice": "a dummy condition that forces load of array length, using it forces initialization of mirrorArrayLen()" }, { "content": "/**\n* @title Set-general condition, encapsulating all conditions of Set \n* @notice this condition recaps the general characteristics of Set. It should hold for all set implementations i.e. AddressSet, UintSet, Bytes32Set\n* @return conjunction of the Set three essential properties.\n*/\ndefinition SET_INVARIANT() returns bool = MAP_POINTS_INSIDE_ARRAY() && MAP_IS_INVERSE_OF_ARRAY() && ARRAY_IS_INVERSE_OF_MAP() && CVL_LOAD_ARRAY_LENGTH();", "type": "definition", "id": "SET_INVARIANT", "title": "Set-general condition, encapsulating all conditions of Set", "return": { "type": "bool" }, "notice": "this condition recaps the general characteristics of Set. It should hold for all set implementations i.e. AddressSet, UintSet, Bytes32Set" }, { "content": "/**\n * @title Size of stored value does not exceed the size of an address type.\n * @notice must be used for AddressSet, must not be used for Bytes32Set, UintSet\n * @return true if all array entries are less than 160 bits.\n **/\ndefinition VALUE_IN_BOUNDS_OF_TYPE_ADDRESS() returns bool = (forall uint256 i. to_uint256(mirrorArray(i)) < TWO_TO_160());", "type": "definition", "id": "VALUE_IN_BOUNDS_OF_TYPE_ADDRESS", "title": "Size of stored value does not exceed the size of an address type.", "return": { "type": "bool", "comment": "true if all array entries are less than 160 bits." }, "notice": "must be used for AddressSet, must not be used for Bytes32Set, UintSet" }, { "content": "/**\n * @title A complete invariant condition for AddressSet\n * @notice invariant addressSetInvariant proves that this condition holds\n * @return conjunction of the Set-general and AddressSet-specific conditions\n **/\ndefinition ADDRESS_SET_INVARIANT() returns bool = SET_INVARIANT() && VALUE_IN_BOUNDS_OF_TYPE_ADDRESS();", "type": "definition", "id": "ADDRESS_SET_INVARIANT", "title": "A complete invariant condition for AddressSet", "return": { "type": "bool", "comment": "conjunction of the Set-general and AddressSet-specific conditions" }, "notice": "invariant addressSetInvariant proves that this condition holds" }, { "content": "/**\n * @title A complete invariant condition for UintSet, Bytes32Set\n * @notice for UintSet and Bytes2St no type-specific condition is required because the type size is the same as the native type (bytes32) size\n * @return the Set-general condition\n **/\ndefinition UINT_SET_INVARIANT() returns bool = SET_INVARIANT();", "type": "definition", "id": "UINT_SET_INVARIANT", "title": "A complete invariant condition for UintSet, Bytes32Set", "return": { "type": "bool", "comment": "the Set-general condition" }, "notice": "for UintSet and Bytes2St no type-specific condition is required because the type size is the same as the native type (bytes32) size" }, { "content": "/**\n * @title Out of bound array entries are zero\n * @notice A non-essential condition. This condition can be proven as an invariant, but it is not necessary for proving the Set correctness.\n * @return true if all entries beyond array length are zero\n **/\ndefinition ARRAY_OUT_OF_BOUND_ZERO() returns bool = forall uint256 i. (i >= mirrorArrayLen()) => (mirrorArray(i) == 0);", "type": "definition", "id": "ARRAY_OUT_OF_BOUND_ZERO", "title": "Out of bound array entries are zero", "return": { "type": "bool", "comment": "true if all entries beyond array length are zero" }, "notice": "A non-essential condition. This condition can be proven as an invariant, but it is not necessary for proving the Set correctness." }, { "content": "/**\n * @title ghost mirror map, mimics Set map\n **/\nghost mirrorMap(bytes32) returns uint256{\n init_state axiom forall bytes32 a. mirrorMap(a) == 0;\n}", "type": "ghost", "id": "mirrorMap", "title": "ghost mirror map, mimics Set map", "return": { "type": "uint256" } }, { "content": "/**\n * @title ghost mirror array, mimics Set array\n **/\nghost mirrorArray(uint256) returns bytes32{\n init_state axiom forall uint256 i. mirrorArray(i) == 0;\n}", "type": "ghost", "id": "mirrorArray", "title": "ghost mirror array, mimics Set array", "return": { "type": "bytes32" } }, { "content": "/**\n * @title ghost mirror array length, mimics Set array length\n * @notice ghost includes an assumption about the array length. \n * If the assumption were not written in the ghost function it should be written in every rule and invariant.\n * The assumption holds: breaking the assumptions would violate the invariant condition 'map(array(index)) == index + 1'. Set map uses 0 as a sentinel value, so the array cannot contain MAX_INT different values. \n * The assumption is necessary: if a value is added when length==MAX_INT then length overflows and becomes zero.\n **/\nghost mirrorArrayLen() returns uint256{\n init_state axiom mirrorArrayLen() == 0;\n axiom mirrorArrayLen() != MAX_UINT256(); \n}", "type": "ghost", "id": "mirrorArrayLen", "title": "ghost mirror array length, mimics Set array length", "return": { "type": "uint256" }, "notice": "ghost includes an assumption about the array length.\nIf the assumption were not written in the ghost function it should be written in every rule and invariant.\nThe assumption holds: breaking the assumptions would violate the invariant condition 'map(array(index)) == index + 1'. Set map uses 0 as a sentinel value, so the array cannot contain MAX_INT different values.\nThe assumption is necessary: if a value is added when length==MAX_INT then length overflows and becomes zero." }, { "content": "/**\n * @title main Set general invariant\n **/\ninvariant setInvariant()\n SET_INVARIANT()", "type": "invariant", "id": "setInvariant", "title": "main Set general invariant" }, { "content": "/**\n * @title main AddressSet invariant\n * @dev user of the spec should add 'requireInvariant addressSetInvariant();' to every rule and invariant that refer to a contract that instantiates AddressSet \n **/\ninvariant addressSetInvariant()\n ADDRESS_SET_INVARIANT()", "type": "invariant", "id": "addressSetInvariant", "title": "main AddressSet invariant", "dev": "user of the spec should add 'requireInvariant addressSetInvariant();' to every rule and invariant that refer to a contract that instantiates AddressSet" }, { "content": "/**\n * @title addAddress() successfully adds an address\n **/\nrule api_add_succeeded()\n{\n env e;\n address a;\n requireInvariant addressSetInvariant();\n require !contains(e, a);\n assert addAddress(e, a);\n assert contains(e, a);\n}", "type": "rule", "id": "api_add_succeeded", "title": "addAddress() successfully adds an address" }, { "content": "/**\n * @title addAddress() fails to add an address if it already exists \n * @notice check set membership using contains()\n **/\nrule api_add_failed_contains()\n{\n env e;\n address a;\n requireInvariant addressSetInvariant();\n require contain(e, a);\n assert !addAddress(e, a);\n}", "type": "rule", "id": "api_add_failed_contains", "title": "addAddress() fails to add an address if it already exists", "notice": "check set membership using contains()" }, { "content": "/**\n * @title addAddress() fails to add an address if it already exists \n * @notice check set membership using atIndex()\n **/\nrule api_add_failed_at()\n{\n env e;\n address a;\n uint256 index;\n requireInvariant addressSetInvariant();\n require atIndex(e, index) == a;\n assert !addAddress(e, a);\n}", "type": "rule", "id": "api_add_failed_at", "title": "addAddress() fails to add an address if it already exists", "notice": "check set membership using atIndex()" }, { "content": "/**\n * @title contains() succeed after addAddress succeeded \n **/\nrule api_address_contained_affter_add()\n{\n env e;\n address a;\n requireInvariant addressSetInvariant();\n addAddress(e, a);\n assert contains(e, a);\n}", "type": "rule", "id": "api_address_contained_affter_add", "title": "contains() succeed after addAddress succeeded" }, { "content": "/**\n * @title _removeAddress() succeeds to remove an address if it existed \n * @notice check set membership using contains()\n **/\nrule api_remove_succeeded_contains()\n{\n env e;\n address a;\n requireInvariant addressSetInvariant();\n require contains(e, a);\n assert _removeAddress(e, a);\n}", "type": "rule", "id": "api_remove_succeeded_contains", "title": "_removeAddress() succeeds to remove an address if it existed", "notice": "check set membership using contains()" }, { "content": "/**\n * @title _removeAddress() fails to remove address if it didn't exist \n **/\nrule api_remove_failed()\n{\n env e;\n address a;\n requireInvariant addressSetInvariant();\n require !contains(e, a);\n assert !_removeAddress(e, a);\n}", "type": "rule", "id": "api_remove_failed", "title": "_removeAddress() fails to remove address if it didn't exist" }, { "content": "/**\n * @title _removeAddress() succeeds to remove an address if it existed \n * @notice check set membership using atIndex()\n **/\nrule api_remove_succeeded_at()\n{\n env e;\n address a;\n uint256 index;\n requireInvariant addressSetInvariant();\n require atIndex(e, index) == a;\n assert _removeAddress(e, a);\n}", "type": "rule", "id": "api_remove_succeeded_at", "title": "_removeAddress() succeeds to remove an address if it existed", "notice": "check set membership using atIndex()" }, { "content": "/**\n * @title contains() failed after an address was removed\n **/\nrule api_not_contains_affter_remove()\n{\n env e;\n address a;\n requireInvariant addressSetInvariant();\n _removeAddress(e, a);\n assert !contains(e, a);\n}", "type": "rule", "id": "api_not_contains_affter_remove", "title": "contains() failed after an address was removed" }, { "content": "/**\n * @title contains() succeeds if atIndex() succeeded\n **/\nrule cover_at_contains()\n{\n env e;\n address a = 0;\n requireInvariant addressSetInvariant();\n uint256 index;\n require atIndex(e, index) == a;\n assert contains(e, a);\n}", "type": "rule", "id": "cover_at_contains", "title": "contains() succeeds if atIndex() succeeded" }, { "content": "/**\n * @title cover properties, checking various array lengths\n * @notice The assertion should fail - it's a cover property written as an assertion. For large length, beyond loop_iter the assertion should pass.\n **/\n\nrule cover_len0(){requireInvariant addressSetInvariant();assert mirrorArrayLen() != 0;}", "type": "rule", "id": "cover_len0", "title": "cover properties, checking various array lengths", "notice": "The assertion should fail - it's a cover property written as an assertion. For large length, beyond loop_iter the assertion should pass." }, { "content": "rule cover_len1(){requireInvariant addressSetInvariant();assert mirrorArrayLen() != 1;}", "type": "rule", "id": "cover_len1", "title": "Cover len1" }, { "content": "rule cover_len2(){requireInvariant addressSetInvariant();assert mirrorArrayLen() != 2;}", "type": "rule", "id": "cover_len2", "title": "Cover len2" }, { "content": "rule cover_len3(){requireInvariant addressSetInvariant();assert mirrorArrayLen() != 3;}", "type": "rule", "id": "cover_len3", "title": "Cover len3" }, { "content": "rule cover_len4(){requireInvariant addressSetInvariant();assert mirrorArrayLen() != 4;}", "type": "rule", "id": "cover_len4", "title": "Cover len4" }, { "content": "rule cover_len5(){requireInvariant addressSetInvariant();assert mirrorArrayLen() != 5;}", "type": "rule", "id": "cover_len5", "title": "Cover len5" }, { "content": "rule cover_len6(){requireInvariant addressSetInvariant();assert mirrorArrayLen() != 6;}", "type": "rule", "id": "cover_len6", "title": "Cover len6" }, { "content": "rule cover_len7(){requireInvariant addressSetInvariant();assert mirrorArrayLen() != 7;}", "type": "rule", "id": "cover_len7", "title": "Cover len7" }, { "content": "rule cover_len8(){requireInvariant addressSetInvariant(); assert mirrorArrayLen() != 8;}", "type": "rule", "id": "cover_len8", "title": "Cover len8" }, { "content": "rule cover_len16(){requireInvariant addressSetInvariant(); assert mirrorArrayLen() != 16;}", "type": "rule", "id": "cover_len16", "title": "Cover len16" }, { "content": "rule cover_len32(){requireInvariant addressSetInvariant(); assert mirrorArrayLen() != 32;}", "type": "rule", "id": "cover_len32", "title": "Cover len32" }, { "content": "rule cover_len64(){requireInvariant addressSetInvariant(); assert mirrorArrayLen() != 64;}", "type": "rule", "id": "cover_len64", "title": "Cover len64" }, { "content": "rule cover_len128(){requireInvariant addressSetInvariant(); assert mirrorArrayLen() != 128;}", "type": "rule", "id": "cover_len128", "title": "Cover len128" }, { "content": "rule cover_len256(){requireInvariant addressSetInvariant(); assert mirrorArrayLen() != 256;}", "type": "rule", "id": "cover_len256", "title": "Cover len256" }, { "content": "rule cover_len512(){requireInvariant addressSetInvariant(); assert mirrorArrayLen() != 512;}", "type": "rule", "id": "cover_len512", "title": "Cover len512" }, { "content": "rule cover_len1024(){requireInvariant addressSetInvariant(); assert mirrorArrayLen() != 1024;}", "type": "rule", "id": "cover_len1024", "title": "Cover len1024" } ] ================================================ FILE: certora/gho/specs/set.spec ================================================ methods{ function getFacilitatorsListLen() external returns (uint256) envfree; } definition MAX_UINT256() returns uint256 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; definition MAX_UINT256Bytes32() returns bytes32 = to_bytes32(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); //todo: remove once CERT-1060 is resolved definition TWO_TO_160() returns uint256 = 0x10000000000000000000000000000000000000000; /** * Set map entries point to valid array entries * @notice an essential condition of the set, should hold for evert Set implementation * @return true if all map entries points to valid indexes of the array. */ definition MAP_POINTS_INSIDE_ARRAY() returns bool = forall bytes32 a. mirrorMap[a] <= mirrorArrayLen; /** * Set map is the inverse function of set array. * @notice an essential condition of the set, should hold for evert Set implementation * @notice this condition depends on the other set conditions, but the other conditions do not depend on this condition. * If this condition is omitted the rest of the conditions still hold, but the other conditions are required to prove this condition. * @return true if for every valid index of the array it holds that map(array(index)) == index + 1. */ definition MAP_IS_INVERSE_OF_ARRAY() returns bool = forall uint256 i. (i < mirrorArrayLen) => to_mathint(mirrorMap[mirrorArray[i]]) == i + 1; /** * Set array is the inverse function of set map * @notice an essential condition of the set, should hold for evert Set implementation * @return true if for every non-zero bytes32 value stored in in the set map it holds that array(map(value) - 1) == value */ definition ARRAY_IS_INVERSE_OF_MAP() returns bool = forall bytes32 a. forall uint256 b. to_mathint(b) == mirrorMap[a]-1 => (mirrorMap[a] != 0) => (mirrorArray[b] == a); /** * load array length * @notice a dummy condition that forces load of array length, using it forces initialization of mirrorArrayLen * @return always true */ definition CVL_LOAD_ARRAY_LENGTH() returns bool = (getFacilitatorsListLen() == getFacilitatorsListLen()); /** * Set-general condition, encapsulating all conditions of Set * @notice this condition recaps the general characteristics of Set. It should hold for all set implementations i.e. AddressSet, UintSet, Bytes32Set * @return conjunction of the Set three essential properties. */ definition SET_INVARIANT() returns bool = MAP_POINTS_INSIDE_ARRAY() && MAP_IS_INVERSE_OF_ARRAY() && ARRAY_IS_INVERSE_OF_MAP() && CVL_LOAD_ARRAY_LENGTH(); /** * Size of stored value does not exceed the size of an address type. * @notice must be used for AddressSet, must not be used for Bytes32Set, UintSet * @return true if all array entries are less than 160 bits. **/ definition VALUE_IN_BOUNDS_OF_TYPE_ADDRESS() returns bool = (forall uint256 i. (mirrorArray[i]) & to_bytes32(max_uint160) == mirrorArray[i]); /** * A complete invariant condition for AddressSet * @notice invariant addressSetInvariant proves that this condition holds * @return conjunction of the Set-general and AddressSet-specific conditions **/ definition ADDRESS_SET_INVARIANT() returns bool = SET_INVARIANT() && VALUE_IN_BOUNDS_OF_TYPE_ADDRESS(); /** * A complete invariant condition for UintSet, Bytes32Set * @notice for UintSet and Bytes2St no type-specific condition is required because the type size is the same as the native type (bytes32) size * @return the Set-general condition **/ definition UINT_SET_INVARIANT() returns bool = SET_INVARIANT(); /** * Out of bound array entries are zero * @notice A non-essential condition. This condition can be proven as an invariant, but it is not necessary for proving the Set correctness. * @return true if all entries beyond array length are zero **/ definition ARRAY_OUT_OF_BOUND_ZERO() returns bool = forall uint256 i. (i >= mirrorArrayLen) => (mirrorArray[i] == to_bytes32(0)); // For CVL use /** * ghost mirror map, mimics Set map **/ ghost mapping(bytes32 => uint256) mirrorMap{ init_state axiom forall bytes32 a. mirrorMap[a] == 0; axiom forall bytes32 a. mirrorMap[a] >= 0 && mirrorMap[a] <= MAX_UINT256(); //todo: remove once https://certora.atlassian.net/browse/CERT-1060 is resolved } /** * ghost mirror array, mimics Set array **/ ghost mapping(uint256 => bytes32) mirrorArray{ init_state axiom forall uint256 i. mirrorArray[i] == to_bytes32(0); axiom forall uint256 a. mirrorArray[a] & MAX_UINT256Bytes32() == mirrorArray[a]; // axiom forall uint256 a. to_uint256(mirrorArray[a]) >= 0 && to_uint256(mirrorArray[a]) <= MAX_UINT256(); //todo: remove once CERT-1060 is resolved //axiom forall uint256 a. to_mathint(mirrorArray[a]) >= 0 && to_mathint(mirrorArray[a]) <= MAX_UINT256(); //todo: use this axiom when cast bytes32 to mathint is supported } /** * ghost mirror array length, mimics Set array length * @notice ghost includes an assumption about the array length. * If the assumption were not written in the ghost function it should be written in every rule and invariant. * The assumption holds: breaking the assumptions would violate the invariant condition 'map(array(index)) == index + 1'. Set map uses 0 as a sentinel value, so the array cannot contain MAX_INT different values. * The assumption is necessary: if a value is added when length==MAX_INT then length overflows and becomes zero. **/ ghost uint256 mirrorArrayLen{ init_state axiom mirrorArrayLen == 0; axiom to_mathint(mirrorArrayLen) < TWO_TO_160() - 1; //todo: remove once CERT-1060 is resolved } /** * hook for Set array stores * @dev user of this spec must replace _list with the instance name of the Set. **/ hook Sstore _facilitatorsList .(offset 0)[INDEX uint256 index] bytes32 newValue (bytes32 oldValue) STORAGE { mirrorArray[index] = newValue; } /** * hook for Set array loads * @dev user of this spec must replace _list with the instance name of the Set. **/ hook Sload bytes32 value _facilitatorsList .(offset 0)[INDEX uint256 index] STORAGE { require(mirrorArray[index] == value); } /** * hook for Set map stores * @dev user of this spec must replace _list with the instance name of the Set. **/ hook Sstore _facilitatorsList .(offset 32)[KEY bytes32 key] uint256 newIndex (uint256 oldIndex) STORAGE { mirrorMap[key] = newIndex; } /** * hook for Set map loads * @dev user of this spec must replace _list with the instance name of the Set. **/ hook Sload uint256 index _facilitatorsList .(offset 32)[KEY bytes32 key] STORAGE { require(mirrorMap[key] == index); } /** * hook for Set array length stores * @dev user of this spec must replace _list with the instance name of the Set. **/ hook Sstore _facilitatorsList .(offset 0).(offset 0) uint256 newLen (uint256 oldLen) STORAGE { mirrorArrayLen = newLen; } /** * hook for Set array length load * @dev user of this spec must replace _facilitatorsList with the instance name of the Set. **/ hook Sload uint256 len _facilitatorsList .(offset 0).(offset 0) STORAGE { require mirrorArrayLen == len; } /** * main Set general invariant **/ invariant setInvariant() SET_INVARIANT(); /** * main AddressSet invariant * @dev user of the spec should add 'requireInvariant addressSetInvariant();' to every rule and invariant that refer to a contract that instantiates AddressSet **/ invariant addressSetInvariant() ADDRESS_SET_INVARIANT(); /** * addAddress() successfully adds an address **/ rule api_add_succeeded() { env e; address a; requireInvariant addressSetInvariant(); require !contains(e, a); assert addAddress(e, a); assert contains(e, a); } /** * addAddress() fails to add an address if it already exists * @notice check set membership using contains() **/ rule api_add_failed_contains() { env e; address a; requireInvariant addressSetInvariant(); require contain(e, a); assert !addAddress(e, a); } /** * addAddress() fails to add an address if it already exists * @notice check set membership using atIndex() **/ rule api_add_failed_at() { env e; address a; uint256 index; requireInvariant addressSetInvariant(); require atIndex(e, index) == a; assert !addAddress(e, a); } /** * contains() succeed after addAddress succeeded **/ rule api_address_contained_after_add() { env e; address a; requireInvariant addressSetInvariant(); addAddress(e, a); assert contains(e, a); } /** * _removeAddress() succeeds to remove an address if it existed * @notice check set membership using contains() **/ rule api_remove_succeeded_contains() { env e; address a; requireInvariant addressSetInvariant(); require contains(e, a); assert _removeAddress(e, a); } /** * _removeAddress() fails to remove address if it didn't exist **/ rule api_remove_failed() { env e; address a; requireInvariant addressSetInvariant(); require !contains(e, a); assert !_removeAddress(e, a); } /** * _removeAddress() succeeds to remove an address if it existed * @notice check set membership using atIndex() **/ rule api_remove_succeeded_at() { env e; address a; uint256 index; requireInvariant addressSetInvariant(); require atIndex(e, index) == a; assert _removeAddress(e, a); } /** * contains() failed after an address was removed **/ rule api_not_contains_after_remove() { env e; address a; requireInvariant addressSetInvariant(); _removeAddress(e, a); assert !contains(e, a); } /** * contains() succeeds if atIndex() succeeded **/ rule cover_at_contains() { env e; address a = 0; requireInvariant addressSetInvariant(); uint256 index; require atIndex(e, index) == a; assert contains(e, a); } /** * cover properties, checking various array lengths * @notice The assertion should fail - it's a cover property written as an assertion. For large length, beyond loop_iter the assertion should pass. **/ rule cover_len0(){requireInvariant addressSetInvariant();assert mirrorArrayLen != 0;} rule cover_len1(){requireInvariant addressSetInvariant();assert mirrorArrayLen != 1;} rule cover_len2(){requireInvariant addressSetInvariant();assert mirrorArrayLen != 2;} rule cover_len3(){requireInvariant addressSetInvariant();assert mirrorArrayLen != 3;} rule cover_len4(){requireInvariant addressSetInvariant();assert mirrorArrayLen != 4;} rule cover_len5(){requireInvariant addressSetInvariant();assert mirrorArrayLen != 5;} rule cover_len6(){requireInvariant addressSetInvariant();assert mirrorArrayLen != 6;} rule cover_len7(){requireInvariant addressSetInvariant();assert mirrorArrayLen != 7;} rule cover_len8(){requireInvariant addressSetInvariant(); assert mirrorArrayLen != 8;} rule cover_len16(){requireInvariant addressSetInvariant(); assert mirrorArrayLen != 16;} rule cover_len32(){requireInvariant addressSetInvariant(); assert mirrorArrayLen != 32;} rule cover_len64(){requireInvariant addressSetInvariant(); assert mirrorArrayLen != 64;} rule cover_len128(){requireInvariant addressSetInvariant(); assert mirrorArrayLen != 128;} rule cover_len256(){requireInvariant addressSetInvariant(); assert mirrorArrayLen != 256;} rule cover_len512(){requireInvariant addressSetInvariant(); assert mirrorArrayLen != 512;} rule cover_len1024(){requireInvariant addressSetInvariant(); assert mirrorArrayLen != 1024;} ================================================ FILE: certora/gho/specs/summarizations.spec ================================================ // Definition of RAY unit function first_term(uint256 x, uint256 y) returns uint256 { return x; } ghost mapping(uint256 => mapping(uint256 => uint256)) rayMulSummariztionValues; function rayMulSummariztion(uint256 x, uint256 y) returns uint256 { if ((x == 0) || (y == 0)) { return 0; } if (x == ray()) { return y; } if (y == ray()) { return x; } if (y > x) { if (y > ray()) { require rayMulSummariztionValues[y][x] >= x; } if (x > ray()) { require rayMulSummariztionValues[y][x] >= y; } return rayMulSummariztionValues[y][x]; } else{ if (x > ray()) { require rayMulSummariztionValues[x][y] >= y; } if (y > ray()) { require rayMulSummariztionValues[x][y] >= x; } return rayMulSummariztionValues[x][y]; } } ================================================ FILE: certora/gsm/conf/gsm/FixedFeeStrategy.conf ================================================ { "files": [ "certora/gsm/harness/FixedFeeStrategyHarness.sol", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, "hashing_length_bound":"416", "solc": "solc8.10", "rule_sanity" : "basic", "msg": "Otakar: FixedFeeStrategy", "multi_assert_check": true, "smt_timeout": "4000", "prover_args": [ "-depth 20", ], "verify": "FixedFeeStrategyHarness:certora/gsm/specs/gsm/FixedFeeStrategy.spec", } ================================================ FILE: certora/gsm/conf/gsm/OracleSwapFreezer.conf ================================================ { "files": [ "certora/gsm/harness/OracleSwapFreezerHarness.sol", "src/contracts/facilitators/gsm/swapFreezer/OracleSwapFreezer.sol", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, "hashing_length_bound":"416", "solc": "solc8.10", "rule_sanity" : "basic", "multi_assert_check": true, "msg": "OracleSwapFreezer", "smt_timeout": "4000", "prover_args": [ "-copyLoopUnroll 6", "-depth 20", ], "verify": "OracleSwapFreezerHarness:certora/gsm/specs/gsm/OracleSwapFreezer.spec", } ================================================ FILE: certora/gsm/conf/gsm/balances-buy.conf ================================================ { "files": [ "certora/gsm/harness/GsmHarness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/FixedPriceStrategyHarness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "certora/gsm/harness/DiffHelper.sol", "src/contracts/gho/GhoToken.sol", ], "link": [ "GsmHarness:GHO_TOKEN=GhoToken", "GsmHarness:PRICE_STRATEGY=FixedPriceStrategyHarness", "GsmHarness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "rule_sanity" : "basic", "optimistic_hashing":true, "hashing_length_bound":"416", "solc": "solc8.10", "msg": "balances - buy", "prover_args": [ "-copyLoopUnroll 6", "-depth 20", ], "verify": "GsmHarness:certora/gsm/specs/gsm/balances-buy.spec", } ================================================ FILE: certora/gsm/conf/gsm/balances-sell.conf ================================================ { "files": [ "certora/gsm/harness/GsmHarness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/FixedPriceStrategyHarness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "certora/gsm/harness/DiffHelper.sol", "src/contracts/gho/GhoToken.sol", ], "link": [ "GsmHarness:GHO_TOKEN=GhoToken", "GsmHarness:PRICE_STRATEGY=FixedPriceStrategyHarness", "GsmHarness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "rule_sanity" : "basic", "optimistic_hashing":true, "hashing_length_bound":"416", "solc": "solc8.10", "msg": "balances - sell", "prover_args": [ "-copyLoopUnroll 6", "-depth 20", ], "verify": "GsmHarness:certora/gsm/specs/gsm/balances-sell.spec", } ================================================ FILE: certora/gsm/conf/gsm/fees-buy.conf ================================================ { "files": [ "certora/gsm/harness/GsmHarness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/FixedPriceStrategyHarness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "certora/gsm/harness/DiffHelper.sol", "src/contracts/gho/GhoToken.sol", ], "link": [ "GsmHarness:GHO_TOKEN=GhoToken", "GsmHarness:PRICE_STRATEGY=FixedPriceStrategyHarness", "GsmHarness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "rule_sanity" : "basic", "optimistic_hashing":true, "hashing_length_bound":"416", "solc": "solc8.10", "msg": "fees - buy depth 20 mult-assert noadaptive no linears", "multi_assert_check": true, "prover_args": [ "-copyLoopUnroll 6", "-depth 20", "-adaptiveSolverConfig false", "-smt_nonLinearArithmetic true", ], "verify": "GsmHarness:certora/gsm/specs/gsm/fees-buy.spec", } ================================================ FILE: certora/gsm/conf/gsm/fees-sell.conf ================================================ { "files": [ "certora/gsm/harness/GsmHarness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/FixedPriceStrategyHarness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "certora/gsm/harness/DiffHelper.sol", "src/contracts/gho/GhoToken.sol", ], "link": [ "GsmHarness:GHO_TOKEN=GhoToken", "GsmHarness:PRICE_STRATEGY=FixedPriceStrategyHarness", "GsmHarness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "rule_sanity" : "basic", "optimistic_hashing":true, "hashing_length_bound":"416", "solc": "solc8.10", "smt_timeout": "7200", "msg": "fees - sell", "multi_assert_check": true, "prover_args": [ "-copyLoopUnroll 6", "-depth 20", "-adaptiveSolverConfig false", "-smt_nonLinearArithmetic true", ], "verify": "GsmHarness:certora/gsm/specs/gsm/fees-sell.spec" } ================================================ FILE: certora/gsm/conf/gsm/finishedRules.conf ================================================ { "files": [ "certora/gsm/harness/GsmHarness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/ERC20Helper.sol", "certora/gsm/harness/FixedPriceStrategyHarness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "src/contracts/gho/GhoToken.sol", ], "link": [ "GsmHarness:GHO_TOKEN=GhoToken", "GsmHarness:PRICE_STRATEGY=FixedPriceStrategyHarness", "GsmHarness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, "hashing_length_bound":"416", "rule_sanity" : "basic", "solc": "solc8.10", "msg": "optimalityOfBuy", "smt_timeout": "4000", "multi_assert_check": true, "prover_args": [ "-copyLoopUnroll 6", "-depth 20", ], "verify": "GsmHarness:certora/gsm/specs/gsm/gho-gsm-finishedRules.spec", } ================================================ FILE: certora/gsm/conf/gsm/getAmount_properties.conf ================================================ { "files": [ "certora/gsm/harness/GsmHarness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/ERC20Helper.sol", "certora/gsm/harness/FixedPriceStrategyHarness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "src/contracts/gho/GhoToken.sol", ], "link": [ "GsmHarness:GHO_TOKEN=GhoToken", "GsmHarness:PRICE_STRATEGY=FixedPriceStrategyHarness", "GsmHarness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, "rule_sanity" : "basic", "hashing_length_bound":"416", "solc": "solc8.10", "smt_timeout": "7200", "multi_assert_check": true, "msg": "gsm properties", "prover_args": [ "-copyLoopUnroll 6", "-depth 20" ], "verify": "GsmHarness:certora/gsm/specs/gsm/getAmount_properties.spec", } ================================================ FILE: certora/gsm/conf/gsm/gho-assetToGhoInvertibility.conf ================================================ { "files": [ "certora/gsm/harness/GsmHarness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/ERC20Helper.sol", "certora/gsm/harness/FixedPriceStrategyHarness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "src/contracts/gho/GhoToken.sol", ], "link": [ "GsmHarness:GHO_TOKEN=GhoToken", "GsmHarness:PRICE_STRATEGY=FixedPriceStrategyHarness", "GsmHarness:_feeStrategy=FixedFeeStrategyHarness" ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, "rule_sanity" : "basic", "hashing_length_bound":"416", "solc": "solc8.10", "msg": "gsm getAsset/GhoAmountForBuy/SellAsset invertibility rules", "smt_timeout": "7200", "prover_args": [ "-copyLoopUnroll 6", "-depth 20" ], "multi_assert_check": true, "verify": "GsmHarness:certora/gsm/specs/gsm/AssetToGhoInvertibility.spec", } ================================================ FILE: certora/gsm/conf/gsm/gho-fixedPriceStrategy.conf ================================================ { "files": [ "certora/gsm/harness/FixedPriceStrategyHarness.sol", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, "hashing_length_bound":"416", "solc": "solc8.10", "msg": "gsm4626 - getAssetAmountInGho and getGhoAmountInAsset are inverse", "smt_timeout": "7200", "rule_sanity" : "basic", "prover_args": [ "-copyLoopUnroll 6", "-depth 20" ], "multi_assert_check": true, "verify": "FixedPriceStrategyHarness:certora/gsm/specs/gsm/FixedPriceStrategy.spec", } ================================================ FILE: certora/gsm/conf/gsm/gho-gsm-2.conf ================================================ { "files": [ "certora/gsm/harness/GsmHarness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/FixedPriceStrategyHarness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "certora/gsm/harness/ERC20Helper.sol:ERC20Helper", "src/contracts/gho/GhoToken.sol", ], "link": [ "GsmHarness:GHO_TOKEN=GhoToken", "GsmHarness:PRICE_STRATEGY=FixedPriceStrategyHarness", "GsmHarness:_feeStrategy=FixedFeeStrategyHarness", "GsmHarness:UNDERLYING_ASSET=DummyERC20B" ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "parametric_contracts": [ "GsmHarness"], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "rule_sanity" : "basic", "optimistic_hashing":true, "hashing_length_bound":"416", "solc": "solc8.10", "msg": "gsm properties", "prover_args": [ "-copyLoopUnroll 6", "-depth 20", "-smt_hashingScheme plainInjectivity" ], "verify": "GsmHarness:certora/gsm/specs/gsm/gho-gsm-2.spec", } ================================================ FILE: certora/gsm/conf/gsm/gho-gsm.conf ================================================ { "files": [ "certora/gsm/harness/GsmHarness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/ERC20Helper.sol", "certora/gsm/harness/FixedPriceStrategyHarness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "src/contracts/gho/GhoToken.sol", ], "link": [ "GsmHarness:GHO_TOKEN=GhoToken", "GsmHarness:PRICE_STRATEGY=FixedPriceStrategyHarness", "GsmHarness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "parametric_contracts": [ "GsmHarness"], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, "rule_sanity" : "basic", "hashing_length_bound":"416", "solc": "solc8.10", "msg": "gsm properties", "smt_timeout": "7200", "prover_args": [ "-copyLoopUnroll 6", "-depth 20" ], "verify": "GsmHarness:certora/gsm/specs/gsm/gho-gsm.spec", } ================================================ FILE: certora/gsm/conf/gsm/gho-gsm_inverse.conf ================================================ { "files": [ "certora/gsm/harness/GsmHarness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/FixedPriceStrategyHarness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "certora/gsm/harness/ERC20Helper.sol", "src/contracts/gho/GhoToken.sol", ], "link": [ "GsmHarness:GHO_TOKEN=GhoToken", "GsmHarness:PRICE_STRATEGY=FixedPriceStrategyHarness", "GsmHarness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, "hashing_length_bound":"416", "solc": "solc8.10", "rule_sanity" : "basic", "msg": "gsm properties", "smt_timeout": "7200", "prover_args": [ "-copyLoopUnroll 6", "-depth 20" ], "verify": "GsmHarness:certora/gsm/specs/gsm/gho-gsm_inverse.spec", } ================================================ FILE: certora/gsm/conf/gsm/optimality.conf ================================================ { "files": [ "certora/gsm/harness/GsmHarness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/ERC20Helper.sol", "certora/gsm/harness/FixedPriceStrategyHarness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "src/contracts/gho/GhoToken.sol", ], "link": [ "GsmHarness:GHO_TOKEN=GhoToken", "GsmHarness:PRICE_STRATEGY=FixedPriceStrategyHarness", "GsmHarness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "rule_sanity" : "basic", "loop_iter":"1", "optimistic_hashing":true, "hashing_length_bound":"416", "solc": "solc8.10", "msg": "optimality of buyAsset - multi_assert", "multi_assert_check": true, "prover_args": [ "-copyLoopUnroll 6", "-depth 20" ], "verify": "GsmHarness:certora/gsm/specs/gsm/optimality.spec", } ================================================ FILE: certora/gsm/conf/gsm4626/balances-buy-4626.conf ================================================ { "files": [ "certora/gsm/harness/Gsm4626Harness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/ERC20Helper.sol", "certora/gsm/harness/FixedPriceStrategy4626Harness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "src/contracts/gho/GhoToken.sol", "certora/gsm/harness/DiffHelper.sol", ], "link": [ "Gsm4626Harness:GHO_TOKEN=GhoToken", "Gsm4626Harness:PRICE_STRATEGY=FixedPriceStrategy4626Harness", "Gsm4626Harness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, "rule_sanity" : "basic", "hashing_length_bound":"416", "solc": "solc8.10", "msg": "4626 balances - buy", "prover_args": [ "-copyLoopUnroll 6", "-depth 20", ], "verify": "Gsm4626Harness:certora/gsm/specs/gsm4626/balances-buy-4626.spec", } ================================================ FILE: certora/gsm/conf/gsm4626/balances-sell-4626.conf ================================================ { "files": [ "certora/gsm/harness/Gsm4626Harness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/ERC20Helper.sol", "certora/gsm/harness/FixedPriceStrategy4626Harness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "src/contracts/gho/GhoToken.sol", "certora/gsm/harness/DiffHelper.sol", ], "link": [ "Gsm4626Harness:GHO_TOKEN=GhoToken", "Gsm4626Harness:PRICE_STRATEGY=FixedPriceStrategy4626Harness", "Gsm4626Harness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, // "rule_sanity" : "basic", "hashing_length_bound":"416", "solc": "solc8.10", // "smt_timeout": "7200", "msg": "4626 balances - sell", "prover_args": [ "-copyLoopUnroll 6", "-depth 30", ], "verify": "Gsm4626Harness:certora/gsm/specs/gsm4626/balances-sell-4626.spec", } ================================================ FILE: certora/gsm/conf/gsm4626/fees-buy-4626.conf ================================================ { "files": [ "certora/gsm/harness/Gsm4626Harness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/FixedPriceStrategy4626Harness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "src/contracts/gho/GhoToken.sol", "certora/gsm/harness/DiffHelper.sol", ], "link": [ "Gsm4626Harness:GHO_TOKEN=GhoToken", "Gsm4626Harness:PRICE_STRATEGY=FixedPriceStrategy4626Harness", "Gsm4626Harness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, "multi_assert_check": true, "rule_sanity" : "basic", "hashing_length_bound":"416", "solc": "solc8.10", "msg": "4626 fees - buy", "prover_args": [ "-copyLoopUnroll 6", "-depth 20", ], "verify": "Gsm4626Harness:certora/gsm/specs/gsm4626/fees-buy-4626.spec", } ================================================ FILE: certora/gsm/conf/gsm4626/fees-sell-4626.conf ================================================ { "files": [ "certora/gsm/harness/Gsm4626Harness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/FixedPriceStrategy4626Harness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "src/contracts/gho/GhoToken.sol", "certora/gsm/harness/DiffHelper.sol", ], "link": [ "Gsm4626Harness:GHO_TOKEN=GhoToken", "Gsm4626Harness:PRICE_STRATEGY=FixedPriceStrategy4626Harness", "Gsm4626Harness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", // "rule_sanity" : "basic", "optimistic_hashing":true, "hashing_length_bound":"416", "solc": "solc8.10", "msg": "4626 fees - sell", "prover_args": [ "-copyLoopUnroll 6", "-depth 20", ], "verify": "Gsm4626Harness:certora/gsm/specs/gsm4626/fees-sell-4626.spec", } ================================================ FILE: certora/gsm/conf/gsm4626/finishedRules4626.conf ================================================ { "files": [ "certora/gsm/harness/Gsm4626Harness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/ERC20Helper.sol", "certora/gsm/harness/FixedPriceStrategy4626Harness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "src/contracts/gho/GhoToken.sol", ], "link": [ "Gsm4626Harness:GHO_TOKEN=GhoToken", "Gsm4626Harness:PRICE_STRATEGY=FixedPriceStrategy4626Harness", "Gsm4626Harness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, "rule_sanity" : "basic", "hashing_length_bound":"416", "solc": "solc8.10", "msg": "finishedRuless4626", "multi_assert_check": true, "smt_timeout": "4000", "prover_args": [ "-copyLoopUnroll 6", "-depth 20", // "-newSplitParallel true", // "-smt_hashingScheme plainInjectivity", ], "verify": "Gsm4626Harness:certora/gsm/specs/gsm4626/gho-gsm-finishedRules4626.spec", } ================================================ FILE: certora/gsm/conf/gsm4626/getAmount_4626_properties.conf ================================================ { "files": [ "certora/gsm/harness/Gsm4626Harness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/ERC20Helper.sol", "certora/gsm/harness/FixedPriceStrategy4626Harness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "src/contracts/gho/GhoToken.sol", ], "link": [ "Gsm4626Harness:GHO_TOKEN=GhoToken", "Gsm4626Harness:PRICE_STRATEGY=FixedPriceStrategy4626Harness", "Gsm4626Harness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, // "rule_sanity" : "basic", "hashing_length_bound":"416", "solc": "solc8.10", // "smt_timeout": "7200", "multi_assert_check": true, "msg": "gsm 4626 properties", "prover_args": [ "-copyLoopUnroll 6", "-depth 30" ], "verify": "Gsm4626Harness:certora/gsm/specs/gsm4626/getAmount_4626_properties.spec", } ================================================ FILE: certora/gsm/conf/gsm4626/gho-assetToGhoInvertibility4626.conf ================================================ { "files": [ "certora/gsm/harness/Gsm4626Harness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/ERC20Helper.sol", "certora/gsm/harness/FixedPriceStrategy4626Harness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "src/contracts/gho/GhoToken.sol", ], "link": [ "Gsm4626Harness:GHO_TOKEN=GhoToken", "Gsm4626Harness:PRICE_STRATEGY=FixedPriceStrategy4626Harness", "Gsm4626Harness:_feeStrategy=FixedFeeStrategyHarness" ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "rule_sanity" : "basic", "optimistic_hashing":true, "hashing_length_bound":"416", "solc": "solc8.10", "msg": "gsm4626 getAsset/GhoAmountForBuy/SellAsset invertibility rules", "smt_timeout": "7200", "prover_args": [ "-copyLoopUnroll 6", "-depth 20" ], "multi_assert_check": true, "verify": "Gsm4626Harness:certora/gsm/specs/gsm4626/AssetToGhoInvertibility4626.spec", } ================================================ FILE: certora/gsm/conf/gsm4626/gho-fixedPriceStrategy4626.conf ================================================ { "files": [ "certora/gsm/harness/FixedPriceStrategy4626Harness.sol", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, "rule_sanity" : "basic", "hashing_length_bound":"416", "solc": "solc8.10", "msg": "gsm4626 - getAssetAmountInGho and getGhoAmountInAsset are inverse", "smt_timeout": "7200", "prover_args": [ "-copyLoopUnroll 6", "-depth 20" ], "multi_assert_check": true, "verify": "FixedPriceStrategy4626Harness:certora/gsm/specs/gsm4626/FixedPriceStrategy4626.spec", } ================================================ FILE: certora/gsm/conf/gsm4626/gho-gsm4626-2.conf ================================================ { "files": [ "certora/gsm/harness/Gsm4626Harness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "src/contracts/gho/GhoToken.sol", "certora/gsm/harness/FixedPriceStrategy4626Harness.sol:FixedPriceStrategy4626Harness", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "certora/gsm/harness/ERC20Helper.sol:ERC20Helper", ], "parametric_contracts": [ "Gsm4626Harness"], "link": [ "Gsm4626Harness:GHO_TOKEN=GhoToken", "Gsm4626Harness:PRICE_STRATEGY=FixedPriceStrategy4626Harness", "Gsm4626Harness:_feeStrategy=FixedFeeStrategyHarness", "Gsm4626Harness:UNDERLYING_ASSET=DummyERC20B" ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "rule_sanity" : "basic", "loop_iter":"1", "optimistic_hashing":true, "hashing_length_bound":"416", "solc": "solc8.10", "msg": "gsm properties", "prover_args": [ "-copyLoopUnroll 6", "-smt_hashingScheme plainInjectivity" ], "verify": "Gsm4626Harness:certora/gsm/specs/gsm4626/gho-gsm4626-2.spec", } ================================================ FILE: certora/gsm/conf/gsm4626/gho-gsm4626.conf ================================================ { "files": [ "certora/gsm/harness/Gsm4626Harness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/ERC20Helper.sol", "certora/gsm/harness/FixedPriceStrategy4626Harness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "src/contracts/gho/GhoToken.sol", ], "link": [ "Gsm4626Harness:GHO_TOKEN=GhoToken", "Gsm4626Harness:PRICE_STRATEGY=FixedPriceStrategy4626Harness", "Gsm4626Harness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "parametric_contracts": [ "Gsm4626Harness"], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, "hashing_length_bound":"416", "solc": "solc8.10", "msg": "gsm 4626 properties", "smt_timeout": "7200", "rule_sanity": "basic", "prover_args": [ "-copyLoopUnroll 6", "-depth 20" ], "verify": "Gsm4626Harness:certora/gsm/specs/gsm4626/gho-gsm4626.spec", } ================================================ FILE: certora/gsm/conf/gsm4626/gho-gsm_4626_inverse.conf ================================================ { "files": [ "certora/gsm/harness/Gsm4626Harness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/FixedPriceStrategy4626Harness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "certora/gsm/harness/ERC20Helper.sol", "src/contracts/gho/GhoToken.sol", ], "link": [ "Gsm4626Harness:GHO_TOKEN=GhoToken", "Gsm4626Harness:PRICE_STRATEGY=FixedPriceStrategy4626Harness", "Gsm4626Harness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, "hashing_length_bound":"416", "solc": "solc8.10", "rule_sanity" : "basic", "msg": "gsm properties", "smt_timeout": "7200", "prover_args": [ "-copyLoopUnroll 6", "-depth 20" ], "verify": "Gsm4626Harness:certora/gsm/specs/gsm4626/gho-gsm_4626_inverse.spec", } ================================================ FILE: certora/gsm/conf/gsm4626/optimality4626.conf ================================================ { "files": [ "certora/gsm/harness/Gsm4626Harness.sol", "certora/gsm/harness/DummyERC20A.sol", "certora/gsm/harness/DummyERC20B.sol", "certora/gsm/harness/ERC20Helper.sol", "certora/gsm/harness/FixedPriceStrategy4626Harness.sol", "certora/gsm/harness/FixedFeeStrategyHarness.sol", "src/contracts/gho/GhoToken.sol", ], "link": [ "Gsm4626Harness:GHO_TOKEN=GhoToken", "Gsm4626Harness:PRICE_STRATEGY=FixedPriceStrategy4626Harness", "Gsm4626Harness:_feeStrategy=FixedFeeStrategyHarness", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", ], "assert_autofinder_success": true, "optimistic_loop":true, "loop_iter":"1", "optimistic_hashing":true, "rule_sanity" : "basic", "hashing_length_bound":"416", "solc": "solc8.10", "msg": "optimality of sell and buy - multi_assert", "multi_assert_check": true, "prover_args": [ "-copyLoopUnroll 6", "-depth 20" ], "verify": "Gsm4626Harness:certora/gsm/specs/gsm4626/optimality4626.spec", } ================================================ FILE: certora/gsm/harness/DiffHelper.sol ================================================ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; contract DiffHelper { function differsByAtMostN(uint256 a, uint256 b, uint256 N) public pure returns (bool) { if (a > b) { return a - b <= N; } else { return b - a <= N; } } } ================================================ FILE: certora/gsm/harness/DummyERC20A.sol ================================================ pragma solidity ^0.8.0; import './DummyERC20Impl.sol'; contract DummyERC20A is DummyERC20Impl {} ================================================ FILE: certora/gsm/harness/DummyERC20B.sol ================================================ pragma solidity ^0.8.0; import './DummyERC20Impl.sol'; contract DummyERC20B is DummyERC20Impl {} ================================================ FILE: certora/gsm/harness/DummyERC20Impl.sol ================================================ // SPDX-License-Identifier: agpl-3.0 pragma solidity ^0.8.0; // with mint contract DummyERC20Impl { uint256 t; mapping(address => uint256) b; mapping(address => mapping(address => uint256)) a; string public name; string public symbol; uint public decimals; function myAddress() public returns (address) { return address(this); } function add(uint a, uint b) internal pure returns (uint256) { uint c = a + b; require(c >= a); return c; } function sub(uint a, uint b) internal pure returns (uint256) { require(a >= b); return a - b; } function totalSupply() external view returns (uint256) { return t; } function balanceOf(address account) external view returns (uint256) { return b[account]; } function transfer(address recipient, uint256 amount) external returns (bool) { b[msg.sender] = sub(b[msg.sender], amount); b[recipient] = add(b[recipient], amount); return true; } function allowance(address owner, address spender) external view returns (uint256) { return a[owner][spender]; } function approve(address spender, uint256 amount) external returns (bool) { a[msg.sender][spender] = amount; return true; } function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { b[sender] = sub(b[sender], amount); b[recipient] = add(b[recipient], amount); a[sender][msg.sender] = sub(a[sender][msg.sender], amount); return true; } } ================================================ FILE: certora/gsm/harness/ERC20Helper.sol ================================================ // SPDX-License-Identifier: agpl-3.0 pragma solidity ^0.8.0; import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; contract ERC20Helper { function tokenBalanceOf(address token, address user) public returns (uint256) { return IERC20(token).balanceOf(user); } function tokenTotalSupply(address token) public returns (uint256) { return IERC20(token).totalSupply(); } } ================================================ FILE: certora/gsm/harness/FixedFeeStrategyHarness.sol ================================================ pragma solidity ^0.8.0; import {FixedFeeStrategy} from '../../../src/contracts/facilitators/gsm/feeStrategy/FixedFeeStrategy.sol'; import {PercentageMath} from '@aave/core-v3/contracts/protocol/libraries/math/PercentageMath.sol'; contract FixedFeeStrategyHarness is FixedFeeStrategy { constructor(uint256 buyFee, uint256 sellFee) FixedFeeStrategy(buyFee, sellFee) {} function getBuyFeeBP() external view returns (uint256) { return _buyFee; } function getSellFeeBP() external view returns (uint256) { return _sellFee; } function getPercMathPercentageFactor() external view returns (uint256) { return PercentageMath.PERCENTAGE_FACTOR; } } ================================================ FILE: certora/gsm/harness/FixedPriceStrategy4626Harness.sol ================================================ pragma solidity ^0.8.0; import {FixedPriceStrategy4626} from '../../../src/contracts/facilitators/gsm/priceStrategy/FixedPriceStrategy4626.sol'; contract FixedPriceStrategy4626Harness is FixedPriceStrategy4626 { constructor( uint256 priceRatio, address underlyingAsset, uint8 underlyingAssetDecimals ) FixedPriceStrategy4626(priceRatio, underlyingAsset, underlyingAssetDecimals) {} function getUnderlyingAssetUnits() external view returns (uint256) { return _underlyingAssetUnits; } function getPriceRatio() external view returns (uint256) { return PRICE_RATIO; } } ================================================ FILE: certora/gsm/harness/FixedPriceStrategyHarness.sol ================================================ pragma solidity ^0.8.0; import {FixedPriceStrategy} from '../../../src/contracts/facilitators/gsm/priceStrategy/FixedPriceStrategy.sol'; contract FixedPriceStrategyHarness is FixedPriceStrategy { constructor( uint256 priceRatio, address underlyingAsset, uint8 underlyingAssetDecimals ) FixedPriceStrategy(priceRatio, underlyingAsset, underlyingAssetDecimals) {} function getUnderlyingAssetUnits() external view returns (uint256) { return _underlyingAssetUnits; } function getUnderlyingAssetDecimals() external view returns (uint256) { return UNDERLYING_ASSET_DECIMALS; } function getPriceRatio() external view returns (uint256) { return PRICE_RATIO; } } ================================================ FILE: certora/gsm/harness/Gsm4626Harness.sol ================================================ pragma solidity ^0.8.0; import {Gsm4626} from '../../../src/contracts/facilitators/gsm/Gsm4626.sol'; import {IGhoToken} from '../../../src/contracts/gho/interfaces/IGhoToken.sol'; import {IGsmPriceStrategy} from '../../../src/contracts/facilitators/gsm/priceStrategy/interfaces/IGsmPriceStrategy.sol'; import {FixedPriceStrategy4626Harness} from './FixedPriceStrategy4626Harness.sol'; import {FixedFeeStrategyHarness} from './FixedFeeStrategyHarness.sol'; import {IGsmFeeStrategy} from '../../../src/contracts/facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol'; import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; import {IERC4626} from '@openzeppelin/contracts/interfaces/IERC4626.sol'; contract Gsm4626Harness is Gsm4626 { constructor( address ghoToken, address underlyingAsset, address priceStrategy ) Gsm4626(ghoToken, underlyingAsset, priceStrategy) {} function getAccruedFee() external view returns (uint256) { return _accruedFees; } function getCurrentExposure() external view returns (uint256) { return _currentExposure; } function getGhoMinted() public view returns (uint256 ghoMinted) { (, ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); } function getDearth() external view returns (uint256 dearth) { (, dearth) = _getCurrentBacking(getGhoMinted()); } function getExcess() external view returns (uint256 excess) { (excess, ) = _getCurrentBacking(getGhoMinted()); } function getPriceRatio() external returns (uint256 priceRatio) { priceRatio = FixedPriceStrategy4626Harness(PRICE_STRATEGY).PRICE_RATIO(); } function getAssetPriceInGho(uint256 amount, bool roundUp) external returns (uint256 priceInGho) { priceInGho = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho(amount, roundUp); } function getUnderlyingAssetUnits() external returns (uint256 underlyingAssetUnits) { underlyingAssetUnits = FixedPriceStrategy4626Harness(PRICE_STRATEGY).getUnderlyingAssetUnits(); } function zeroModulo(uint256 x, uint256 y, uint256 z) external pure { require((x * y) % z == 0); } function getBuyFeeBP() external returns (uint256) { return FixedFeeStrategyHarness(_feeStrategy).getBuyFeeBP(); } function getSellFeeBP() external returns (uint256) { return FixedFeeStrategyHarness(_feeStrategy).getSellFeeBP(); } function getPercMathPercentageFactor() external view returns (uint256) { return FixedFeeStrategyHarness(_feeStrategy).getPercMathPercentageFactor(); } function getCurrentGhoBalance() external view returns (uint256) { return IERC20(GHO_TOKEN).balanceOf(address(this)); } function getCurrentUnderlyingBalance() external view returns (uint256) { return IERC20(UNDERLYING_ASSET).balanceOf(address(this)); } function giftGho(address sender, uint amount) external { IGhoToken(GHO_TOKEN).transferFrom(sender, address(this), amount); } function giftUnderlyingAsset(address sender, uint amount) external { IERC20(UNDERLYING_ASSET).transferFrom(sender, address(this), amount); } function getSellFee(uint256 amount) external returns (uint256) { return IGsmFeeStrategy(_feeStrategy).getSellFee(amount); } function getBuyFee(uint256 amount) external returns (uint256) { return IGsmFeeStrategy(_feeStrategy).getBuyFee(amount); } function balanceOfUnderlying(address a) external view returns (uint256) { return IERC20(UNDERLYING_ASSET).balanceOf(a); } function balanceOfGho(address a) external view returns (uint256) { return IGhoToken(GHO_TOKEN).balanceOf(a); } function getGhoBalanceOfThis() external view returns (uint256) { return IGhoToken(GHO_TOKEN).balanceOf(address(this)); } function getExceed() external view returns (uint256 exceed) { (exceed, ) = _getCurrentBacking(getGhoMinted()); } function cumulateYieldInGho() external { _cumulateYieldInGho(); } function balanceOfUnderlyingDirect(address a) external view returns (uint256) { return IERC4626(UNDERLYING_ASSET).balanceOf(a); } function getFacilitatorBucket() public view returns (uint256 ghoBucketLevel, uint256 ghoMinted) { (ghoBucketLevel, ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); } function getUnderlyingAssetDecimals() external returns (uint256 underlyingAssetDecimals) { underlyingAssetDecimals = IGsmPriceStrategy(PRICE_STRATEGY).UNDERLYING_ASSET_DECIMALS(); } } ================================================ FILE: certora/gsm/harness/GsmHarness.sol ================================================ pragma solidity ^0.8.0; import {Gsm} from '../../../src/contracts/facilitators/gsm/Gsm.sol'; import {IGhoToken} from '../../../src/contracts/gho/interfaces/IGhoToken.sol'; import {IGsmPriceStrategy} from '../../../src/contracts/facilitators/gsm/priceStrategy/interfaces/IGsmPriceStrategy.sol'; import {IGsmFeeStrategy} from '../../../src/contracts/facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol'; import {FixedPriceStrategyHarness} from './FixedPriceStrategyHarness.sol'; import {FixedFeeStrategyHarness} from './FixedFeeStrategyHarness.sol'; import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; contract GsmHarness is Gsm { constructor( address ghoToken, address underlyingAsset, address priceStrategy ) Gsm(ghoToken, underlyingAsset, priceStrategy) {} function getAccruedFee() external view returns (uint256) { return _accruedFees; } function getCurrentExposure() external view returns (uint128) { return _currentExposure; } function getGhoMinted() public view returns (uint256 ghoMinted) { (, ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); } function getPriceRatio() external returns (uint256 priceRatio) { priceRatio = FixedPriceStrategyHarness(PRICE_STRATEGY).PRICE_RATIO(); } function getUnderlyingAssetUnits() external returns (uint256 underlyingAssetUnits) { underlyingAssetUnits = FixedPriceStrategyHarness(PRICE_STRATEGY).getUnderlyingAssetUnits(); } function getUnderlyingAssetDecimals() external returns (uint256 underlyingAssetDecimals) { underlyingAssetDecimals = IGsmPriceStrategy(PRICE_STRATEGY).UNDERLYING_ASSET_DECIMALS(); } function getAssetPriceInGho(uint256 amount, bool roundUp) external returns (uint256 priceInGho) { priceInGho = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho(amount, roundUp); } function zeroModulo(uint256 x, uint256 y, uint256 z) external pure { require((x * y) % z == 0); } function getSellFee(uint256 amount) external returns (uint256) { return IGsmFeeStrategy(_feeStrategy).getSellFee(amount); } function getBuyFee(uint256 amount) external returns (uint256) { return IGsmFeeStrategy(_feeStrategy).getBuyFee(amount); } function getBuyFeeBP() external returns (uint256) { return FixedFeeStrategyHarness(_feeStrategy).getBuyFeeBP(); } function getSellFeeBP() external returns (uint256) { return FixedFeeStrategyHarness(_feeStrategy).getSellFeeBP(); } function getPercMathPercentageFactor() external view returns (uint256) { return FixedFeeStrategyHarness(_feeStrategy).getPercMathPercentageFactor(); } function balanceOfUnderlying(address a) external view returns (uint256) { return IERC20(UNDERLYING_ASSET).balanceOf(a); } function balanceOfGho(address a) external view returns (uint256) { return IGhoToken(GHO_TOKEN).balanceOf(a); } function getCurrentGhoBalance() external view returns (uint256) { return IERC20(GHO_TOKEN).balanceOf(address(this)); } function getCurrentUnderlyingBalance() external view returns (uint256) { return IERC20(UNDERLYING_ASSET).balanceOf(address(this)); } function giftGho(address sender, uint amount) external { IGhoToken(GHO_TOKEN).transferFrom(sender, address(this), amount); } function giftUnderlyingAsset(address sender, uint amount) external { IERC20(UNDERLYING_ASSET).transferFrom(sender, address(this), amount); } function getGhoBalanceOfThis() external view returns (uint256) { return IGhoToken(GHO_TOKEN).balanceOf(address(this)); } } ================================================ FILE: certora/gsm/harness/OracleSwapFreezerHarness.sol ================================================ pragma solidity ^0.8.0; import {OracleSwapFreezer} from '../../../src/contracts/facilitators/gsm/swapFreezer/OracleSwapFreezer.sol'; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; import {IPriceOracle} from '@aave/core-v3/contracts/interfaces/IPriceOracle.sol'; //import {AutomationCompatibleInterface} from '../dependencies/chainlink/AutomationCompatibleInterface.sol'; import {IGsm} from '../../../src/contracts/facilitators/gsm/interfaces/IGsm.sol'; contract OracleSwapFreezerHarness is OracleSwapFreezer { constructor( IGsm gsm, address underlyingAsset, IPoolAddressesProvider addressProvider, uint128 freezeLowerBound, uint128 freezeUpperBound, uint128 unfreezeLowerBound, uint128 unfreezeUpperBound, bool allowUnfreeze ) OracleSwapFreezer( gsm, underlyingAsset, addressProvider, freezeLowerBound, freezeUpperBound, unfreezeLowerBound, unfreezeUpperBound, allowUnfreeze ) {} function validateBounds( uint128 freezeLowerBound, uint128 freezeUpperBound, uint128 unfreezeLowerBound, uint128 unfreezeUpperBound, bool allowUnfreeze ) external pure returns (bool) { return _validateBounds( freezeLowerBound, freezeUpperBound, unfreezeLowerBound, unfreezeUpperBound, allowUnfreeze ); } function isActionAllowed(Action actionToExecute) external view returns (bool) { return _isActionAllowed(actionToExecute); } function getAction() external view returns (uint8) { Action res = _getAction(); if (res == Action.NONE) return 0; if (res == Action.FREEZE) return 1; if (res == Action.UNFREEZE) return 2; return 3; } function isSeized() external view returns (bool) { return GSM.getIsSeized(); } function isFreezeAllowed() external view returns (bool) { return _isActionAllowed(Action.FREEZE); } function isUnfreezeAllowed() external view returns (bool) { return _isActionAllowed(Action.UNFREEZE); } function isFrozen() external view returns (bool) { return GSM.getIsFrozen(); } function getPrice() external view returns (uint256) { return IPriceOracle(ADDRESS_PROVIDER.getPriceOracle()).getAssetPrice(UNDERLYING_ASSET); } function hasRole() external view returns (bool) { return GSM.hasRole(GSM.SWAP_FREEZER_ROLE(), address(this)); } } ================================================ FILE: certora/gsm/munged/.gitignore ================================================ * !.gitignore ================================================ FILE: certora/gsm/specs/GsmMethods/aave_fee_limits.spec ================================================ function feeLimits(env e) { require currentContract.getSellFeeBP(e) <= 5000 && currentContract.getBuyFeeBP(e) < 5000 && (currentContract.getSellFeeBP(e) > 0 || currentContract.getBuyFeeBP(e) > 0); } ================================================ FILE: certora/gsm/specs/GsmMethods/aave_price_fee_limits.spec ================================================ import "aave_price_limits.spec"; import "aave_fee_limits.spec"; ================================================ FILE: certora/gsm/specs/GsmMethods/aave_price_fee_limits_strict.spec ================================================ function feeLimits(env e) { require currentContract.getSellFeeBP(e) <= 1000 && currentContract.getBuyFeeBP(e) < 1000 && (currentContract.getSellFeeBP(e) > 0 || currentContract.getBuyFeeBP(e) > 0); } function priceLimits(env e) { uint8 exp; require 5 <= exp; require exp <= 27; require getUnderlyingAssetUnits(e) == require_uint256((10^exp)) && getPriceRatio(e) >= 10^16 && getPriceRatio(e) <= 10^20; } ================================================ FILE: certora/gsm/specs/GsmMethods/aave_price_limits.spec ================================================ function priceLimits(env e) { uint8 exp; require 5 <= exp; require exp <= 27; require getUnderlyingAssetUnits(e) == require_uint256((10^exp)) && getPriceRatio(e) >= 10^16 && getPriceRatio(e) <= 10^20; } ================================================ FILE: certora/gsm/specs/GsmMethods/erc20.spec ================================================ // ERC20 methods methods { function _.name() external => DISPATCHER(true); function _.symbol() external => DISPATCHER(true); function _.decimals() external => DISPATCHER(true); function _.totalSupply() external => DISPATCHER(true); function _.balanceOf(address) external => DISPATCHER(true); function _.allowance(address,address) external => DISPATCHER(true); function _.approve(address,uint256) external => DISPATCHER(true); function _.transfer(address,uint256) external => DISPATCHER(true); function _.transferFrom(address,address,uint256) external => DISPATCHER(true); } ================================================ FILE: certora/gsm/specs/GsmMethods/erc4626.spec ================================================ methods { function _.previewWithdraw(uint256 vaultAssets) external with (env e) => mulDivSummaryRounding(vaultAssets, 3, 5, Math.Rounding.Up) expect uint256; function _.convertToShares(uint256 vaultAssets) external with (env e) => require_uint256(vaultAssets * 3 / 5) expect uint256; function _.previewMint(uint256 shares) external with (env e) => mulDivSummaryRounding(shares, 5, 3, Math.Rounding.Up) expect uint256; function _.convertToAssets(uint256 shares) external with (env e) => require_uint256(shares * 5 / 3) expect uint256; } ================================================ FILE: certora/gsm/specs/GsmMethods/methods4626_base.spec ================================================ import "./erc20.spec"; using FixedPriceStrategy4626Harness as _priceStrategy; using FixedFeeStrategyHarness as _FixedFeeStrategy; using GhoToken as _ghoToken; using ERC20Helper as erc20Helper; /////////////////// Methods //////////////////////// methods { function _ghoToken.transferFrom(address from, address to, uint256 amount) external returns bool with (env e) => erc20_transferFrom_assumption(calledContract, e, from, to, amount); function _ghoToken.mint(address account, uint256 amount) external with (env e) => erc20_mint_assumption(calledContract, e, account, amount); function _ghoToken.transfer(address to, uint256 amount) external returns bool with (env e) => erc20_transfer_assumption(calledContract, e, to, amount); function getAvailableLiquidity() external returns (uint256) envfree; function getCurrentBacking() external returns(uint256, uint256) envfree; function erc20Helper.tokenBalanceOf(address token, address user) external returns (uint256) envfree; function erc20Helper.tokenTotalSupply(address token) external returns (uint256) envfree; // GSM.sol function _.UNDERLYING_ASSET() external => DISPATCHER(true); // priceStrategy function _priceStrategy.getAssetPriceInGho(uint256, bool) external returns(uint256) envfree; function _priceStrategy.getUnderlyingAssetUnits() external returns(uint256) envfree; function _priceStrategy.PRICE_RATIO() external returns(uint256) envfree; // feeStrategy function _FixedFeeStrategy.getBuyFeeBP() external returns(uint256) envfree; function _FixedFeeStrategy.getSellFeeBP() external returns(uint256) envfree; function _FixedFeeStrategy.getBuyFee(uint256) external returns(uint256) envfree; function _FixedFeeStrategy.getSellFee(uint256) external returns(uint256) envfree; // GhoToken function _ghoToken.getFacilitatorBucket(address) external returns (uint256, uint256) envfree; function _ghoToken.balanceOf(address) external returns (uint256) envfree; // Harness function getGhoMinted() external returns(uint256) envfree; function getPriceRatio() external returns (uint256) envfree; function getAccruedFees() external returns (uint256) envfree; } definition harnessOnlyMethods(method f) returns bool = (f.selector == sig:getAccruedFees().selector || f.selector == sig:getGhoMinted().selector || f.selector == sig:getDearth().selector || f.selector == sig:getPriceRatio().selector); definition buySellAssetsFunctions(method f) returns bool = (f.selector == sig:buyAsset(uint256,address).selector || f.selector == sig:buyAssetWithSig(address,uint256,address,uint256,bytes).selector || f.selector == sig:sellAsset(uint256,address).selector || f.selector == sig:sellAssetWithSig(address,uint256,address,uint256,bytes).selector); function basicBuySellSetup( env e, address receiver){ require receiver != currentContract; require e.msg.sender != currentContract; require UNDERLYING_ASSET(e) != _ghoToken; } function erc20_transferFrom_assumption(address token, env e, address from, address to, uint256 amount) returns bool { require erc20Helper.tokenBalanceOf(token, from) + erc20Helper.tokenBalanceOf(token, to) <= max_uint256; return _ghoToken.transferFrom(e, from, to, amount); } function erc20_mint_assumption(address token, env e, address account, uint256 amount) { require erc20Helper.tokenBalanceOf(token, account) + amount <= max_uint256; _ghoToken.mint(e, account, amount); } function erc20_transfer_assumption(address token, env e, address to, uint256 amount) returns bool{ require erc20Helper.tokenBalanceOf(token, to) + amount <= max_uint256; return _ghoToken.transfer(e, to, amount); } ================================================ FILE: certora/gsm/specs/GsmMethods/methods_base-Martin.spec ================================================ import "./erc20.spec"; using GhoToken as _ghoToken; using ERC20Helper as erc20Helper; /////////////////// Methods //////////////////////// methods { function _ghoToken.transferFrom(address from, address to, uint256 amount) external returns bool with (env e) => erc20_transferFrom_assumption(calledContract, e, from, to, amount); function _ghoToken.mint(address account, uint256 amount) external with (env e) => erc20_mint_assumption(calledContract, e, account, amount); function erc20Helper.tokenBalanceOf(address token, address user) external returns (uint256) envfree; function erc20Helper.tokenTotalSupply(address token) external returns (uint256) envfree; function getAvailableLiquidity() external returns (uint256) envfree; // GSM.sol // function _.previewRedeem(uint256 shares) external with(env e) => sharesToVaultAssets(e.block.timestamp, shares) expect uint256; // function _.previewWithdraw(uint256 vaultAssets) external with(env e) => vaultAssetsToShares(vaultAssets) expect uint256; function _.UNDERLYING_ASSET() external => DISPATCHER(true); function _.GHO_TOKEN() external => DISPATCHER(true); // GhoToken function _ghoToken.getFacilitatorBucket(address) external returns (uint256, uint256) envfree; // Harness function getGhoMinted() external returns(uint256) envfree; function getPriceRatio() external returns (uint256) envfree; function zeroModulo(uint256, uint256, uint256) external envfree; } definition harnessOnlyMethods(method f) returns bool = (f.selector == sig:getAccruedFees().selector || f.selector == sig:getGhoMinted().selector || f.selector == sig:getPriceRatio().selector || f.selector == sig:getExposureCap().selector || f.selector == sig:getGhoMinted().selector || f.selector == sig:getGhoMinted().selector || f.selector == sig:getPriceRatio().selector || f.selector == sig:getUnderlyingAssetUnits().selector || f.selector == sig:getUnderlyingAssetDecimals().selector || f.selector == sig:getAssetPriceInGho(uint256, bool).selector || f.selector == sig:getAssetPriceInGho(uint256, bool).selector || f.selector == sig:getSellFee(uint256).selector || f.selector == sig:getBuyFee(uint256).selector || f.selector == sig:getBuyFeeBP().selector || f.selector == sig:getSellFeeBP().selector || f.selector == sig:getPercMathPercentageFactor().selector || f.selector == sig:balanceOfGho(address).selector || f.selector == sig:getCurrentGhoBalance().selector || f.selector == sig:getCurrentUnderlyingBalance().selector || f.selector == sig:getGhoBalanceOfThis().selector || f.selector == sig:giftGho(address, uint).selector || f.selector == sig:giftUnderlyingAsset(address, uint).selector || f.selector == sig:balanceOfUnderlying(address).selector || f.selector == sig:getCurrentExposure().selector); // Wrapping function of erc20 transferFrom that guarantees no overflow. function erc20_transferFrom_assumption(address token, env e, address from, address to, uint256 amount) returns bool { require erc20Helper.tokenBalanceOf(token, from) + erc20Helper.tokenBalanceOf(token, to) <= max_uint256; return _ghoToken.transferFrom(e, from, to, amount); } // Wrapping function of erc20 mint that guarantees no overflow. function erc20_mint_assumption(address token, env e, address account, uint256 amount) { require erc20Helper.tokenBalanceOf(token, account) + amount <= max_uint256; _ghoToken.mint(e, account, amount); } /** * Maps shares to an arbitrary value ghost mapping(uint256 => mapping(uint256 => uint256)) shares_ghost { axiom (forall uint256 timestamp. forall uint256 shares1. forall uint256 shares2. (!(shares1 <= shares2) => !(shares_ghost[timestamp][shares1] <= shares_ghost[timestamp][shares2])) && shares_ghost[timestamp][0] == 0 && (shares_ghost[timestamp][shares1]/shares1 == shares_ghost[timestamp][shares2]/shares2)); } **/ function sharesToVaultAssets(uint256 timestamp, uint256 shares) returns uint256 { return require_uint256(shares * 5 / 3); // return assert_uint256((shares*3)/5); // MY ORIGINAL //return shares_ghost[timestamp][shares]; } function vaultAssetsToShares(uint256 vaultAssets) returns uint256 { return mulDivSummaryRounding(vaultAssets, 3, 5, Math.Rounding.Up); // return require_uint256((vaultAssets*5)/3); // MY ORIGINAL } ================================================ FILE: certora/gsm/specs/GsmMethods/methods_base.spec ================================================ import "./erc20.spec"; using FixedPriceStrategyHarness as _priceStrategy; using FixedFeeStrategyHarness as _FixedFeeStrategy; using GhoToken as _ghoToken; using ERC20Helper as erc20Helper; /////////////////// Methods //////////////////////// methods { function _ghoToken.transferFrom(address from, address to, uint256 amount) external returns bool with (env e) => erc20_transferFrom_assumption(calledContract, e, from, to, amount); function _ghoToken.mint(address account, uint256 amount) external with (env e) => erc20_mint_assumption(calledContract, e, account, amount); function _ghoToken.transfer(address to, uint256 amount) external returns bool with (env e) => erc20_transfer_assumption(calledContract, e, to, amount); function getAvailableLiquidity() external returns (uint256) envfree; function erc20Helper.tokenBalanceOf(address token, address user) external returns (uint256) envfree; function erc20Helper.tokenTotalSupply(address token) external returns (uint256) envfree; // GSM.sol function _.UNDERLYING_ASSET() external => DISPATCHER(true); // priceStrategy function _priceStrategy.getAssetPriceInGho(uint256, bool roundUp) external returns(uint256) envfree; function _priceStrategy.getUnderlyingAssetUnits() external returns(uint256) envfree; function _priceStrategy.PRICE_RATIO() external returns(uint256) envfree; function _priceStrategy.getUnderlyingAssetDecimals() external returns(uint256) envfree; // feeStrategy function _FixedFeeStrategy.getBuyFeeBP() external returns(uint256) envfree; function _FixedFeeStrategy.getSellFeeBP() external returns(uint256) envfree; function _FixedFeeStrategy.getBuyFee(uint256) external returns(uint256) envfree; function _FixedFeeStrategy.getSellFee(uint256) external returns(uint256) envfree; // GhoToken function _ghoToken.getFacilitatorBucket(address) external returns (uint256, uint256) envfree; function _ghoToken.balanceOf(address) external returns (uint256) envfree; // Harness function getGhoMinted() external returns(uint256) envfree; function getPriceRatio() external returns (uint256) envfree; function getAccruedFees() external returns (uint256) envfree; function balanceOfUnderlying(address) external returns (uint256) envfree; } definition harnessOnlyMethods(method f) returns bool = (f.selector == sig:getAccruedFees().selector || f.selector == sig:getGhoMinted().selector || f.selector == sig:getPriceRatio().selector || f.selector == sig:getExposureCap().selector || f.selector == sig:getGhoMinted().selector || f.selector == sig:getGhoMinted().selector || f.selector == sig:getPriceRatio().selector || f.selector == sig:getUnderlyingAssetUnits().selector || f.selector == sig:getUnderlyingAssetDecimals().selector || f.selector == sig:getAssetPriceInGho(uint256, bool).selector || f.selector == sig:getAssetPriceInGho(uint256, bool).selector || f.selector == sig:getSellFee(uint256).selector || f.selector == sig:getBuyFee(uint256).selector || f.selector == sig:getBuyFeeBP().selector || f.selector == sig:getSellFeeBP().selector || f.selector == sig:getPercMathPercentageFactor().selector || f.selector == sig:balanceOfGho(address).selector || f.selector == sig:getCurrentGhoBalance().selector || f.selector == sig:getCurrentUnderlyingBalance().selector || f.selector == sig:getGhoBalanceOfThis().selector || f.selector == sig:giftGho(address, uint).selector || f.selector == sig:giftUnderlyingAsset(address, uint).selector || f.selector == sig:balanceOfUnderlying(address).selector || f.selector == sig:getCurrentExposure().selector); definition buySellAssetsFunctions(method f) returns bool = (f.selector == sig:buyAsset(uint256,address).selector || f.selector == sig:buyAssetWithSig(address,uint256,address,uint256,bytes).selector || f.selector == sig:sellAsset(uint256,address).selector || f.selector == sig:sellAssetWithSig(address,uint256,address,uint256,bytes).selector); function basicBuySellSetup( env e, address receiver){ require receiver != currentContract; require e.msg.sender != currentContract; require UNDERLYING_ASSET(e) != _ghoToken; } function erc20_transferFrom_assumption(address token, env e, address from, address to, uint256 amount) returns bool { require erc20Helper.tokenBalanceOf(token, from) + erc20Helper.tokenBalanceOf(token, to) <= max_uint256; return _ghoToken.transferFrom(e, from, to, amount); } function erc20_mint_assumption(address token, env e, address account, uint256 amount) { require erc20Helper.tokenBalanceOf(token, account) + amount <= max_uint256; _ghoToken.mint(e, account, amount); } function erc20_transfer_assumption(address token, env e, address to, uint256 amount) returns bool{ require erc20Helper.tokenBalanceOf(token, to) + amount <= max_uint256; return _ghoToken.transfer(e, to, amount); } ================================================ FILE: certora/gsm/specs/GsmMethods/methods_divint_summary.spec ================================================ // The (unverified) summary for OpenZeppelin's `Math.mulDiv`. // Use with care! methods { function Math.mulDiv(uint256 x, uint256 y, uint256 denominator) internal returns (uint256) => mulDivSummary(x, y, denominator); function Math.mulDiv(uint256 x, uint256 y, uint256 denominator, Math.Rounding rounding) internal returns (uint256) => mulDivSummaryRounding(x, y, denominator, rounding); } function mulDivSummary(uint256 x, uint256 y, uint256 denominator) returns uint256 { require denominator > 0; return require_uint256((x*y)/denominator); } function mulDivSummaryRounding(uint256 x, uint256 y, uint256 denominator, Math.Rounding rounding) returns uint256 { if (rounding == Math.Rounding.Up) { require denominator > 0; return require_uint256((x * y + denominator - 1) / denominator); } else { return mulDivSummary(x, y, denominator); } } ================================================ FILE: certora/gsm/specs/GsmMethods/shared.spec ================================================ import "../GsmMethods/methods_base-Martin.spec"; import "../GsmMethods/methods_divint_summary.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; /** * * SHARED RULES * */ /** * * SHARED FUNCTIONALITY * */ // Computes sum of assets of the addresses passed as parameters taking into account that the // some of the addresses may be the same. function assetOfUsers(env e, address sender, address receiver, address originator, mathint ghoDecimals, mathint underlyingAssetUnits) returns mathint { mathint result = getTotalAsset(e, sender, ghoDecimals, underlyingAssetUnits); mathint result1; mathint result2; if (sender != receiver) { result1 = result + getTotalAsset(e, receiver, ghoDecimals, underlyingAssetUnits); } else { result1 = result; } if (sender != originator && receiver != originator) { result2 = result1 + getTotalAsset(e, originator, ghoDecimals, underlyingAssetUnits); } else { result2 = result1; } return result2; } // Returns sum of all assets of the given address function getTotalAsset(env e, address a, mathint ghoDecimals, mathint underlyingAssetUnits) returns mathint { return ghoDecimals*balanceOfUnderlying(e,a) + underlyingAssetUnits*balanceOfGho(e,a); } function functionDispatcher(method f, env e, address receiver, address originator, uint256 amount) { uint256 deadline; bytes signature; calldataarg args; if (f.selector == sig:sellAsset(uint256,address).selector) { sellAsset(e, amount, receiver); } else if (f.selector == sig:buyAssetWithSig(address,uint256,address,uint256,bytes).selector) { buyAssetWithSig(e, originator, amount, receiver, deadline, signature); } else if (f.selector == sig:sellAssetWithSig(address,uint256,address,uint256,bytes).selector) { sellAssetWithSig(e, originator, amount, receiver, deadline, signature); } else if (f.selector == sig:buyAsset(uint256,address).selector) { buyAsset(e, amount, receiver); } else if (f.selector == sig:giftUnderlyingAsset(address, uint).selector) { giftUnderlyingAsset(e, originator, amount); } else if (f.selector == sig:giftGho(address, uint).selector) { giftGho(e, originator, amount); } else { f(e,args); } } ================================================ FILE: certora/gsm/specs/gsm/AssetToGhoInvertibility.spec ================================================ import "../GsmMethods/methods_base.spec"; methods { function _.mulDiv(uint256 x, uint256 y, uint256 denominator) internal => mulDivSummary(x, y, denominator) expect (uint256); function _.mulDiv(uint256 x, uint256 y, uint256 denominator, Math.Rounding rounding) internal => mulDivSummaryWithRounding(x, y, denominator, rounding) expect (uint256); } function mulDivSummary(uint256 x, uint256 y, uint256 denominator) returns uint256 { require denominator > 0; return require_uint256((x * y) / denominator); } function mulDivSummaryWithRounding(uint256 x, uint256 y, uint256 denominator, Math.Rounding rounding) returns uint256 { require denominator > 0; if (rounding == Math.Rounding.Up) { return require_uint256((x * y + denominator - 1) / denominator); } else return require_uint256((x * y) / denominator); } // FULL REPORT AT: https://prover.certora.com/output/17512/c87a46588a694009988c74cd330e3451?anonymousKey=81afc1084fb6e444019f84f769cbce4cd06cdc11 // The view function getGhoAmountForBuyAsset is the inverse of getAssetAmountForBuyAsset /** ******************************************************** ***** BASIC PROPERTIES - SIMILAR TO OTAKAR's RULES ***** ******************************************************** */ // @title actual gho amount returned getAssetAmountForBuyAsset should be less than max gho amount specified by the user // STATUS: PASS // https://prover.certora.com/output/11775/e6a4acd004b6450bbc109f6dc30288ef?anonymousKey=57eb2fef7c06c14a84f14f4e2c1e206f4b884269 rule basicProperty_getAssetAmountForBuyAsset() { env e; require getPriceRatio(e) > 0; require _FixedFeeStrategy.getBuyFeeBP(e) <= 10000; uint256 maxGhoAmount; uint256 actualGhoAmount; _, actualGhoAmount, _, _ = getAssetAmountForBuyAsset(e, maxGhoAmount); assert actualGhoAmount <= maxGhoAmount; } // @title getAssetAmountForBuyAsset should return the same asset and gho amount for an amount of gho suggested as the selling amount // STATUS: PASS // https://prover.certora.com/output/11775/e6a4acd004b6450bbc109f6dc30288ef?anonymousKey=57eb2fef7c06c14a84f14f4e2c1e206f4b884269 rule basicProperty2_getAssetAmountForBuyAsset() { env e; mathint priceRatio = getPriceRatio(e); require priceRatio == 9*10^17 || priceRatio == 10^18 || priceRatio == 5*10^18; mathint uau = _priceStrategy.getUnderlyingAssetUnits(e); uint8 underlyingAssetDecimals; require underlyingAssetDecimals < 25 && underlyingAssetDecimals > 5; require uau == 10^underlyingAssetDecimals; mathint buyFee = _FixedFeeStrategy.getBuyFeeBP(e); require buyFee == 0 || buyFee == 1000 || buyFee == 357 || buyFee == 9000 || buyFee == 10000; uint256 maxGhoAmount; uint256 assetsBought; uint256 assetsBought2; uint256 actualGhoAmount; uint256 actualGhoAmount2; uint256 grossAmount; uint256 grossAmount2; uint256 fee; uint256 fee2; assetsBought, actualGhoAmount, grossAmount, fee = getAssetAmountForBuyAsset(e, maxGhoAmount); assetsBought2, actualGhoAmount2, grossAmount2, fee2 = getAssetAmountForBuyAsset(e, actualGhoAmount); assert assetsBought == assetsBought2 && actualGhoAmount == actualGhoAmount2 && grossAmount == grossAmount2 && fee == fee2; } // @title actual gho amount returned getGhoAmountForBuyAsset should be more than the min amount specified by the user // STATUS: PASS // https://prover.certora.com/output/11775/e6a4acd004b6450bbc109f6dc30288ef?anonymousKey=57eb2fef7c06c14a84f14f4e2c1e206f4b884269 rule basicProperty_getGhoAmountForBuyAsset() { env e; require getPriceRatio(e) > 0; require _FixedFeeStrategy.getBuyFeeBP(e) < 10000; uint256 minAssetAmount; uint256 actualAssetAmount; actualAssetAmount, _, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount); assert minAssetAmount <= actualAssetAmount; } // @title actual gho amount returned getAssetAmountForSellAsset should be more than the min amount specified by the user // STATUS: PASS // https://prover.certora.com/output/11775/e6a4acd004b6450bbc109f6dc30288ef?anonymousKey=57eb2fef7c06c14a84f14f4e2c1e206f4b884269 rule basicProperty_getAssetAmountForSellAsset() { env e; require getPriceRatio(e) > 0; require _FixedFeeStrategy.getSellFeeBP(e) < 10000; uint256 minGhoAmount; uint256 actualGhoAmount; _, actualGhoAmount, _, _ = getAssetAmountForSellAsset(e, minGhoAmount); assert minGhoAmount <= actualGhoAmount; } // @title actual asset amount returned getGhoAmountForSellAsset should be less than the max amount specified by the user // STATUS: PASS // https://prover.certora.com/output/11775/e6a4acd004b6450bbc109f6dc30288ef?anonymousKey=57eb2fef7c06c14a84f14f4e2c1e206f4b884269 rule basicProperty_getGhoAmountForSellAsset() { env e; require getPriceRatio(e) > 0; require _FixedFeeStrategy.getSellFeeBP(e) < 10000; uint256 maxAssetAmount; uint256 actualAssetAmount; actualAssetAmount, _, _, _ = getGhoAmountForSellAsset(e, maxAssetAmount); assert actualAssetAmount <= maxAssetAmount; } // @title getGhoAmountForBuyAsset should return the same amount for an asset amount suggested by it // STATUS: TIMEOUT // https://prover.certora.com/output/11775/ded636a7d0af4862b389cb8c0ae88914?anonymousKey=21e61bef920130667fb07d930f134cd1b4c5027a // rule basicProperty2_getGhoAmountForBuyAsset() { // env e; // mathint priceRatio = getPriceRatio(e); // require priceRatio == 9*10^17 || priceRatio == 10^18 || priceRatio == 5*10^18; // mathint uau = _priceStrategy.getUnderlyingAssetUnits(e); // uint8 underlyingAssetDecimals; // require underlyingAssetDecimals < 25 && underlyingAssetDecimals > 5; // require uau == 10^underlyingAssetDecimals; // mathint buyFee = _FixedFeeStrategy.getBuyFeeBP(e); // require buyFee == 0 || buyFee == 1000 || buyFee == 357 || buyFee == 9000 || buyFee == 9999; // uint256 minAssetAmount; // uint256 assetsBought; uint256 assetsBought2; // uint256 actualGhoAmount; uint256 actualGhoAmount2; // uint256 grossAmount; uint256 grossAmount2; // uint256 fee; uint256 fee2; // assetsBought, actualGhoAmount, grossAmount, fee = getGhoAmountForBuyAsset(e, minAssetAmount); // assetsBought2, actualGhoAmount2, grossAmount2, fee2 = getGhoAmountForBuyAsset(e, assetsBought); // assert assetsBought == assetsBought2 && actualGhoAmount == actualGhoAmount2 && grossAmount == grossAmount2 && fee == fee2; // } /** *********************************** ***** BUY ASSET INVERSE RULE ***** *********************************** */ // @title getAssetAmountForBuyAsset is inverse of getGhoAmountForBuyAsset // STATUS: PASS // https://prover.certora.com/output/11775/e6a4acd004b6450bbc109f6dc30288ef?anonymousKey=57eb2fef7c06c14a84f14f4e2c1e206f4b884269 rule buyAssetInverse_all() { env e; mathint priceRatio = getPriceRatio(e); require priceRatio > 0; mathint uau = _priceStrategy.getUnderlyingAssetUnits(e); uint8 underlyingAssetDecimals; require underlyingAssetDecimals <= 30 && underlyingAssetDecimals >= 1; require uau == 10^underlyingAssetDecimals; require _FixedFeeStrategy.getBuyFeeBP(e) < 5000; uint256 maxGhoAmount; uint256 assetAmount1; uint256 assetAmount2; uint256 gho1; uint256 gho2; uint256 gross1; uint256 gross2; uint256 fee1; uint256 fee2; assetAmount1, gho1, gross1, fee1 = getAssetAmountForBuyAsset(e, maxGhoAmount); assetAmount2, gho2, gross2, fee2 = getGhoAmountForBuyAsset(e, assetAmount1); assert assetAmount1 == assetAmount2, "asset amount"; assert gho1 == gho2, "gho amount"; assert gross1 == gross2, "gross amount"; assert fee1 == fee2, "fee"; } /** ************************************ ***** SELL ASSET INVERSE RULES ***** ************************************ */ // @title getAssetAmountForSellAsset is inverse of getGhoAmountForSellAsset // STATUS: VIOLATED // Value from getGhoAmountForSellAsset can be smaller by 1. // https://prover.certora.com/output/11775/e6a4acd004b6450bbc109f6dc30288ef?anonymousKey=57eb2fef7c06c14a84f14f4e2c1e206f4b884269 // rule sellAssetInverse_gross() { // env e; // require to_mathint(getPriceRatio(e)) > 0; // mathint uau = _priceStrategy.getUnderlyingAssetUnits(e); // uint8 underlyingAssetDecimals; // require underlyingAssetDecimals <= 30 && underlyingAssetDecimals >= 1; // require uau == 10^underlyingAssetDecimals; // require _FixedFeeStrategy.getSellFeeBP(e) < 5000; // uint256 minGhoAmount; // uint256 assetAmount; // uint256 grossAmount; // uint256 grossAmount2; // assetAmount, _, grossAmount, _ = getAssetAmountForSellAsset(e, minGhoAmount); // _, _, grossAmount2, _ = getGhoAmountForSellAsset(e, assetAmount); // assert grossAmount == grossAmount2; // } // @title getAssetAmountForSellAsset is inverse of getGhoAmountForSellAsset // STATUS: VIOLATED // Value from getGhoAmountForSellAsset can be smaller by 1 (the difference is the same as for gross amount - their respective differences are equal to ghoAmount). // https://prover.certora.com/output/11775/e6a4acd004b6450bbc109f6dc30288ef?anonymousKey=57eb2fef7c06c14a84f14f4e2c1e206f4b884269 // rule sellAssetInverse_fee() { // env e; // mathint randomCoefficient; // require randomCoefficient == 5 || randomCoefficient == 9 || randomCoefficient == 1 || randomCoefficient == 25 || randomCoefficient == 10; // require to_mathint(getPriceRatio(e)) == 10^17 * randomCoefficient; // mathint uau = _priceStrategy.getUnderlyingAssetUnits(e); // uint8 underlyingAssetDecimals; // require underlyingAssetDecimals < 25 && underlyingAssetDecimals > 5; // require uau == 10^underlyingAssetDecimals; // require _FixedFeeStrategy.getSellFeeBP(e) < 5000; // uint256 minGhoAmount; // uint256 assetAmount; // uint256 fee; // uint256 fee2; // assetAmount, _, _, fee = getAssetAmountForSellAsset(e, minGhoAmount); // _, _, _, fee2 = getGhoAmountForSellAsset(e, assetAmount); // assert fee == fee2; // } // @title getAssetAmountForSellAsset is inverse of getGhoAmountForSellAsset // STATUS: PASS // https://prover.certora.com/output/11775/d1d79caba11d4708a64c6273b914af83?anonymousKey=77944410212cb77cd8de01ce41b9f5a7f52780fd rule sellAssetInverse_all() { env e; require 10^16 <= getPriceRatio(e) && getPriceRatio(e) <= 10^20; mathint uau = _priceStrategy.getUnderlyingAssetUnits(e); uint8 underlyingAssetDecimals; require underlyingAssetDecimals <= 30 && underlyingAssetDecimals >= 1; require uau == 10^underlyingAssetDecimals; require _FixedFeeStrategy.getSellFeeBP(e) < 5000; uint256 minGhoAmount; uint256 assetAmount; uint256 assetAmount2; uint256 ghoAmount; uint256 ghoAmount2; uint256 grossAmount; uint256 grossAmount2; uint256 fee; uint256 fee2; assetAmount, ghoAmount, grossAmount, fee = getAssetAmountForSellAsset(e, minGhoAmount); assetAmount2, ghoAmount2, grossAmount2, fee2 = getGhoAmountForSellAsset(e, assetAmount); assert assetAmount == assetAmount2, "asset amount"; assert ghoAmount == ghoAmount2, "gho amount"; assert grossAmount2 <= grossAmount && to_mathint(grossAmount) <= grossAmount2 + 1, "gross amount off by at most 1"; assert fee2 <= fee && to_mathint(fee) <= fee2 + 1, "fee by at most 1"; assert (fee == fee2) <=> (grossAmount == grossAmount2), "fee off by 1 iff gross amount off by 1"; } ================================================ FILE: certora/gsm/specs/gsm/FixedFeeStrategy.spec ================================================ // verifies properties of FixedFeestrategy import "../GsmMethods/aave_fee_limits.spec"; import "../GsmMethods/methods_divint_summary.spec"; methods { function getBuyFeeBP() external returns uint256 envfree; function getSellFeeBP() external returns uint256 envfree; function getPercMathPercentageFactor() external returns uint256 envfree; function getBuyFee(uint256) external returns uint256 envfree; function getSellFee(uint256) external returns uint256 envfree; function getGrossAmountFromTotalBought(uint256) external returns (uint256)envfree; function getGrossAmountFromTotalSold(uint256) external returns (uint256)envfree; } // @title get{Buy|Sell}Fee(x) <= x // STATUS: PASS // https://prover.certora.com/output/11775/2daedeb4c01a4354bc7889ffd9f4ec25?anonymousKey=eee3f23fb4011ab65bf9a0096bc855dcfb58a780 rule feeIsLowerThanGrossAmount() { env e; feeLimits(e); uint amount; uint buyFee = getBuyFee(amount); assert buyFee <= amount; uint sellFee = getSellFee(amount); assert sellFee <= amount; } // @title get{Buy|Sell}Fee is monotone. x1 <= x2 -> get{Buy|Sell}Fee(x1) <= get{Buy|Sell}Fee(x2) // STATUS: PASS // https://prover.certora.com/output/11775/2daedeb4c01a4354bc7889ffd9f4ec25?anonymousKey=eee3f23fb4011ab65bf9a0096bc855dcfb58a780 rule getFeeIsMonotone() { env e; feeLimits(e); uint amount1; uint amount2; require amount1 < amount2; assert getBuyFee(amount1) <= getBuyFee(amount2); assert getSellFee(amount1) <= getSellFee(amount2); } // @title getGrossAmountFromTotalBought is monotone. // STATUS: PASS // https://prover.certora.com/output/11775/2daedeb4c01a4354bc7889ffd9f4ec25?anonymousKey=eee3f23fb4011ab65bf9a0096bc855dcfb58a780 rule getGrossAmountFromTotalBought_isMonotoneInTotalAmount() { env e; feeLimits(e); uint amount1; uint amount2; require amount1 < amount2; assert getGrossAmountFromTotalBought(amount1) <= getGrossAmountFromTotalBought(amount2); } // @title getGrossAmountFromTotalSold is monotone. // STATUS: PASS // https://prover.certora.com/output/11775/2daedeb4c01a4354bc7889ffd9f4ec25?anonymousKey=eee3f23fb4011ab65bf9a0096bc855dcfb58a780 rule getGrossAmountFromTotalSold_isMonotoneInTotalAmount() { env e; feeLimits(e); uint amount1; uint amount2; //require amount1 * getPercMathPercentageFactor() < max_uint256; //otherwise the result of the function overflows. //require amount2 * getPercMathPercentageFactor() < max_uint256; require amount1 < amount2; assert getGrossAmountFromTotalSold(amount1) <= getGrossAmountFromTotalSold(amount2); } function differsByAtMostOne(mathint x, mathint y) returns bool { mathint diff = x - y; return -1 <= diff && diff <= 1; } // @title getGrossAmountFromTotalBought function calculates gross amount correctly // STATUS: PASS // https://prover.certora.com/output/11775/2daedeb4c01a4354bc7889ffd9f4ec25?anonymousKey=eee3f23fb4011ab65bf9a0096bc855dcfb58a780 rule byuFeeAndInverse0() { env e; feeLimits(e); uint amount; uint buyFee = getBuyFee(amount); mathint sum = amount + buyFee; require sum < max_uint256; uint amount2 = getGrossAmountFromTotalBought(assert_uint256(sum)); //assert differsByAtMostOne(amount, amount2); assert amount == amount2; } // @title getGrossAmountFromTotalBought is inverse to getBuyFee. // STATUS: PASS // https://prover.certora.com/output/11775/2daedeb4c01a4354bc7889ffd9f4ec25?anonymousKey=eee3f23fb4011ab65bf9a0096bc855dcfb58a780 rule byuFeeAndInverse1() { env e; feeLimits(e); uint amount; uint buyFee = getBuyFee(amount); mathint sum = amount + buyFee; require sum < max_uint256; uint amount2 = getGrossAmountFromTotalBought(assert_uint256(sum)); assert differsByAtMostOne(amount, amount2); //assert amount == amount2; } // @title getGrossAmountFromTotalBought is inverse to getBuyFee. // STATUS: PASS // https://prover.certora.com/output/11775/2daedeb4c01a4354bc7889ffd9f4ec25?anonymousKey=eee3f23fb4011ab65bf9a0096bc855dcfb58a780 rule getGrossAmountFromTotalBought_isCorrect() { env e; feeLimits(e); uint GhoAmount; uint grossAmount = getGrossAmountFromTotalBought(GhoAmount); uint buyFee = getBuyFee(grossAmount); mathint reallySold = grossAmount + buyFee; assert differsByAtMostOne(reallySold, GhoAmount); } // @title getGrossAmountFromTotalSold is inverse to getSellFee. // STATUS: PASS // https://prover.certora.com/output/11775/2daedeb4c01a4354bc7889ffd9f4ec25?anonymousKey=eee3f23fb4011ab65bf9a0096bc855dcfb58a780 rule getGrossAmountFromTotalSold_isCorrect() { env e; feeLimits(e); uint ghoToReceive; uint grossAmount = getGrossAmountFromTotalSold(ghoToReceive); uint sellFee = getSellFee(grossAmount); mathint reallyReceived = grossAmount - sellFee; assert assert_uint256(reallyReceived) == ghoToReceive; } // @title getSellFee never reverts. // STATUS: PASS // https://prover.certora.com/output/40748/1b5b658d0a4b49c3844cff4efd397cf0?anonymousKey=cab52c3a200bf976702ffb1c232760ab249e3e2e rule GetSellFeeNeverReverts() { env e; feeLimits(e); uint amount; uint sellFee = getSellFee@withrevert(amount); assert !lastReverted; } // @title getBuyFee never reverts. // STATUS: PASS // https://prover.certora.com/output/40748/18c697324f4c4c858a7aaa966f0eed79?anonymousKey=7724c74905fb397687da58d101235307fa1b7109 // rule GetBuyFeeNeverReverts() { env e; feeLimits(e); uint amount; uint buyFee = getBuyFee@withrevert(amount); assert !lastReverted; } // // @title No method can change fees. // // STATUS: PASS // // https://prover.certora.com/output/11775/2daedeb4c01a4354bc7889ffd9f4ec25?anonymousKey=eee3f23fb4011ab65bf9a0096bc855dcfb58a780 // rule noMethodCanChangeFees(method f) // { // env e; // calldataarg args; // uint sellFeeBefore = getSellFeeBP(); // uint buyFeeBefore = getBuyFeeBP(); // f(e,args); // assert getSellFeeBP() == sellFeeBefore; // assert getBuyFeeBP() == buyFeeBefore; // } ================================================ FILE: certora/gsm/specs/gsm/FixedPriceStrategy.spec ================================================ // import "../GsmMethods/methods_base.spec"; methods { function getAssetPriceInGho(uint256, bool) external returns (uint256) envfree; function getGhoPriceInAsset(uint256, bool) external returns (uint256) envfree; function _.mulDiv(uint256 x, uint256 y, uint256 denominator) internal => mulDivSummary(x, y, denominator) expect (uint256); function _.mulDiv(uint256 x, uint256 y, uint256 denominator, Math.Rounding rounding) internal => mulDivSummaryWithRounding(x, y, denominator, rounding) expect (uint256); } function mulDivSummary(uint256 x, uint256 y, uint256 denominator) returns uint256 { require denominator > 0; return require_uint256((x * y) / denominator); } function mulDivSummaryWithRounding(uint256 x, uint256 y, uint256 denominator, Math.Rounding rounding) returns uint256 { require denominator > 0; if (rounding == Math.Rounding.Up) { return require_uint256((x * y + denominator - 1) / denominator); } else return require_uint256((x * y) / denominator); } // Full report at https://prover.certora.com/output/17512/ed7722cf57e54d228e6f3487bd15661e?anonymousKey=4bf315b7502b8c338c4b4cd8bcfe7ae9eb858782 // @title getAssetPirce is monotonic // STATUS: PASS // https://prover.certora.com/output/11775/924ac54bf2c645cfb2509898c5893163?anonymousKey=ffc27e798c9c5333b00dc04acbc527dccd3c11d5 rule getAssetPriceIsMonotone() { env e; uint256 amount1; uint256 amount2; assert amount1 > amount2 => getAssetPriceInGho(amount1, false) >= getAssetPriceInGho(amount2, false); assert amount1 > amount2 => getAssetPriceInGho(amount1, true) >= getAssetPriceInGho(amount2, true); } // @title getGhoPirce is monotonic // STATUS: PASS // https://prover.certora.com/output/11775/924ac54bf2c645cfb2509898c5893163?anonymousKey=ffc27e798c9c5333b00dc04acbc527dccd3c11d5 rule getGhoPriceIsMonotone() { env e; uint256 amount1; uint256 amount2; assert amount1 > amount2 => getGhoPriceInAsset(amount1, false) >= getGhoPriceInAsset(amount2, false); assert amount1 > amount2 => getGhoPriceInAsset(amount1, true) >= getGhoPriceInAsset(amount2, true); } // ******************** // // *** ERROR BOUNDS *** // // ******************** // /* assetAmount - _underlyingAssetUnits/PRICE_RATIO - 1 <= getGhoPriceInAsset(getAssetPriceInGho(assetAmount, false), -) <= assetAmount assetAmount <= getGhoPriceInAsset(getAssetPriceInGho(assetAmount, true), -) <= assetAmount + _underlyingAssetUnits/PRICE_RATIO + 1 */ // @title getGhoPriceInAsset and getAssetPriceInGho are inverse of each other // STATUS: PASS // https://prover.certora.com/output/11775/924ac54bf2c645cfb2509898c5893163?anonymousKey=ffc27e798c9c5333b00dc04acbc527dccd3c11d5 rule assetToGhoAndBackErrorBounds() { env e; uint256 originalAssetAmount; mathint underlyingAssetUnits = getUnderlyingAssetUnits(e); require underlyingAssetUnits > 0; // safe as this number should be equal to 10 ** underlyingAssetDecimals uint256 priceRatio = getPriceRatio(e); require priceRatio > 0; mathint maxError = underlyingAssetUnits/priceRatio +1; mathint newAssetAmountDD = getGhoPriceInAsset(getAssetPriceInGho(originalAssetAmount, false), false); mathint newAssetAmountDU = getGhoPriceInAsset(getAssetPriceInGho(originalAssetAmount, false), true); mathint newAssetAmountUD = getGhoPriceInAsset(getAssetPriceInGho(originalAssetAmount, true), false); mathint newAssetAmountUU = getGhoPriceInAsset(getAssetPriceInGho(originalAssetAmount, true), true); assert originalAssetAmount - maxError <= newAssetAmountDD && newAssetAmountDD <= to_mathint(originalAssetAmount), "rounding down then down"; assert originalAssetAmount - maxError <= newAssetAmountDU && newAssetAmountDU <= to_mathint(originalAssetAmount), "rounding down then up"; assert to_mathint(originalAssetAmount) <= newAssetAmountUD && newAssetAmountUD <= originalAssetAmount + maxError, "rounding up then down"; assert to_mathint(originalAssetAmount) <= newAssetAmountUU && newAssetAmountUU <= originalAssetAmount + maxError, "rounding up then up"; } /* ghoAmount - PRICE_RATIO / _underlyingAssetUnits - 1 <= getAssetPriceInGho(getGhoPriceInAsset(ghoAmount,false),-) <= ghoAmount ghoAmount <= getAssetPriceInGho(getGhoPriceInAsset(ghoAmount,false),-) <= ghoAmount + PRICE_RATIO / _underlyingAssetUnits + 1 */ // @title getGhoPriceInAsset and getAssetPriceInGho are inverse of each other // STATUS: PASS // https://prover.certora.com/output/11775/924ac54bf2c645cfb2509898c5893163?anonymousKey=ffc27e798c9c5333b00dc04acbc527dccd3c11d5 rule ghoToAssetAndBackErrorBounds() { env e; uint256 originalAmountOfGho; mathint underlyingAssetUnits = getUnderlyingAssetUnits(e); require underlyingAssetUnits > 0; // safe as this number should be equal to 10 ** underlyingAssetDecimals uint256 priceRatio = getPriceRatio(e); require priceRatio > 0; mathint maxError = priceRatio/underlyingAssetUnits +1; mathint newGhoAmountDD = getAssetPriceInGho(getGhoPriceInAsset(originalAmountOfGho, false), false); mathint newGhoAmountDU = getAssetPriceInGho(getGhoPriceInAsset(originalAmountOfGho, false), true); mathint newGhoAmountUD = getAssetPriceInGho(getGhoPriceInAsset(originalAmountOfGho, true), false); mathint newGhoAmountUU = getAssetPriceInGho(getGhoPriceInAsset(originalAmountOfGho, true), true); assert originalAmountOfGho - maxError <= newGhoAmountDD && newGhoAmountDD <= to_mathint(originalAmountOfGho), "rounding down then down"; assert originalAmountOfGho - maxError <= newGhoAmountDU && newGhoAmountDU <= to_mathint(originalAmountOfGho), "rounding down then up"; assert to_mathint(originalAmountOfGho) <= newGhoAmountUD && newGhoAmountUD <= originalAmountOfGho + maxError, "rounding up then down"; assert to_mathint(originalAmountOfGho) <= newGhoAmountUU && newGhoAmountUU <= originalAmountOfGho + maxError, "rounding up then up"; } ================================================ FILE: certora/gsm/specs/gsm/OracleSwapFreezer.spec ================================================ // verifies properties of OracleSwapFreezer methods { function getFreezeBound() external returns (uint128, uint128) envfree; function getUnfreezeBound() external returns (uint128, uint128) envfree; function validateBounds(uint128,uint128,uint128,uint128,bool) external returns bool envfree; function _.hasRole(bytes32, address) external => hasRole expect bool; function _.getAssetPrice(address) external => CONSTANT; function _.getPriceOracle() external => CONSTANT; function _.getIsSeized() external => CONSTANT; function _.SWAP_FREEZER_ROLE() external => CONSTANT; function _.getIsFrozen() external => CONSTANT; } function boundsAreValid() returns bool { uint128 freezeLower; uint128 freezeUpper; uint128 unFreezeLower; uint128 unFreezeUpper; freezeLower, freezeUpper = getFreezeBound(); unFreezeLower, unFreezeUpper = getUnfreezeBound(); return validateBounds(freezeLower, freezeUpper, unFreezeLower, unFreezeUpper, true); } ghost bool hasRole; // @title Freeze action is executable under specified conditions // Freeze action is executable if GSM is not seized, not frozen and price is outside of the freeze bounds // STATUS: PASS // https://prover.certora.com/output/40748/9802a015eadc415ab6e449384f60e944?anonymousKey=e43bbc0fc9409b164be311adbadaa6d473db1a00 rule freezeExecutable() { env e; uint256 price = getPrice(e); require hasRole == true; require !isFrozen(e) && !isSeized(e); uint128 freezeLower; uint128 freezeUpper; freezeLower, freezeUpper = getFreezeBound(); require price < require_uint256(freezeLower) || price > require_uint256(freezeUpper); assert price != 0 => getAction(e) == 1; //represents the freeze action } // @title Unfreeze action is executable under specified conditions //Unfreeze action is executable if GSM is not seized, frozen, unfreezing is allowed and price is inside the unfreeze bounds // STATUS: PASS // https://prover.certora.com/output/11775/184ae7de9b56415088118d8e6d027ff3?anonymousKey=4f8fcda010d0dbba62ed4fd5663650233a3f7969 rule unfreezeExecutable() { env e; uint256 price = getPrice(e); require hasRole == true; require boundsAreValid(); require isFrozen(e) && !isSeized(e); uint128 unFreezeLower; uint128 unFreezeUpper; unFreezeLower, unFreezeUpper = getUnfreezeBound(); require price >= require_uint256(unFreezeLower) && price <= require_uint256(unFreezeUpper); assert getCanUnfreeze(e) => getAction(e) == 2; //represents the unfreeze action } // @title Unfreeze boundaries are contained in freeze boundaries //Unfreeze boundaries are "contained" in freeze boundaries, where freezeLowerBound < unfreezeLowerBound and unfreezeUpperBound < freezeUpperBound // STATUS: PASS // https://prover.certora.com/output/11775/184ae7de9b56415088118d8e6d027ff3?anonymousKey=4f8fcda010d0dbba62ed4fd5663650233a3f7969 rule boundsAreContained() { env e; require boundsAreValid(); uint128 freezeLower; uint128 freezeUpper; freezeLower, freezeUpper = getFreezeBound(); uint128 unfreezeLower; uint128 unfreezeUpper; unfreezeLower, unfreezeUpper = getUnfreezeBound(); assert freezeLower < unfreezeLower && unfreezeUpper < freezeUpper; } // @title freeze and unfreeze are never executable at the same time. //there should never be an oracle price that could allow both freeze and unfreeze // STATUS: PASS // https://prover.certora.com/output/11775/184ae7de9b56415088118d8e6d027ff3?anonymousKey=4f8fcda010d0dbba62ed4fd5663650233a3f7969 rule freezeAndUnfreezeAreExclusive() { env e; require boundsAreValid(); assert !(isFreezeAllowed(e) && isUnfreezeAllowed(e)); } ================================================ FILE: certora/gsm/specs/gsm/balances-buy.spec ================================================ import "../GsmMethods/erc20.spec"; import "../GsmMethods/methods_divint_summary.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; using DiffHelper as diffHelper; // ========================= Buying ============================== // The results are available in this run: // https://prover.certora.com/output/40748/8433d4a7f3194f019a7ae98ddb872694/?anonymousKey=55effda6e5861528384a148b2b714a373ce5a637 // Issue: "Inconsistency in the amount of GHO user asks to sell and how much GHO is deducted from user account at the end." // Rules broken: "R4_sellGhoUpdatesAssetBuyerGhoBalanceGe" // Example property: """ // Case 1. // Let GHO amount `g = 6`, price ratio `PR = 4`, underlying asset units // `UAU = 1`, buy fee in BP `buyFeeBP = 0`. The change in GHO balance // is 8. // // Case 2. // Let GHO amount `g = 3*10^36+5`, price ratio `PR = 1*10^36+2`, // underlying asset units `UAU = 1`, buy fee in BP `buyFeeBP = 0. The // change in GHO balance is 2*10^36+4 // """ // // Description: """ // GSM provides a way to swap an underlying asset against GHO. // Technically this is implemented by providing the following API functions for // swapping, where the argument `a` is in asset: // - `buyAsset(a)` // - `sellAsset(a)` // // In case the user wants to instead buy or sell GHO, there is no direct // way to achieve this with the API. When buying, respectively selling, // GHO, the user needs to first call `getAssetAmountForSellAsset(g)`, // respectively `getAssetAmountForSellAsset(g)`, to obtain the amount of // asset `a` that, when provided to the correct swap function, executes // the buy or sell based on the amount `g` in GHO. It is not always // possible to specify an amount of asset that will result in exactly // `g` GHO being bought or sold. For example, if the price of one asset // is 10 GHO, and the number of decimals in GHO and asset are the same, // it is not possible to sell exactly 1 GHO using the API: assuming no // fees, one would presumably sell either 0 GHO or 10 GHO. Depending on // the properties of asset, the fees, and the amount `g` of GHO, the // code might result in more or less than `g` GHO being sold. // """ // Mitigation / Fix: """Refactor the API, fix rounding directions. Fix // #168""" // Severity: "High" // Note: from https://github.com/Certora/gho-gsm/pull/10 // Issue: // User may pay more GHO than the maximum they provided // Description: // The user may ask the amount of assets to provide for `buyAsset` by calling // `getAssetAmountForBuyAsset(max)`, where `max` is the maximum amount of GHO // user is willing to pay. When the return value is provided to `buyAsset`, it // is possible that the user is charged more than `max` GHO. // Note: from https://github.com/Certora/gho-gsm/pull/12 // Issue: // The exact amount of GHO returned by `getAssetAmountForBuyAsset(max)` can be higher than `max` // Description: // The user may ask the amount of assets to provide for `buyAsset` by calling // `getAssetAmountForBuyAsset(max)`, where `max` is the maximum amount of GHO // user is willing to pay. One of the return values of // `getAssetAmountForBuyAsset` is the exact amount of GHO that will be deducted. // This value can be higher than `max`. // Note: from https://github.com/Certora/gho-gsm/pull/12 // @Title The exact amount of GHO returned by `getAssetAmountForBuyAsset(maxGho)` is less than or equal to `maxGho` // . -[getAssetAmountForBuyAsset(x)]-> . // exactGHO <= goWithFee // where exactGHO is the 2nd return value of getAssetAmountForBuyAsset // Holds. // (1) rule R1_getAssetAmountForBuyAssetRV2 { env e; feeLimits(e); priceLimits(e); // Note: not required? require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation require GHO_TOKEN(e) != UNDERLYING_ASSET(e); // This is inflation prevention (and also avoids an overflow) uint256 ghoWithFee; uint256 assetsToBuy; uint256 exactGHO; address receiver; // For debugging: uint256 priceRatio = getPriceRatio(e); uint256 underlyingAssetUnits = getUnderlyingAssetUnits(e); _, exactGHO, _, _ = getAssetAmountForBuyAsset(e, ghoWithFee); assert exactGHO <= ghoWithFee; } // @Title The exact amount of GHO returned by `getAssetAmountForBuyAsset(maxGho)` can be less than `maxGho` // The second return value of `getAssetAmountForBuyAsset(x)` can be less // than x. // . -[getAssetAmountForBuyAsset(x)]-> . // exactGHO <= goWithFee // where exactGHO is the 2nd return value of getAssetAmountForBuyAsset // Holds // (1a) rule R1a_getAssetAmountForBuyAssetRV2 { env e; feeLimits(e); priceLimits(e); // Note: not required? require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation require GHO_TOKEN(e) != UNDERLYING_ASSET(e); // This is inflation prevention (and also avoids an overflow) uint256 ghoWithFee; uint256 assetsToBuy; uint256 exactGHO; address receiver; // For debugging: uint256 priceRatio = getPriceRatio(e); uint256 underlyingAssetUnits = getUnderlyingAssetUnits(e); _, exactGHO, _, _ = getAssetAmountForBuyAsset(e, ghoWithFee); satisfy exactGHO < ghoWithFee; } // @Title The difference in the exact amount of GHO returned by `getAssetAmountForBuyAsset(maxGho)` and `maxGho` can be greater than 10^13 // (1-UB) rule R1UB_getAssetAmountForBuyAssetRV2_UB { env e; feeLimits(e); priceLimits(e); // Note: not required? require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation require GHO_TOKEN(e) != UNDERLYING_ASSET(e); // This is inflation prevention (and also avoids an overflow) uint256 ghoWithFee; uint256 assetsToBuy; uint256 exactGHO; address receiver; // For debugging: uint256 priceRatio = getPriceRatio(e); uint256 underlyingAssetUnits = getUnderlyingAssetUnits(e); _, exactGHO, _, _ = getAssetAmountForBuyAsset(e, ghoWithFee); uint256 N = 10^13; satisfy !diffHelper.differsByAtMostN(e, exactGHO, ghoWithFee, N); } // @Title The exact amount of GHO returned by `getAssetAmountForBuyAsset(x)` matches the GHO amount deduced from user at `buyAsset` // . -[getAssetAmountForBuyAsset(x)]-> . -[buyAsset(exactGHO)]-> . // ghoBalance_1 - ghoBalance_2 = exactGHO // where exactGHO is the 2nd return value of getAssetAmountForBuyAsset // Holds. // (2) rule R2_getAssetAmountForBuyAssetRV_vs_GhoBalance { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation require GHO_TOKEN(e) != UNDERLYING_ASSET(e); // This is inflation prevention (and also avoids an overflow) uint256 ghoWithFee; uint256 assetsToBuy; uint256 exactGHO; address receiver; // For debugging: uint256 priceRatio = getPriceRatio(e); uint256 underlyingAssetUnits = getUnderlyingAssetUnits(e); assetsToBuy, exactGHO, _, _ = getAssetAmountForBuyAsset(e, ghoWithFee); uint256 buyerGhoBalanceBefore = balanceOfGho(e, e.msg.sender); require assetsToBuy <= max_uint128; buyAsset(e, assert_uint128(assetsToBuy), receiver); uint256 buyerGhoBalanceAfter = balanceOfGho(e, e.msg.sender); mathint balanceDiff = buyerGhoBalanceBefore - buyerGhoBalanceAfter; assert to_mathint(exactGHO) == balanceDiff; } // @Title The asset amount deduced from user's account at `buyAsset(minAssets)` is at least `minAssets` // -[buyAsset]-> // assetsToBuy <= |buyerAssetBalanceAfter - buyerAssetBalanceBefore| // (3) // Holds. rule R3_buyAssetUpdatesAssetBuyerAssetBalanceLe { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation require GHO_TOKEN(e) != UNDERLYING_ASSET(e); // This is inflation prevention (and also avoids an overflow) uint256 assetsToBuy; address receiver; require receiver != currentContract; // Otherwise GHO is burned but asset value doesn't increase. (This is only a problem for my bookkeeping) // For debugging: uint256 priceRatio = getPriceRatio(e); uint256 underlyingAssetUnits = getUnderlyingAssetUnits(e); require assetsToBuy <= max_uint128; uint256 receiverAssetBalanceBefore = balanceOfUnderlying(e, receiver); buyAsset(e, assert_uint128(assetsToBuy), receiver); uint256 receiverAssetBalanceAfter = balanceOfUnderlying(e, receiver); uint256 balanceDiff = require_uint256(receiverAssetBalanceAfter - receiverAssetBalanceBefore); assert assetsToBuy <= balanceDiff; } // @Title The asset amount deduced from user's account at `buyAsset(minAssets)` can be more than `minAssets` // -[buyAsset]-> // assetsToBuy < |buyerAssetBalanceAfter - buyerAssetBalanceBefore| // (3a) // Holds. rule R3a_buyAssetUpdatesAssetBuyerAssetBalanceLt { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation require GHO_TOKEN(e) != UNDERLYING_ASSET(e); // This is inflation prevention (and also avoids an overflow) uint256 assetsToBuy; address receiver; require receiver != currentContract; // Otherwise GHO is burned but asset value doesn't increase. (This only a problem for my bookkeeping) // For debugging: uint256 priceRatio = getPriceRatio(e); uint256 underlyingAssetUnits = getUnderlyingAssetUnits(e); require assetsToBuy <= max_uint128; uint256 receiverAssetBalanceBefore = balanceOfUnderlying(e, receiver); buyAsset(e, assert_uint128(assetsToBuy), receiver); uint256 receiverAssetBalanceAfter = balanceOfUnderlying(e, receiver); uint256 balanceDiff = require_uint256(receiverAssetBalanceAfter - receiverAssetBalanceBefore); satisfy assetsToBuy < balanceDiff; } // @Title The difference between asset amount deduced from user's account at `buyAsset(minAssets)` and `minAssets` can be more than 10^10 // (3-UB) // Holds. I.e., the error can be at least 10^10 rule R3UB_buyAssetUpdatesAssetBuyerAssetBalanceUB { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation require GHO_TOKEN(e) != UNDERLYING_ASSET(e); // This is inflation prevention (and also avoids an overflow) uint256 assetsToBuy; address receiver; require receiver != currentContract; // Otherwise GHO is burned but asset value doesn't increase. (This is only a problem for my bookkeeping) // For debugging: uint256 priceRatio = getPriceRatio(e); uint256 underlyingAssetUnits = getUnderlyingAssetUnits(e); require assetsToBuy <= max_uint128; uint256 receiverAssetBalanceBefore = balanceOfUnderlying(e, receiver); buyAsset(e, assert_uint128(assetsToBuy), receiver); uint256 receiverAssetBalanceAfter = balanceOfUnderlying(e, receiver); uint256 balanceDiff = require_uint256(receiverAssetBalanceAfter - receiverAssetBalanceBefore); uint256 N = 10^10; satisfy !diffHelper.differsByAtMostN(e, assetsToBuy, balanceDiff, N); } // @Title The amount of GHO deduced from user's account at `buyAsset` is less than or equal to the value passed to `getAssetAmountForBuyAsset` // . -[getAssetAmountForBuyAsset(x)]-> . -[buyAsset]-> . // buyerGhoBalanceBefore - buyerGhoBalanceAfter <= goWithFee // (4) // Holds. rule R4_sellGhoUpdatesAssetBuyerGhoBalanceGe { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation require GHO_TOKEN(e) != UNDERLYING_ASSET(e); // This is inflation prevention (and also avoids an overflow) uint256 ghoWithFee; uint256 assetsToBuy; address receiver; // For debugging: uint256 priceRatio = getPriceRatio(e); uint256 underlyingAssetUnits = getUnderlyingAssetUnits(e); assetsToBuy, _, _, _ = getAssetAmountForBuyAsset(e, ghoWithFee); require assetsToBuy <= max_uint128; uint256 buyerGhoBalanceBefore = balanceOfGho(e, e.msg.sender); buyAsset(e, assert_uint128(assetsToBuy), receiver); uint256 buyerGhoBalanceAfter = balanceOfGho(e, e.msg.sender); mathint balanceDiff = buyerGhoBalanceBefore - buyerGhoBalanceAfter; assert to_mathint(ghoWithFee) >= balanceDiff; } // @Title The amount of GHO deduced from user's account at `buyAsset` can be less than the value passed to `getAssetAmountForBuyAsset` // . -[getAssetAmountForBuyAsset(x)]-> . -[buyAsset]-> . // \exists x . buyerGhoBalanceBefore - buyerGhoBalanceAfter < goWithFee // (4a) // Holds: https://prover.certora.com/output/40748/c44b117fccd94853a171b7d88ec93815/?anonymousKey=2ba26dfa6fbbde84014221222db1cbf0b8badc39 rule R4a_sellGhoUpdatesAssetBuyerGhoBalanceGt { env e; feeLimits(e); priceLimits(e); uint256 ghoWithFee; uint256 assetsToBuy; address receiver; // For debugging: uint256 priceRatio = getPriceRatio(e); uint256 underlyingAssetUnits = getUnderlyingAssetUnits(e); require receiver != e.msg.sender; // Otherwise the sold GHO will just come back to me. assetsToBuy, _, _, _ = getAssetAmountForBuyAsset(e, ghoWithFee); require assetsToBuy <= max_uint128; uint256 buyerGhoBalanceBefore = balanceOfGho(e, e.msg.sender); buyAsset(e, assert_uint128(assetsToBuy), receiver); uint256 buyerGhoBalanceAfter = balanceOfGho(e, e.msg.sender); mathint balanceDiff = buyerGhoBalanceBefore - buyerGhoBalanceAfter; satisfy to_mathint(ghoWithFee) > balanceDiff; } // @Title The difference in the amount of GHO deduced from user's account at `buyAsset` and the value passed to `getAssetAmountForBuyAsset` can be more than 10^13 // . -[getAssetAmountForBuyAsset(x)]-> . -[buyAsset]-> . // max |buyerGhoBalanceBefore - buyerGhoBalanceAfter - goWithFee| // (4-UB) rule R4UB_sellGhoUpdatesAssetBuyerGhoBalanceUB { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation require GHO_TOKEN(e) != UNDERLYING_ASSET(e); // This is inflation prevention (and also avoids an overflow) uint256 ghoWithFee; uint256 assetsToBuy; address receiver; // For debugging: uint256 priceRatio = getPriceRatio(e); uint256 underlyingAssetUnits = getUnderlyingAssetUnits(e); assetsToBuy, _, _, _ = getAssetAmountForBuyAsset(e, ghoWithFee); require assetsToBuy <= max_uint128; uint256 buyerGhoBalanceBefore = balanceOfGho(e, e.msg.sender); buyAsset(e, assert_uint128(assetsToBuy), receiver); uint256 buyerGhoBalanceAfter = balanceOfGho(e, e.msg.sender); uint256 balanceDiff = require_uint256(buyerGhoBalanceBefore - buyerGhoBalanceAfter); uint256 N = 10^13; satisfy !diffHelper.differsByAtMostN(e, ghoWithFee, balanceDiff, N); } ================================================ FILE: certora/gsm/specs/gsm/balances-sell.spec ================================================ import "../GsmMethods/erc20.spec"; import "../GsmMethods/methods_divint_summary.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; using DiffHelper as diffHelper; // ========================= Selling ============================== // The user wants to buy GHO and asks how much asset should be sold. Fees are // not included in user's GHO buying order. // // https://prover.certora.com/output/40748/82b017e6272940189f89a69de371f386/?anonymousKey=f4acb19d25cf33db1c2473eab71b6a8f1e53181d // @Title The exact amount of GHO returned by `getAssetAmountForSellAsset(minGho)` is at least `minGho` // Check that recipient's GHO balance is updated correctly // User wants to buy `minGhoToSend` GHO. // User asks for the assets required: `(assetsToSpend, ghoToReceive, ghoToSpend, fee) := getAssetAmountForSellAsset(minGhoToReceive)` // Let balance difference of the recipient be balanceDiff. // (1): minGhoToReceive <= ghoToReceive // Holds. rule R1_getAssetAmountForSellAsset_arg_vs_return { env e; feeLimits(e); priceLimits(e); uint256 minGhoToReceive; uint256 ghoToReceive; _, ghoToReceive, _, _ = getAssetAmountForSellAsset(e, minGhoToReceive); assert minGhoToReceive <= ghoToReceive; } // @Title The exact amount of GHO returned by `getAssetAmountForSellAsset(minGho)` can be greater than `minGho` // Shows < // (1a) // Holds. rule R1a_buyGhoUpdatesGhoBalanceCorrectly1 { env e; feeLimits(e); priceLimits(e); uint256 minGhoToReceive; uint256 ghoToReceive; _, _, ghoToReceive, _ = getAssetAmountForSellAsset(e, minGhoToReceive); satisfy minGhoToReceive < ghoToReceive; } // @Title The exact amount of GHO returned by `getAssetAmountForSellAsset` is equal to the amount obtained after `sellAsset` // getAssetAmountForSellAsset returns exactGhoToReceive. Does this match the exact GHO received after the corresponding sellAsset? // Holds. // (2) rule R2_getAssetAmountForSellAsset_sellAsset_eq { env e; feeLimits(e); priceLimits(e); uint256 minGhoToReceive; uint256 ghoToReceive; uint256 assetsToSell; require currentContract.UNDERLYING_ASSET(e) != currentContract.GHO_TOKEN(e); // Otherwise we only measure the fee. address recipient; require recipient != currentContract; // Otherwise the balance grows because of the fees. assetsToSell, ghoToReceive, _, _ = getAssetAmountForSellAsset(e, minGhoToReceive); uint256 ghoBalanceBefore = balanceOfGho(e, recipient); sellAsset(e, assetsToSell, recipient); uint256 ghoBalanceAfter = balanceOfGho(e, recipient); uint256 balanceDiff = require_uint256(ghoBalanceAfter - ghoBalanceBefore); assert balanceDiff == ghoToReceive; } // @Title The asset amount deduced from the user's account at `sellAsset(_, maxAsset, _)` is at most `maxAsset` // Check that user's asset balance is // decreased correctly. Shows >= // (3) // STATUS: TIMEOUT // https://prover.certora.com/output/11775/9e60de94fefe4aa5b20bd4ae1342dfcb?anonymousKey=a94125580ee2a1b2d268bb476ff90664f53b30e4 rule R3_sellAssetUpdatesAssetBalanceCorrectly { env e; feeLimits(e); priceLimits(e); uint128 assets; address seller = e.msg.sender; address recipient; require e.msg.sender != currentContract; require currentContract.UNDERLYING_ASSET(e) != currentContract.GHO_TOKEN(e); // Inflation prevention! uint256 balanceBefore = balanceOfUnderlying(e, seller); sellAsset(e, assets, recipient); uint256 balanceAfter = balanceOfUnderlying(e, seller); mathint balanceDiff = balanceBefore - balanceAfter; assert to_mathint(assets) >= balanceDiff; } // @Title The asset amount deduced from the user's account at `sellAsset(_, maxAsset, _)` can be less than `maxAsset` // Check that user's asset balance is // decreased correctly. Shows > // (3a) rule R3a_sellAssetUpdatesAssetBalanceCorrectly { env e; feeLimits(e); priceLimits(e); uint128 assets; address seller = e.msg.sender; address recipient; require e.msg.sender != currentContract; require currentContract.UNDERLYING_ASSET(e) != currentContract.GHO_TOKEN(e); // Inflation prevention! uint256 balanceBefore = balanceOfUnderlying(e, seller); sellAsset(e, assets, recipient); uint256 balanceAfter = balanceOfUnderlying(e, seller); mathint balanceDiff = balanceBefore - balanceAfter; satisfy to_mathint(assets) > balanceDiff; } // @Title The GHO amount added to the user's account at `sellAsset` is at least the value `x` passed to `getAssetAmountForSellAsset(x)` // (4) // STATUS: TIMEOUT // https://prover.certora.com/output/11775/04f9aa998c0045839ed5e0fa8f17465d?anonymousKey=ba48d972104e53d04391136fa6e98e2eaeaf7d56 rule R4_buyGhoUpdatesGhoBalanceCorrectly { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; require currentContract.UNDERLYING_ASSET(e) != currentContract.GHO_TOKEN(e); // Inflation prevention address seller = e.msg.sender; address recipient; require recipient != currentContract; // Otherwise the balance grows because of the fees. uint256 minGhoToSend; uint256 assetsToSpend; assetsToSpend, _, _, _ = getAssetAmountForSellAsset(e, minGhoToSend); require assetsToSpend < max_uint128; uint256 balanceBefore = balanceOfGho(e, recipient); sellAsset(e, assert_uint128(assetsToSpend), recipient); uint256 balanceAfter = balanceOfGho(e, recipient); require balanceAfter >= balanceBefore; // No overflow uint256 balanceDiff = require_uint256(balanceAfter - balanceBefore); assert minGhoToSend <= balanceDiff; } // @Title The GHO amount added to the user's account at `sellAsset` can be greater than the value `x` passed to `getAssetAmountForSellAsset(x)` // Show that the GHO amount requested by the user to be transferred to the // recipient can be less than what the recipient receives, even when fees are considered. // (4a) rule R4a_buyGhoAmountGtGhoBalanceChange { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; require currentContract.UNDERLYING_ASSET(e) != currentContract.GHO_TOKEN(e); // Inflation prevention address seller = e.msg.sender; address recipient; require recipient != currentContract; // Otherwise the balance grows because of the fees. uint256 minGhoToSend; uint256 assetsToSpend; assetsToSpend, _, _, _ = getAssetAmountForSellAsset(e, minGhoToSend); require assetsToSpend < max_uint128; uint256 balanceBefore = balanceOfGho(e, recipient); sellAsset(e, assert_uint128(assetsToSpend), recipient); uint256 balanceAfter = balanceOfGho(e, recipient); require balanceAfter >= balanceBefore; // No overflow uint256 balanceDiff = require_uint256(balanceAfter - balanceBefore); satisfy minGhoToSend < balanceDiff; } ================================================ FILE: certora/gsm/specs/gsm/fees-buy.spec ================================================ import "../GsmMethods/erc20.spec"; import "../GsmMethods/methods_divint_summary.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; using DiffHelper as diffHelper; // Issue: // Inconsistency in the reported and accrued fees when buying asset // Description: // When a swap takes place in GSM, the contract may collect a fee. The fee is // represented in basic points. When a concrete transaction takes place the fee in // basic points is used to obtain a concrete fee in GHO. The API exposes the fee // in three different ways. Directly based on BP through `getBuyFee(x)`, as the fee // reported by `getAssetAmountForBuyAsset(x)`, and as the fee accrued through // `buyAsset(a)`. The fee reported by `getBuyFee(x)` can be less than, greater // than, or equal to the fee accrued by `buyAsset(a)`. Similarly, the fee // reported by `getAssetAmountForBuyAsset(x)` can be less than, greater than, or // equal to the fee accrued by `buyAsset` // Mitigation/Fix: // TODO // Note: from https://github.com/Certora/gho-gsm/pull/10 // ========================= Buying ============================== // A successful run: // https://prover.certora.com/output/40748/c6f0cc0e2d794e2c997ce7ec2f37ca48/?anonymousKey=5af25f5d1ed5c68b6b1db47fdc5e04bc673752b6 // @title The fee reported by `getBuyFee` is greater than or equal to the fee reported by `getAssetAmountForBuyAsset` // getBuyFee -(>=)-> getAssetAmountForBuyAsset // Shows >= // Holds // (1) rule R1_getBuyFeeGeGetAssetAmountForBuyAsset { env e; feeLimits(e); priceLimits(e); uint128 ghoAmount; uint256 estimatedBuyFee = getBuyFee(e, ghoAmount); require estimatedBuyFee + ghoAmount <= max_uint256; uint256 amountOfGhoToSell = assert_uint256(estimatedBuyFee + ghoAmount); uint256 fee; _, _, _, fee = getAssetAmountForBuyAsset(e, amountOfGhoToSell); assert estimatedBuyFee >= fee; } // @title The fee reported by `getBuyFee` can be greater than the fee reported by `getAssetAmountForBuyAsset` // getBuyFee -(>=)-> getAssetAmountForBuyAsset. // Shows > // (1a) // Holds. rule R1a_getBuyFeeNeGetAssetAmountForBuyAsset { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation uint128 ghoAmount; uint256 estimatedBuyFee = getBuyFee(e, ghoAmount); require estimatedBuyFee + ghoAmount <= max_uint256; uint256 amountOfGhoToSell = assert_uint256(estimatedBuyFee + ghoAmount); uint256 fee; _, _, _, fee = getAssetAmountForBuyAsset(e, amountOfGhoToSell); satisfy estimatedBuyFee > fee; } // @title The fee reported by `getBuyFee` can differ from the fee reported by `getAssetAmountForBuyAsset` by at least 10^3 // getBuyFee -(>=, ?)-> getAssetAmountForBuyAsset // (1-UB) // Holds. rule R1UB_getBuyFeeGeGetAssetAmountForBuyAssetUB { env e; feeLimits(e); priceLimits(e); uint128 ghoAmount; uint256 estimatedBuyFee = getBuyFee(e, ghoAmount); require estimatedBuyFee + ghoAmount <= max_uint256; uint256 amountOfGhoToSell = assert_uint256(estimatedBuyFee + ghoAmount); uint256 fee; _, _, _, fee = getAssetAmountForBuyAsset(e, amountOfGhoToSell); uint N = 10^3; satisfy !diffHelper.differsByAtMostN(e, fee, estimatedBuyFee, N); } // @title The fee reported by `getAssetAmountForBuyAsset` is equal to the fee accrued by `buyAsset` // getAssetAmountForBuyAsset -(==)-> buyAsset // Show == // (2) // Holds. rule R2_getAssetAmountForBuyAssetNeBuyAssetFee { env e; feeLimits(e); priceLimits(e); address receiver; uint256 preAccruedFees = currentContract._accruedFees; uint256 amountOfGhoToSell; uint256 estimatedFee; uint256 assetAmount; assetAmount, _, _, estimatedFee = getAssetAmountForBuyAsset(e, amountOfGhoToSell); require assetAmount <= max_uint128; // No overflow buyAsset(e, assert_uint128(assetAmount), receiver); uint256 postAccruedFees = currentContract._accruedFees; uint256 actualFee = assert_uint256(postAccruedFees - preAccruedFees); assert estimatedFee == actualFee; } // @title The fee reported by `getAssetAmountForBuyAsset` is equal to the fee accrued by `getBuyFee` // getAssetAmountForBuyAssetFee -(==)-> getBuyFee // Shows == // Holds. // (3) rule R3_getAssetAmountForBuyAssetFeeEqGetBuyFee { env e; feeLimits(e); priceLimits(e); uint256 estimatedFee; uint256 grossGho; uint256 amountOfGhoToSellWithFee; _, _, grossGho, estimatedFee = getAssetAmountForBuyAsset(e, amountOfGhoToSellWithFee); uint256 fee = getBuyFee(e, grossGho); assert fee == estimatedFee; } // @title The fee reported by `getBuyFee` is greater than or equal to the fee accrued by `buyAsset` // getBuyFee -(>=)-> buyAsset // shows that the estimated fee >= actual fee // Holds. // (4) rule R4_estimatedBuyFeeGeActualBuyFee { env e; feeLimits(e); priceLimits(e); uint128 ghoAmount; address receiver; uint256 preAccruedFees = currentContract._accruedFees; uint256 estimatedBuyFee = getBuyFee(e, ghoAmount); require estimatedBuyFee + ghoAmount <= max_uint256; uint256 amountOfGhoToSell = assert_uint256(estimatedBuyFee + ghoAmount); uint256 assetAmount; assetAmount, _, _, _ = getAssetAmountForBuyAsset(e, amountOfGhoToSell); require assetAmount <= max_uint128; // No overflow buyAsset(e, assert_uint128(assetAmount), receiver); uint256 postAccruedFees = currentContract._accruedFees; uint256 actualFee = assert_uint256(postAccruedFees - preAccruedFees); assert estimatedBuyFee >= actualFee; } // @title The fee reported by `getBuyFee` can be greater than the fee deduced by `buyAsset` // getBuyFee -(>=)-> buyAsset // shows that the estimated fee can be > than actual fee (but isn't necessarily always) // Holds. // (4a) rule R4a_estimatedBuyFeeGtActualBuyFee { env e; feeLimits(e); priceLimits(e); uint256 priceRatio = getPriceRatio(e); uint128 ghoAmount; address receiver; uint256 preAccruedFees = currentContract._accruedFees; uint256 estimatedBuyFee = getBuyFee(e, ghoAmount); require estimatedBuyFee + ghoAmount <= max_uint256; uint256 amountOfGhoToSell = assert_uint256(estimatedBuyFee + ghoAmount); uint256 assetAmount; assetAmount, _, _, _ = getAssetAmountForBuyAsset(e, amountOfGhoToSell); require assetAmount <= max_uint128; // No overflow buyAsset(e, assert_uint128(assetAmount), receiver); uint256 postAccruedFees = currentContract._accruedFees; uint256 actualFee = assert_uint256(postAccruedFees - preAccruedFees); satisfy estimatedBuyFee > actualFee; } ================================================ FILE: certora/gsm/specs/gsm/fees-sell.spec ================================================ import "../GsmMethods/erc20.spec"; import "../GsmMethods/methods_divint_summary.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; using DiffHelper as diffHelper; // Study how well the estimated fees match the actual fees. // Issue: "Inconsistency in the reported and accrued fees when selling asset" // Rules broken: "R3_estimatedSellFeeCanBeHigherThanActualSellFee" // Example property: """""" // // Description: """ // When a swap takes place in GSM, the contract may collect a fee. The // fee is represented in basic points. When a concrete transaction // takes place the fee in basic points is used to obtain a concrete fee // in GHO. // The API exposes the fee in three different ways. Directly based on BP // through `getSellFee(x)`, as the fee reported by // `getAssetAmountForSellAsset(x)`, and as the fee accrued through // `sellAsset(a)`. The fee reported by `getSellFee(x)` can be less than, // greater than, or equal to the fee accrued by `sellAsset(a)`. // """ // Mitigation / Fix: """TODO""" // Severity: "Medium" // Note: from https://github.com/Certora/gho-gsm/pull/10 // ========================= Selling ============================== // The results are available in this run: // https://prover.certora.com/output/40748/de214c37fe2549d0b11461087d191d9f?anonymousKey=2711135cf621015f610eabd5a685b8f82e47ff67 // @Title The fee reported by `getAssetAmountForSellAsset` is greater than or equal to the fee reported by `getSellFee` // getAssetAmountForSellAssetFee -(>=)-> getSellFee // Shows >= // (1) // rule R1_getAssetAmountForSellAssetFeeGeGetSellFee { env e; feeLimits(e); priceLimits(e); uint256 estimatedFee; uint256 amountOfGhoToBuy; uint256 exactAmountOfGhoToReceive; _, exactAmountOfGhoToReceive, _, estimatedFee = getAssetAmountForSellAsset(e, amountOfGhoToBuy); uint256 fee = getSellFee(e, amountOfGhoToBuy); assert estimatedFee >= fee; } // @Title The fee reported by `getAssetAmountForSellAsset` can be greater than the fee reported by `getSellFee` // getAssetAmountForSellAssetFee -(>=)-> getSellFee // Shows > // (1a) // rule R1a_getAssetAmountForSellAssetFeeGeGetSellFee { env e; feeLimits(e); priceLimits(e); uint256 estimatedFee; uint256 amountOfGhoToBuy; uint256 exactAmountOfGhoToReceive; _, exactAmountOfGhoToReceive, _, estimatedFee = getAssetAmountForSellAsset(e, amountOfGhoToBuy); uint256 fee = getSellFee(e, amountOfGhoToBuy); satisfy estimatedFee > fee; } // @Title The fee reported by `getAssetAmountForSellAsset` can be greater than or equal to the fee accrued by `sellAsset` // getAssetAmountForSellAsset -(>=)-> sellAsset // Shows >= // (2) rule R2_getAssetAmountForSellAssetVsActualSellFee { env e; feeLimits(e); priceLimits(e); uint256 assetAmount; uint256 estimatedFee; uint256 amountOfGhoToBuy; address receiver; uint256 preAccruedFees = currentContract._accruedFees; assetAmount, _, _, estimatedFee = getAssetAmountForSellAsset(e, amountOfGhoToBuy); sellAsset(e, require_uint128(assetAmount), receiver); uint256 postAccruedFees = currentContract._accruedFees; uint256 actualFee = require_uint256(postAccruedFees - preAccruedFees); assert estimatedFee >= actualFee; } // @Title The fee reported by `getAssetAmountForSellAsset` may differ from the fee accrued by `sellAsset` // getAssetAmountForSellAsset -(>=)-> sellAsset // Shows > // (2a) rule R2a_getAssetAmountForSellAssetNeActualSellFee { env e; feeLimits(e); priceLimits(e); uint256 assetAmount; uint256 estimatedFee; uint256 amountOfGhoToBuy; address receiver; uint256 preAccruedFees = currentContract._accruedFees; assetAmount, _, _, estimatedFee = getAssetAmountForSellAsset(e, amountOfGhoToBuy); sellAsset(e, require_uint128(assetAmount), receiver); uint256 postAccruedFees = currentContract._accruedFees; uint256 actualFee = require_uint128(postAccruedFees - preAccruedFees); satisfy estimatedFee > actualFee; } // @Title The fee reported by `getSellFee` is less than or equal to the fee accrued by `sellAsset` // getSellFee -(<=)-> sellAsset // shows <= // (3) // rule R3_estimatedSellFeeCanBeHigherThanActualSellFee { env e; feeLimits(e); priceLimits(e); uint128 ghoAmount; address receiver; uint256 preAccruedFees = currentContract._accruedFees; uint256 estimatedSellFee = getSellFee(e, ghoAmount); require ghoAmount <= max_uint128; require estimatedSellFee <= max_uint128; uint256 assetAmount; assetAmount, _, _, _ = getAssetAmountForSellAsset(e, ghoAmount); sellAsset(e, require_uint128(assetAmount), receiver); uint256 postAccruedFees = currentContract._accruedFees; uint256 actualFee = require_uint256(postAccruedFees - preAccruedFees); assert estimatedSellFee <= actualFee; } // @Title The fee reported by `getSellFee` can be less than the fee deduced by `sellAsset` // getSellFee -(<=)-> sellAsset // shows < // (3a) // rule R3a_estimatedSellFeeCanBeLowerThanActualSellFee { env e; feeLimits(e); priceLimits(e); uint128 ghoAmount; address receiver; uint256 preAccruedFees = currentContract._accruedFees; uint256 estimatedSellFee = getSellFee(e, ghoAmount); require ghoAmount <= max_uint128; require estimatedSellFee <= max_uint128; uint256 assetAmount; assetAmount, _, _, _ = getAssetAmountForSellAsset(e, ghoAmount); sellAsset(e, require_uint128(assetAmount), receiver); uint256 postAccruedFees = currentContract._accruedFees; uint256 actualFee = require_uint256(postAccruedFees - preAccruedFees); satisfy estimatedSellFee < actualFee; } // @Title The fee reported by `getSellFee` is less than or equal to the fee reported by `getAssetAmountForSellAsset` // getSellFee -(<=)-> getAssetAmountForSellAsset // (4) rule R4_getSellFeeVsgetAssetAmountForSellAsset { env e; feeLimits(e); priceLimits(e); uint256 ghoAmount; uint256 estimatedSellFee; uint256 sellFee; estimatedSellFee = getSellFee(e, ghoAmount); _, _, _, sellFee = getAssetAmountForSellAsset(e, ghoAmount); assert estimatedSellFee <= sellFee; } // @Title The fee reported by `getSellFee` can be less than the fee reported by `getAssetAmountForSellAsset` // getSellFee -(<=)-> getAssetAmountForSellAsset // (4a) // Shows < rule R4a_getSellFeeVsgetAssetAmountForSellAsset { env e; feeLimits(e); priceLimits(e); uint256 ghoAmount; uint256 estimatedSellFee; uint256 sellFee; estimatedSellFee = getSellFee(e, ghoAmount); _, _, _, sellFee = getAssetAmountForSellAsset(e, ghoAmount); satisfy estimatedSellFee < sellFee; } ================================================ FILE: certora/gsm/specs/gsm/getAmount_properties.spec ================================================ import "../GsmMethods/methods_base.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; import "../GsmMethods/methods_divint_summary.spec"; // @title The amount of asset returned is less than or equal to given param // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getAssetAmountForBuyAsset_correctness() { env e; feeLimits(e); priceLimits(e); uint maxToGive; require maxToGive > 0; uint suggestedAssetToBuy; suggestedAssetToBuy, _, _, _ = getAssetAmountForBuyAsset(e, maxToGive); uint reallyPaid; _, reallyPaid, _, _ = getGhoAmountForBuyAsset(e, suggestedAssetToBuy); assert reallyPaid <= maxToGive; } // @title The amount of gho returned is greater than or equal to given param // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getGhoAmountForBuyAsset_correctness() { env e; feeLimits(e); priceLimits(e); uint256 minAssetAmount; uint suggestedAssetToBuy; suggestedAssetToBuy, _, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount); assert suggestedAssetToBuy >= minAssetAmount; } // @title The amount of gho returned is greater than or equal to given param within bound of 1 // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getGhoAmountForBuyAsset_correctness_bound1() { env e; feeLimits(e); priceLimits(e); uint256 minAssetAmount; uint suggestedAssetToBuy; suggestedAssetToBuy, _, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount); assert require_uint256(suggestedAssetToBuy + 1) >= minAssetAmount; } // @title The amount of asset returned is greater than or equal to given param. // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getAssetAmountForSellAsset_correctness() { env e; feeLimits(e); priceLimits(e); uint minimumToReceive; require minimumToReceive > 0; uint suggestedAssetToSell; suggestedAssetToSell, _, _, _ = getAssetAmountForSellAsset(e, minimumToReceive); uint reallyReceived; _, reallyReceived, _, _ = getGhoAmountForSellAsset(e, suggestedAssetToSell); assert reallyReceived >= minimumToReceive; } // @title The amount of gho returned is less than or equal to given param. // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getGhoAmountForSellAsset_correctness() { env e; feeLimits(e); priceLimits(e); uint maxAssetAmount; uint suggestedAssetToSell; suggestedAssetToSell, _, _, _ = getGhoAmountForSellAsset(e, maxAssetAmount); assert suggestedAssetToSell <= maxAssetAmount; } // @title getAssetAmountForBuyAsset returns value that is as close as possible to user specified amount. // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getAssetAmountForBuyAsset_optimality() { env e; feeLimits(e); priceLimits(e); uint maxGhoToGive; uint suggestedAssetToBuy; suggestedAssetToBuy, _, _, _ = getAssetAmountForBuyAsset(e, maxGhoToGive); uint suggestedGhoToPay; _, suggestedGhoToPay, _, _ = getGhoAmountForBuyAsset(e, suggestedAssetToBuy); uint maxAssetCouldBuy; uint couldBuyAsset; uint couldPayGho; couldBuyAsset, couldPayGho, _, _ = getGhoAmountForBuyAsset(e, maxAssetCouldBuy); require couldPayGho <= maxGhoToGive; require couldPayGho >= suggestedGhoToPay; assert couldBuyAsset <= suggestedAssetToBuy; } // @title getGhoAmountForBuyAsset returns value that is as close as possible to user specified amount. // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getGhoAmountForBuyAsset_optimality() { env e; feeLimits(e); priceLimits(e); uint minAssetToBuy; uint suggestedAssetToBuy; uint suggestedGhoToSpend; suggestedAssetToBuy, suggestedGhoToSpend, _, _ = getGhoAmountForBuyAsset(e, minAssetToBuy); uint min2AssetsToBuy; uint couldBuy; uint couldPay; couldBuy, couldPay, _, _ = getGhoAmountForBuyAsset(e, min2AssetsToBuy); require couldBuy >= minAssetToBuy; //require couldPay >= suggestedGhoToPay; assert couldPay >= suggestedGhoToSpend; } // @title getGhoAmountForSellAsset returns value that is as close as possible to user specified amount. // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getGhoAmountForSellAsset_optimality() { env e; feeLimits(e); priceLimits(e); uint maxAssetToSell; uint suggestedAssetToSell; uint suggestedGhoToGain; suggestedAssetToSell, suggestedGhoToGain, _, _ = getGhoAmountForSellAsset(e, maxAssetToSell); uint maxAssetToSell2; uint couldSell; uint couldGain; couldSell, couldGain, _, _ = getGhoAmountForSellAsset(e, maxAssetToSell2); require couldSell <= maxAssetToSell; //require couldPay >= suggestedGhoToPay; assert suggestedGhoToGain >= couldGain; } // @title getAssetAmountForSellAsset returns value that is as close as possible to user specified amount. // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getAssetAmountForSellAsset_optimality() { // proves that if user wants to receive at least X gho // and the system tells them to sell Y assets, // then there is no amount W < Y that would already provide X gho. env e; feeLimits(e); priceLimits(e); uint wantsToReceive; //6 uint suggestedAssetToSell; suggestedAssetToSell, _, _, _ = getAssetAmountForSellAsset(e, wantsToReceive); //2 uint reallySold; //1 uint reallyReceived; _, reallyReceived, _, _ = getGhoAmountForSellAsset(e, reallySold); require reallyReceived >= wantsToReceive; assert suggestedAssetToSell <= reallySold; } // @title The first two return values of getAssetAmountForBuyAsset are univalent (https://en.wikipedia.org/wiki/Binary_relation#Specific_types_of_binary_relations) // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getAssetAmountForBuyAsset_funcProperty_LR() { // if (A, B, _, _) = getAssetAmountForBuyAsset(X) then B is function of A env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAssetToBuy1; uint totalPay1; suggestedAssetToBuy1, totalPay1, _, _ = getAssetAmountForBuyAsset(e, amount1); uint256 amount2; uint suggestedAssetToBuy2; uint totalPay2; suggestedAssetToBuy2, totalPay2, _, _ = getAssetAmountForBuyAsset(e, amount2); require suggestedAssetToBuy1 == suggestedAssetToBuy2; assert totalPay1 == totalPay2; } // @title The first two return values of getAssetAmountForBuyAsset are univalent (https://en.wikipedia.org/wiki/Binary_relation#Specific_types_of_binary_relations) // STATUS: TIMEOUT // https://prover.certora.com/output/11775/ca6d0e522361477cb6a74761b7ff087f?anonymousKey=c514848ed2a5ef6b6762574f2f9f0a30a3f5f57f rule getAssetAmountForBuyAsset_funcProperty_RL() { // if (A, B, _, _) = getAssetAmountForBuyAsset(X) then B is function of A env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAssetToBuy1; uint totalPay1; suggestedAssetToBuy1, totalPay1, _, _ = getAssetAmountForBuyAsset(e, amount1); uint256 amount2; uint suggestedAssetToBuy2; uint totalPay2; suggestedAssetToBuy2, totalPay2, _, _ = getAssetAmountForBuyAsset(e, amount2); require totalPay1 == totalPay2; assert suggestedAssetToBuy1 == suggestedAssetToBuy2; } // @title The first two return values of getGhoAmountForBuyAsset are univalent (https://en.wikipedia.org/wiki/Binary_relation#Specific_types_of_binary_relations) // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getGhoAmountForBuyAsset_funcProperty() { // if (A, B, _, _) = getGhoAmountForBuyAsset(X) then B is function of A env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAssetToBuy1; uint totalPay1; suggestedAssetToBuy1, totalPay1, _, _ = getGhoAmountForBuyAsset(e, amount1); uint256 amount2; uint suggestedAssetToBuy2; uint totalPay2; suggestedAssetToBuy2, totalPay2, _, _ = getGhoAmountForBuyAsset(e, amount2); assert (suggestedAssetToBuy1 == suggestedAssetToBuy2) == (totalPay1 == totalPay2); } // @title The first two return values of getGhoAmountForBuyAsset are univalent (https://en.wikipedia.org/wiki/Binary_relation#Specific_types_of_binary_relations) // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getGhoAmountForBuyAsset_funcProperty_LR() { // if (A, B, _, _) = getGhoAmountForBuyAsset(X) then B is function of A env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAssetToBuy1; uint totalPay1; suggestedAssetToBuy1, totalPay1, _, _ = getGhoAmountForBuyAsset(e, amount1); uint256 amount2; uint suggestedAssetToBuy2; uint totalPay2; suggestedAssetToBuy2, totalPay2, _, _ = getGhoAmountForBuyAsset(e, amount2); require suggestedAssetToBuy1 == suggestedAssetToBuy2; assert totalPay1 == totalPay2; } // @title The first two return values of getGhoAmountForBuyAsset are univalent (https://en.wikipedia.org/wiki/Binary_relation#Specific_types_of_binary_relations) // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getGhoAmountForBuyAsset_funcProperty_RL() { // if (A, B, _, _) = getGhoAmountForBuyAsset(X) then B is function of A env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAssetToBuy1; uint totalPay1; suggestedAssetToBuy1, totalPay1, _, _ = getGhoAmountForBuyAsset(e, amount1); uint256 amount2; uint suggestedAssetToBuy2; uint totalPay2; suggestedAssetToBuy2, totalPay2, _, _ = getGhoAmountForBuyAsset(e, amount2); require totalPay1 == totalPay2; assert suggestedAssetToBuy1 == suggestedAssetToBuy2; } // @title The first two return values of getAssetAmountForSellAsset are univalent (https://en.wikipedia.org/wiki/Binary_relation#Specific_types_of_binary_relations) // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getAssetAmountForSellAsset_funcProperty() { // if (A, B, _, _) = getAssetAmountForSellAsset(X) then B is function of A env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAsset1; uint totalPay1; suggestedAsset1, totalPay1, _, _ = getAssetAmountForSellAsset(e, amount1); uint256 amount2; uint suggestedAsset2; uint totalPay2; suggestedAsset2, totalPay2, _, _ = getAssetAmountForSellAsset(e, amount2); assert (suggestedAsset1 == suggestedAsset2) == (totalPay1 == totalPay2); } // @title The first two return values of getAssetAmountForSellAsset are univalent (https://en.wikipedia.org/wiki/Binary_relation#Specific_types_of_binary_relations) // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getAssetAmountForSellAsset_funcProperty_LR() { // if (A, B, _, _) = getAssetAmountForSellAsset(X) then B is function of A env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAsset1; uint totalPay1; suggestedAsset1, totalPay1, _, _ = getAssetAmountForSellAsset(e, amount1); uint256 amount2; uint suggestedAsset2; uint totalPay2; suggestedAsset2, totalPay2, _, _ = getAssetAmountForSellAsset(e, amount2); require suggestedAsset1 == suggestedAsset2; assert totalPay1 == totalPay2; } // @title The first two return values of getAssetAmountForSellAsset are univalent (https://en.wikipedia.org/wiki/Binary_relation#Specific_types_of_binary_relations) // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getAssetAmountForSellAsset_funcProperty_RL() { // if (A, B, _, _) = getAssetAmountForSellAsset(X) then B is function of A env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAsset1; uint totalPay1; suggestedAsset1, totalPay1, _, _ = getAssetAmountForSellAsset(e, amount1); uint256 amount2; uint suggestedAsset2; uint totalPay2; suggestedAsset2, totalPay2, _, _ = getAssetAmountForSellAsset(e, amount2); require totalPay1 == totalPay2; assert suggestedAsset1 == suggestedAsset2; } // @title The first two return values of getGhoAmountForSellAsset are univalent (https://en.wikipedia.org/wiki/Binary_relation#Specific_types_of_binary_relations) // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getGhoAmountForSellAsset_funcProperty_LR() { // if (A, B, _, _) = getGhoAmountForSellAsset(X) then B is function of A env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAsset1; uint totalPay1; suggestedAsset1, totalPay1, _, _ = getGhoAmountForSellAsset(e, amount1); uint256 amount2; uint suggestedAsset2; uint totalPay2; suggestedAsset2, totalPay2, _, _ = getGhoAmountForSellAsset(e, amount2); require suggestedAsset1 == suggestedAsset2; assert totalPay1 == totalPay2; } // @title The first two return values of getGhoAmountForSellAsset are univalent (https://en.wikipedia.org/wiki/Binary_relation#Specific_types_of_binary_relations) // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getGhoAmountForSellAsset_funcProperty_RL() { // if (A, B, _, _) = getGhoAmountForSellAsset(X) then B is function of A env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAsset1; uint totalPay1; suggestedAsset1, totalPay1, _, _ = getGhoAmountForSellAsset(e, amount1); uint256 amount2; uint suggestedAsset2; uint totalPay2; suggestedAsset2, totalPay2, _, _ = getGhoAmountForSellAsset(e, amount2); require totalPay1 == totalPay2; assert suggestedAsset1 == suggestedAsset2; } // @title The first two return values of getGhoAmountForSellAsset are univalent (https://en.wikipedia.org/wiki/Binary_relation#Specific_types_of_binary_relations) // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getGhoAmountForSellAsset_funcProperty() { // if (A, B, _, _) = getGhoAmountForSellAsset(X) then B is function of A env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAsset1; uint totalPay1; suggestedAsset1, totalPay1, _, _ = getGhoAmountForSellAsset(e, amount1); uint256 amount2; uint suggestedAsset2; uint totalPay2; suggestedAsset2, totalPay2, _, _ = getGhoAmountForSellAsset(e, amount2); assert (suggestedAsset1 == suggestedAsset2) == (totalPay1 == totalPay2); } // @title getGhoAmountForBuyAsset is additive. Making two small transactions x1, x2, is less favourable for the user than making (x1+x2) // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getGhoAmountForBuyAsset_aditivity() { env e; feeLimits(e); priceLimits(e); uint256 minAssetAmount1; uint bought1; uint paid1; bought1, paid1, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount1); uint256 minAssetAmount2; uint bought2; uint paid2; bought2, paid2, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount2); require require_uint256(bought1 + bought2) > 0; uint256 minAssetAmount3; uint bought3; uint paid3; bought3, paid3, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount3); require require_uint256(bought1 + bought2) >= bought3; assert require_uint256(paid1 + paid2) >= paid3; } // @title getAssetAmountForBuyAsset is additive. Making two small transactions x1, x2, is less favourable for the user than making (x1+x2) // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getAssetAmountForBuyAsset_aditivity() { env e; feeLimits(e); priceLimits(e); uint256 maxGhoAmount1; uint bought1; uint paid1; bought1, paid1, _, _ = getAssetAmountForBuyAsset(e, maxGhoAmount1); uint256 maxGhoAmount2; uint bought2; uint paid2; bought2, paid2, _, _ = getAssetAmountForBuyAsset(e, maxGhoAmount2); require require_uint256(bought1 + bought2) > 0; uint256 maxGhoAmount3; uint bought3; uint paid3; bought3, paid3, _, _ = getAssetAmountForBuyAsset(e, maxGhoAmount3); require require_uint256(bought1 + bought2) == bought3; assert require_uint256(paid1 + paid2) >= paid3; } // @title getGhoAmountForSellAsset is additive. Making two small transactions x1, x2, is less favourable for the user than making (x1+x2) // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getGhoAmountForSellAsset_aditivity() { env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAsset1; uint totalGained1; suggestedAsset1, totalGained1, _, _ = getGhoAmountForSellAsset(e, amount1); uint256 amount2; uint suggestedAsset2; uint totalGained2; suggestedAsset2, totalGained2, _, _ = getGhoAmountForSellAsset(e, amount2); require require_uint256(suggestedAsset1 + suggestedAsset2) > 0; uint256 amount3; uint suggestedAsset3; uint totalGained3; suggestedAsset3, totalGained3, _, _ = getGhoAmountForSellAsset(e, amount3); require require_uint256(suggestedAsset1 + suggestedAsset2) <= suggestedAsset3; assert require_uint256(totalGained1 + totalGained2) <= totalGained3; } // @title getAssetAmountForSellAsset is additive. Making two small transactions x1, x2, is less favourable for the user than making (x1+x2) // STATUS: PASS // https://prover.certora.com/output/11775/414e746701a349e2bbacc696e0fb5446?anonymousKey=1ee0516abf9c3e609824cfac3893e3a34033f15e rule getAssetAmountForSellAsset_aditivity() { env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAsset1; uint totalGained1; suggestedAsset1, totalGained1, _, _ = getAssetAmountForSellAsset(e, amount1); uint256 amount2; uint suggestedAsset2; uint totalGained2; suggestedAsset2, totalGained2, _, _ = getAssetAmountForSellAsset(e, amount2); require require_uint256(suggestedAsset1 + suggestedAsset2) > 0; uint256 amount3; uint suggestedAsset3; uint totalGained3; suggestedAsset3, totalGained3, _, _ = getAssetAmountForSellAsset(e, amount3); require require_uint256(suggestedAsset1 + suggestedAsset2) <= suggestedAsset3; assert require_uint256(totalGained1 + totalGained2) <= totalGained3; } ================================================ FILE: certora/gsm/specs/gsm/gho-gsm-2.spec ================================================ import "../GsmMethods/shared.spec"; using GhoToken as _ghoTokenHook; using DummyERC20B as UNDERLYING_ASSET; using FixedPriceStrategyHarness as _priceStrategy; using FixedFeeStrategyHarness as _FixedFeeStrategy; methods { // priceStrategy function _priceStrategy.getAssetPriceInGho(uint256, bool) external returns(uint256) envfree; function _priceStrategy.getUnderlyingAssetUnits() external returns(uint256) envfree; function _priceStrategy.getUnderlyingAssetDecimals() external returns(uint256) envfree; // feeStrategy function _FixedFeeStrategy.getBuyFeeBP() external returns(uint256) envfree; function _FixedFeeStrategy.getSellFeeBP() external returns(uint256) envfree; } // @title Rule checks that _accruedFees should be <= ghotoken.balanceof(this) with an exception of the function distributeFeesToTreasury(). // STATUS: PASS // https://prover.certora.com/output/11775/281e0b05ac0345edb1d398dcbc329c19?anonymousKey=376f01ddc0cf54741e33c334e83547bb12adba23 rule accruedFeesLEGhoBalanceOfThis(method f) filtered { f -> !f.isView && !harnessOnlyMethods(f) } { env e; calldataarg args; require(getAccruedFee(e) <= getGhoBalanceOfThis(e)); require(e.msg.sender != currentContract); require(UNDERLYING_ASSET(e) != GHO_TOKEN(e)); if (f.selector == sig:buyAssetWithSig(address,uint256,address,uint256,bytes).selector) { address receiver; uint256 amount; address originator; uint256 deadline; bytes signature; require(originator != currentContract); buyAssetWithSig(e, originator, amount, receiver, deadline, signature); } else { f(e,args); } assert getAccruedFee(e) <= getGhoBalanceOfThis(e); } // @title _accruedFees should never decrease, unless fees are being harvested by Treasury // STATUS: PASS // https://prover.certora.com/output/11775/281e0b05ac0345edb1d398dcbc329c19?anonymousKey=376f01ddc0cf54741e33c334e83547bb12adba23 rule accruedFeesNeverDecrease(method f) filtered {f -> f.selector != sig:distributeFeesToTreasury().selector && !harnessOnlyMethods(f)} { env e; calldataarg args; uint256 feesBefore = getAccruedFee(e); f(e,args); assert feesBefore <= getAccruedFee(e); } // @title For price ratio == 1, the total assets of a user should not increase // STATUS: PASS // https://prover.certora.com/output/31688/5c6b516ac67c4417a37e00d4bbc7f0d4/?anonymousKey=9d2c66dfc469003c10961d645f398ae3f8cdf1d8 rule totalAssetsNotIncrease(method f) filtered {f -> f.selector != sig:seize().selector && f.selector != sig:rescueTokens(address, address, uint256).selector && f.selector != sig:distributeFeesToTreasury().selector && f.selector != sig:buyAssetWithSig(address, uint256, address, uint256, bytes).selector && f.selector != sig:sellAssetWithSig(address, uint256, address, uint256, bytes).selector && !harnessOnlyMethods(f)} { env e; // we focus on a user so remove address of contracts require e.msg.sender != currentContract; require(getPriceRatio() == 10^18); // uint8 underlyingAssetDecimals; // require underlyingAssetDecimals <= 36; feeLimits(e); priceLimits(e); // require to_mathint(_priceStrategy.getUnderlyingAssetUnits()) == 10^underlyingAssetDecimals; mathint underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); address other; address receiver; uint256 amount; address originator; require(getAssetPriceInGho(e, amount, false) * underlyingAssetUnits/getPriceRatio() == to_mathint(amount)); require receiver != currentContract; require originator != currentContract; require other != e.msg.sender && other != receiver && other != originator && other != currentContract; mathint totalAssetOtherBefore = getTotalAsset(e, other, getPriceRatio(), underlyingAssetUnits); mathint totalAssetBefore = assetOfUsers(e, e.msg.sender, receiver, originator, getPriceRatio(), underlyingAssetUnits); functionDispatcher(f, e, receiver, originator, amount); mathint totalAssetAfter = assetOfUsers(e, e.msg.sender, receiver, originator, getPriceRatio(), underlyingAssetUnits); assert totalAssetBefore >= totalAssetAfter; assert totalAssetOtherBefore == getTotalAsset(e, other, getPriceRatio(), underlyingAssetUnits); } // @title Rule checks that an overall asset of the system (UA - minted gho) stays same. // STATUS: PASS // https://prover.certora.com/output/31688/92138d4951324b81893fdfb04177dd6a/?anonymousKey=8fadc4e00f7004dfe3525dba321d29a8a9c31424 rule systemBalanceStabilityBuy() { uint256 amount; address receiver; env e; require currentContract != e.msg.sender; require currentContract != receiver; // require(getPriceRatio() == 10^18); // uint8 underlyingAssetDecimals; // require underlyingAssetDecimals <= 25; // require to_mathint(_priceStrategy.getUnderlyingAssetUnits()) == 10^underlyingAssetDecimals; // require _priceStrategy.getUnderlyingAssetDecimals() <= 25; // require to_mathint(_priceStrategy.getUnderlyingAssetUnits()) == 10^_priceStrategy.getUnderlyingAssetDecimals(); feeLimits(e); priceLimits(e); mathint ghoMintedBefore = getGhoMinted(e); mathint balanceBefore = getAssetPriceInGho(e, balanceOfUnderlying(e, currentContract), false) - ghoMintedBefore; buyAsset(e, amount, receiver); mathint ghoMintedAfter = getGhoMinted(e); mathint balanceAfter = getAssetPriceInGho(e, balanceOfUnderlying(e, currentContract), false) - ghoMintedAfter; assert(balanceAfter + 1 >= balanceBefore && balanceAfter <= balanceBefore + 1); // assert balanceAfter + 1 >= balanceBefore; } // @title Rule checks that an overall asset of the system (UA - minted gho) stays same. // STATUS: PASS // https://prover.certora.com/output/11775/281e0b05ac0345edb1d398dcbc329c19?anonymousKey=376f01ddc0cf54741e33c334e83547bb12adba23 rule systemBalanceStabilitySell() { uint256 amount; address receiver; env e; require currentContract != e.msg.sender; require currentContract != receiver; // uint8 underlyingAssetDecimals; // require underlyingAssetDecimals <= 25; // mathint underlyingAssetUnits = 10^underlyingAssetDecimals; // require to_mathint(_priceStrategy.getUnderlyingAssetUnits()) == underlyingAssetUnits; // require(getPriceRatio() == 10^18); feeLimits(e); priceLimits(e); mathint ghoMintedBefore = getGhoMinted(e); mathint balanceBefore = getPriceRatio()*balanceOfUnderlying(e, currentContract)/_priceStrategy.getUnderlyingAssetUnits() - ghoMintedBefore; sellAsset(e, amount, receiver); mathint ghoMintedAfter = getGhoMinted(e); mathint balanceAfter = getPriceRatio()*balanceOfUnderlying(e, currentContract)/_priceStrategy.getUnderlyingAssetUnits() - ghoMintedAfter; // assert(balanceAfter + 1 >= balanceBefore && balanceAfter <= balanceBefore + 1); assert balanceAfter + 1 >= balanceBefore; } ================================================ FILE: certora/gsm/specs/gsm/gho-gsm-Buy.spec ================================================ import "../GsmMethods/methods_base.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; // patch2: violated by at most 2 // https://prover.certora.com/output/6893/cb83daf2e5cf4a929b95833e7e3e818e?anonymousKey=6adb07ee65ae6366f535ccad8379bce3784e21ca rule getAssetAmountForBuyAsset_correctness_bound1() { env e; feeLimits(e); priceLimits(e); uint maxToGive; uint suggestedAssetToBuy; suggestedAssetToBuy, _, _, _ = getAssetAmountForBuyAsset(e, maxToGive); uint reallyPaid; _, reallyPaid, _, _ = getGhoAmountForBuyAsset(e, suggestedAssetToBuy); assert reallyPaid <= require_uint256(maxToGive + 1); } // patch2: holds // https://prover.certora.com/output/6893/cb83daf2e5cf4a929b95833e7e3e818e?anonymousKey=6adb07ee65ae6366f535ccad8379bce3784e21ca rule getAssetAmountForBuyAsset_correctness_bound2() { env e; feeLimits(e); priceLimits(e); uint maxToGive; uint suggestedAssetToBuy; suggestedAssetToBuy, _, _, _ = getAssetAmountForBuyAsset(e, maxToGive); uint reallyPaid; _, reallyPaid, _, _ = getGhoAmountForBuyAsset(e, suggestedAssetToBuy); assert reallyPaid <= require_uint256(maxToGive + 2); } // patch2: holds // https://prover.certora.com/output/6893/9752152c77704030aea9dbef2f410423?anonymousKey=d01df80162910000c5aaa7cc4516add5ad7e1739 rule getAssetAmountForBuyAsset_optimality() { env e; feeLimits(e); priceLimits(e); uint maxToGive; uint suggestedAssetToBuy; suggestedAssetToBuy, _, _, _ = getAssetAmountForBuyAsset(e, maxToGive); uint suggestedGhoToPay; _, suggestedGhoToPay, _, _ = getGhoAmountForBuyAsset(e, suggestedAssetToBuy); uint maxCouldBuy; uint couldBuy; uint couldPay; couldBuy, couldPay, _, _ = getGhoAmountForBuyAsset(e, maxCouldBuy); require couldPay <= maxToGive; require couldPay >= suggestedGhoToPay; assert couldBuy <= suggestedAssetToBuy; } // patch3: holds rule getGhoAmountForBuyAsset_optimality() { env e; feeLimits(e); priceLimits(e); uint minAssetToBuy; // 2 uint suggestedAssetToBuy; // 3 uint suggestedGhoToSpend; // 3 suggestedAssetToBuy, suggestedGhoToSpend, _, _ = getGhoAmountForBuyAsset(e, minAssetToBuy); uint min2AssetsToBuy; // 1 uint couldBuy; // 2 uint couldPay; // 2 couldBuy, couldPay, _, _ = getGhoAmountForBuyAsset(e, min2AssetsToBuy); require couldBuy >= minAssetToBuy; //require couldPay >= suggestedGhoToPay; assert couldPay >= suggestedGhoToSpend; } rule getGhoAmountForBuyAsset_correctness() { env e; feeLimits(e); priceLimits(e); uint256 minAssetAmount; uint suggestedAssetToBuy; suggestedAssetToBuy, _, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount); assert suggestedAssetToBuy >= minAssetAmount; } rule getGhoAmountForBuyAsset_correctness1() { env e; feeLimits(e); priceLimits(e); uint256 minAssetAmount; uint suggestedAssetToBuy; suggestedAssetToBuy, _, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount); assert require_uint256(suggestedAssetToBuy + 1) >= minAssetAmount; } rule getAssetAmountForBuyAsset_funcProperty() { // if (A, B, _, _) = getAssetAmountForBuyAsset(X) then B is function of A env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAssetToBuy1; uint totalPay1; suggestedAssetToBuy1, totalPay1, _, _ = getAssetAmountForBuyAsset(e, amount1); uint256 amount2; uint suggestedAssetToBuy2; uint totalPay2; suggestedAssetToBuy2, totalPay2, _, _ = getAssetAmountForBuyAsset(e, amount2); assert (suggestedAssetToBuy1 == suggestedAssetToBuy2) == (totalPay1 == totalPay2); } rule getGhoAmountForBuyAsset_funcProperty() { // if (A, B, _, _) = getGhoAmountForBuyAsset(X) then B is function of A env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAssetToBuy1; uint totalPay1; suggestedAssetToBuy1, totalPay1, _, _ = getGhoAmountForBuyAsset(e, amount1); uint256 amount2; uint suggestedAssetToBuy2; uint totalPay2; suggestedAssetToBuy2, totalPay2, _, _ = getGhoAmountForBuyAsset(e, amount2); assert (suggestedAssetToBuy1 == suggestedAssetToBuy2) == (totalPay1 == totalPay2); } rule getAssetAmountForSellAsset_funcProperty() { // if (A, B, _, _) = getAssetAmountForSellAsset(X) then B is function of A env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAsset1; uint totalPay1; suggestedAsset1, totalPay1, _, _ = getAssetAmountForSellAsset(e, amount1); uint256 amount2; uint suggestedAsset2; uint totalPay2; suggestedAsset2, totalPay2, _, _ = getAssetAmountForSellAsset(e, amount2); assert (suggestedAsset1 == suggestedAsset2) == (totalPay1 == totalPay2); } rule getGhoAmountForSellAsset_funcProperty() { // if (A, B, _, _) = getGhoAmountForSellAsset(X) then B is function of A env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAsset1; uint totalPay1; suggestedAsset1, totalPay1, _, _ = getGhoAmountForSellAsset(e, amount1); uint256 amount2; uint suggestedAsset2; uint totalPay2; suggestedAsset2, totalPay2, _, _ = getGhoAmountForSellAsset(e, amount2); assert (suggestedAsset1 == suggestedAsset2) == (totalPay1 == totalPay2); } rule getGhoAmountForBuyAsset_aditivity() { env e; feeLimits(e); priceLimits(e); uint256 minAssetAmount1; uint bought1; uint paid1; bought1, paid1, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount1); uint256 minAssetAmount2; uint bought2; uint paid2; bought2, paid2, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount2); require require_uint256(bought1 + bought2) > 0; uint256 minAssetAmount3; uint bought3; uint paid3; bought3, paid3, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount3); assert require_uint256(bought1 + bought2) >= bought3 => require_uint256(paid1 + paid2) >= paid3; } rule getAssetAmountForBuyAsset_aditivity() { env e; feeLimits(e); priceLimits(e); uint256 maxGhoAmount1; uint bought1; uint paid1; bought1, paid1, _, _ = getAssetAmountForBuyAsset(e, maxGhoAmount1); uint256 maxGhoAmount2; uint bought2; uint paid2; bought2, paid2, _, _ = getAssetAmountForBuyAsset(e, maxGhoAmount2); require require_uint256(bought1 + bought2) > 0; uint256 maxGhoAmount3; uint bought3; uint paid3; bought3, paid3, _, _ = getAssetAmountForBuyAsset(e, maxGhoAmount3); assert require_uint256(bought1 + bought2) >= bought3 => require_uint256(paid1 + paid2) >= paid3; } // patch2: violated by at most 2 // https://prover.certora.com/output/6893/cb83daf2e5cf4a929b95833e7e3e818e?anonymousKey=6adb07ee65ae6366f535ccad8379bce3784e21ca rule getAssetAmountForBuyAsset_correctness() { env e; feeLimits(e); priceLimits(e); uint maxToGive; require maxToGive > 0; uint suggestedAssetToBuy; suggestedAssetToBuy, _, _, _ = getAssetAmountForBuyAsset(e, maxToGive); uint reallyPaid; _, reallyPaid, _, _ = getGhoAmountForBuyAsset(e, suggestedAssetToBuy); assert reallyPaid <= maxToGive; } ================================================ FILE: certora/gsm/specs/gsm/gho-gsm-finishedRules.spec ================================================ import "../GsmMethods/methods_base.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; import "../GsmMethods/methods_divint_summary.spec"; rule reachability(method f) { env e; feeLimits(e); priceLimits(e); calldataarg args; f(e,args); satisfy true; } // @title Rescuing GHO never lefts less GHO available than _accruedFees. // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule rescuingGhoKeepsAccruedFees() { address token; address to; uint256 amount; env e; feeLimits(e); priceLimits(e); require token == GHO_TOKEN(e); rescueTokens(e, token, to, amount); assert getCurrentGhoBalance(e) >= getAccruedFee(e); } // @title Rescuing underlying never lefts less underlying available than _currentExposure. //Rescuing the underlying asset should never result in there being less of the underlying (as an ERC-20 balance) than the combined total of the _currentExposure and _tokenizedAssets. // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule rescuingAssetKeepsAccruedFees() { address token; address to; uint256 amount; env e; feeLimits(e); priceLimits(e); require token == UNDERLYING_ASSET(e); rescueTokens(e, token, to, amount); assert getCurrentUnderlyingBalance(e) >= assert_uint256(getCurrentExposure(e)); // + getTokenizedAssets(e)); } // @title buyAsset decreases _currentExposure //When calling buyAsset successfully (i.e., no revert), the _currentExposure should always decrease. // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule buyAssetDecreasesExposure() { env e; feeLimits(e); priceLimits(e); uint128 amount; address receiver; uint128 exposureBefore = getCurrentExposure(e); require amount > 0; buyAsset(e, amount, receiver); assert getCurrentExposure(e) < exposureBefore; } // @title sellAsset increases _currentExposure //When calling sellAsset successfully (i.e., no revert), the _currentExposure should always increase. // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule sellAssetIncreasesExposure() { env e; feeLimits(e); priceLimits(e); uint128 amount; address receiver; uint128 exposureBefore = getCurrentExposure(e); require amount > 0; sellAsset(e, amount, receiver); assert getCurrentExposure(e) > exposureBefore; } // @title If _currentExposure exceeds _exposureCap, sellAsset reverts. // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule cantSellIfExposureTooHigh() { env e; feeLimits(e); priceLimits(e); uint128 amount; address receiver; sellAsset(e, amount, receiver); uint128 exposureCap = getExposureCap(e); uint128 currentExposure = getCurrentExposure(e); assert currentExposure <= exposureCap; } definition canChangeExposureCap(method f) returns bool = f.selector == sig:updateExposureCap(uint128).selector || f.selector == sig:initialize(address,address,uint128).selector|| f.selector == sig:seize().selector; // @title Only updateExposureCap, initialize, seize can change exposureCap. // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule whoCanChangeExposureCap(method f) { env e; feeLimits(e); priceLimits(e); uint256 exposureCapBefore = getExposureCap(e); calldataarg args; f(e, args); uint256 exposureCapAfter = getExposureCap(e); assert exposureCapAfter != exposureCapBefore => canChangeExposureCap(f), "should not change exposure cap"; } // @title Cannot buy or sell if the GSM is frozen. // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule cantBuyOrSellWhenFrozen() { env e; feeLimits(e); priceLimits(e); uint128 amount; address receiver; require getIsFrozen(e); buyAsset@withrevert(e, amount, receiver); assert lastReverted; sellAsset@withrevert(e, amount, receiver); assert lastReverted; } // @title Cannot buy or sell if the GSM is seized. // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule cantBuyOrSellWhenSeized() { env e; feeLimits(e); priceLimits(e); uint128 amount; address receiver; require getIsSeized(e); buyAsset@withrevert(e, amount, receiver); assert lastReverted; sellAsset@withrevert(e, amount, receiver); assert lastReverted; } definition canIncreaseExposure(method f) returns bool = f.selector == sig:sellAsset(uint256,address).selector || f.selector == sig:sellAssetWithSig(address,uint256,address,uint256,bytes).selector; definition canDecreaseExposure(method f) returns bool = f.selector == sig:buyAsset(uint256, address).selector || f.selector == sig:seize().selector || f.selector == sig:buyAssetWithSig(address,uint256,address,uint256,bytes).selector; // @title Only specific methods can change exposure. // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule whoCanChangeExposure(method f) { env e; feeLimits(e); priceLimits(e); uint128 exposureBefore = getCurrentExposure(e); calldataarg args; f(e, args); uint128 exposureAfter = getCurrentExposure(e); assert exposureAfter > exposureBefore => canIncreaseExposure(f), "should not increase exposure"; assert exposureAfter < exposureBefore => canDecreaseExposure(f), "should not decrease exposure"; } definition canIncreaseAccruedFees(method f) returns bool = f.selector == sig:sellAsset(uint256,address).selector || f.selector == sig:sellAssetWithSig(address,uint256,address,uint256,bytes).selector || f.selector == sig:buyAsset(uint256, address).selector || f.selector == sig:buyAssetWithSig(address,uint256,address,uint256,bytes).selector; definition canDecreaseAccruedFees(method f) returns bool = f.selector == sig:distributeFeesToTreasury().selector; // @title Only specific methods can increase / decrease accrued fees // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule whoCanChangeAccruedFees(method f) { env e; feeLimits(e); priceLimits(e); uint256 accruedFeesBefore = getAccruedFee(e); calldataarg args; f(e, args); uint256 accruedFeesAfter = getAccruedFee(e); assert accruedFeesAfter > accruedFeesBefore => canIncreaseAccruedFees(f), "should not increase accrued fees"; assert accruedFeesAfter < accruedFeesBefore => canDecreaseAccruedFees(f), "should not decrease accrued fees"; } // @title It's not possible for _currentExposure to exceed _exposureCap as a result of a call to sellAsset. // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule sellingDoesntExceedExposureCap() { env e; feeLimits(e); priceLimits(e); uint128 amount; address receiver; require getCurrentExposure(e) <= getExposureCap(e); sellAsset(e, amount, receiver); assert getCurrentExposure(e) <= getExposureCap(e); } // @title The buy fee actually collected (after rounding) is at least the required percentage. // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule collectedBuyFeeIsAtLeastAsRequired() { env e; feeLimits(e); priceLimits(e); uint256 assetAmount; uint256 ghoTotal; uint256 ghoGross; uint256 ghoFee; _, ghoTotal, ghoGross, ghoFee = getGhoAmountForBuyAsset(e, assetAmount); // assert getPercMathPercentageFactor(e) * ghoFee >= getBuyFeeBP(e) * ghoGross; satisfy getPercMathPercentageFactor(e) * ghoFee >= getBuyFeeBP(e) * ghoGross; } // @title The buy fee actually collected (after rounding) is at least the required percentage. // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule collectedBuyFeePlus1IsAtLeastAsRequired() { env e; feeLimits(e); priceLimits(e); uint256 assetAmount; uint256 ghoTotal; uint256 ghoGross; uint256 ghoFee; _, ghoTotal, ghoGross, ghoFee = getGhoAmountForBuyAsset(e, assetAmount); assert getPercMathPercentageFactor(e) * require_uint256(ghoFee + 1) >= getBuyFeeBP(e) * ghoGross; } // @title The buy fee actually collected (after rounding) is at least the required percentage. // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule collectedBuyFeePlus2IsAtLeastAsRequired() { env e; feeLimits(e); priceLimits(e); uint256 assetAmount; uint256 ghoTotal; uint256 ghoGross; uint256 ghoFee; _, ghoTotal, ghoGross, ghoFee = getGhoAmountForBuyAsset(e, assetAmount); assert getPercMathPercentageFactor(e) * require_uint256(ghoFee + 2) >= getBuyFeeBP(e) * ghoGross; } // @title The sell fee actually collected (after rounding) is at least the required percentage. // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule collectedSellFeeIsAtLeastAsRequired() { env e; feeLimits(e); priceLimits(e); uint256 ghoAmount; uint256 ghoTotal; uint256 ghoGross; uint256 ghoFee; _, ghoTotal, ghoGross, ghoFee = getGhoAmountForSellAsset(e, ghoAmount); assert getPercMathPercentageFactor(e) * ghoFee >= getSellFeeBP(e) * ghoGross; } // @title getAssetAmountForSellAsset returns a value as close as possible to user specified amount. // STATUS: TIMEOUT // https://prover.certora.com/output/33050/f3a77c3d085d4d289ed2e9bd6e7eec37?anonymousKey=378909505ab1597dcb807b3a3f1097de9b0c08a6 rule getAssetAmountForSellAsset_optimality() { // proves that if user wants to receive at least X gho // and the system tel them to sell Y assets, // then there is no amount W < Y that would also bring X gho. env e; feeLimits(e); priceLimits(e); uint wantsToReceive; uint suggestedAssetToSell; suggestedAssetToSell, _, _, _ = getAssetAmountForSellAsset(e, wantsToReceive); uint reallySold; uint reallyReceived; _, reallyReceived, _, _ = getGhoAmountForSellAsset(e, reallySold); require reallyReceived >= wantsToReceive; assert suggestedAssetToSell <= reallySold; } // @title Exposure below cap is preserved by all methods except updateExposureCap and initialize // STATUS: PASS // https://prover.certora.com/output/6893/14a1440d3114460f8b64b388a706ca46/?anonymousKey=bb420c63b5b5b11810d5d72026ed6cb6baec43ac rule exposureBelowCap(method f) filtered { f -> f.selector != sig:initialize(address,address,uint128).selector && f.selector != sig:updateExposureCap(uint128).selector } { env e; calldataarg args; feeLimits(e); priceLimits(e); require getCurrentExposure(e) <= getExposureCap(e); f(e, args); assert getCurrentExposure(e) <= getExposureCap(e); } // @title getAssetAmountForSellAsset never exceeds the given bound // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule getAssetAmountForSellAsset_correctness() { env e; feeLimits(e); priceLimits(e); uint minimumToReceive; uint suggestedAssetToSell; suggestedAssetToSell, _, _, _ = getAssetAmountForSellAsset(e, minimumToReceive); uint reallyReceived; _, reallyReceived, _, _ = getGhoAmountForSellAsset(e, suggestedAssetToSell); assert reallyReceived >= minimumToReceive; } // @title gifting underlying doesn't change storage // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule giftingUnderlyingDoesntAffectStorageSIMPLE() { env e; feeLimits(e); priceLimits(e); address sender; uint128 amount; calldataarg args; storage initialStorage = lastStorage; giftUnderlyingAsset(e, sender, amount); storage storageAfter = lastStorage; assert storageAfter[currentContract] == initialStorage[currentContract]; } // @title gifting GHO doesn't change storage // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule giftingGhoDoesntAffectStorageSIMPLE() { env e; feeLimits(e); priceLimits(e); address sender; uint128 amount; storage initialStorage = lastStorage; giftGho(e, sender, amount) at initialStorage; storage storageAfter = lastStorage; assert storageAfter[currentContract] == initialStorage[currentContract]; } // @title Return values of sellAsset are monotonically increasing // STATUS: TIMEOUT // https://prover.certora.com/output/11775/abdd5e8dc1634d0a91e6a35647b06412?anonymousKey=8ae78b0142eba6819674647e6e41e1f264df6a12 rule monotonicityOfSellAsset() { env e; feeLimits(e); priceLimits(e); address recipient; uint amount1; uint a1; uint g1; //a1, g1 = sellAsset(e, amount1, recipient); a1, g1, _, _ = getGhoAmountForSellAsset(e, amount1); uint amount2; uint a2; uint g2; //a2, g2 = sellAsset(e, amount2, recipient); a2, g2, _, _ = getGhoAmountForSellAsset(e, amount2); assert a1 <= a2 <=> g1 <= g2; } // @title Return values of buyAsset are monotonically increasing // STATUS: PASS // https://prover.certora.com/output/6893/a4e2f473e8e8464db7528615287b19dc/?anonymousKey=52f6539bd09a3ed26235b922ad83c9737b01fd3d rule monotonicityOfBuyAsset() { env e; feeLimits(e); priceLimits(e); address recipient; uint amount1; uint a1; uint g1; a1, g1 = buyAsset(e, amount1, recipient); uint amount2; uint a2; uint g2; a2, g2 = buyAsset(e, amount2, recipient); assert a1 <= a2 <=> g1 <= g2; } // @title Return values of sellAsset are the same as of getGhoAmountForSellAsset // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule sellAssetSameAsGetGhoAmountForSellAsset() { env e; feeLimits(e); priceLimits(e); address recipient; uint amount; uint a1; uint g1; uint a2; uint g2; a1, g1, _, _ = getGhoAmountForSellAsset(e, amount); a2, g2 = sellAsset(e, amount, recipient); assert a1 == a2 && g1 == g2; } // @title buyAsset never returns value lower than the argument // STATUS: PASS // https://prover.certora.com/output/11775/d2998f74795f45eea2ac8da86fd9a481?anonymousKey=6382a56072f63e64436d7af2b5c1800e07a0be9e rule correctnessOfBuyAsset() { env e; feeLimits(e); priceLimits(e); address recipient; uint amount; uint a; uint g; a, g = buyAsset(e, amount, recipient); assert a >= amount; } // @title sellAsset never returns value greater than the argument // STATUS: PASS // https://prover.certora.com/output/6893/5c3c2e6eef7e463cb20a0cc2caa945d3/?anonymousKey=77247d881df0abc794a51e871ccae36c4b3c4e08 rule correctnessOfSellAsset() { env e; feeLimits(e); priceLimits(e); address recipient; uint amount; uint a; uint g; a, g = sellAsset(e, amount, recipient); assert a <= amount; } ================================================ FILE: certora/gsm/specs/gsm/gho-gsm.spec ================================================ import "../GsmMethods/methods_base.spec"; import "../GsmMethods/methods_divint_summary.spec"; import "../GsmMethods/aave_price_fee_limits_strict.spec"; // @title solvency rule: (ghoBacked + 1>= ghoMinted) – (insolvency, rescue more funds than should, allocate more funds to allocator) - buyAsset Function // STATUS: PASSED // https://prover.certora.com/output/11775/23a09796cb7442679f790c6760b303e1?anonymousKey=c672c1acf218250e313b3d165820544582ce366a rule enoughULtoBackGhoBuyAsset() { uint256 _currentExposure = getAvailableLiquidity(); uint256 _ghoMinted = getGhoMinted(); uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals; require underlyingAssetDecimals <78; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; // rounding up to check for the stricter case where the starting _currentExposure is possibly slightly less than ideal uint256 _ghoBacked = _priceStrategy.getAssetPriceInGho(_currentExposure, true); require _ghoBacked >= _ghoMinted; uint256 amount; address receiver; env e; buyAsset(e, amount, receiver); uint256 ghoMinted_ = getGhoMinted(); uint256 currentExposure_ = getAvailableLiquidity(); uint256 ghoBacked_ = _priceStrategy.getAssetPriceInGho(currentExposure_, false); assert to_mathint(ghoBacked_ + 1)>= to_mathint(ghoMinted_) ,"not enough currentExposure to back the ghoMinted"; } // @title solvency rule: (ghoBacked + 1>= ghoMinted) – (insolvency, rescue more funds than should, allocate more funds to allocator) - buyAsset Function // STATUS: PASSED // https://prover.certora.com/output/11775/23a09796cb7442679f790c6760b303e1?anonymousKey=c672c1acf218250e313b3d165820544582ce366a rule enoughUnderlyingToBackGhoRuleSellAsset() { uint256 _currentExposure = getAvailableLiquidity(); uint256 _ghoMinted = getGhoMinted(); uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals; require underlyingAssetDecimals <78; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; // rounding up to check for the stricter case where the starting _currentExposure is possibly slightly less than ideal uint256 _ghoBacked = _priceStrategy.getAssetPriceInGho(_currentExposure,true); require _ghoBacked >= _ghoMinted;//TRY with backed >= is no TO uint128 amount; address receiver; env e; sellAsset(e, amount, receiver); uint256 ghoMinted_ = getGhoMinted(); uint256 currentExposure_ = getAvailableLiquidity(); uint256 ghoBacked_ = _priceStrategy.getAssetPriceInGho(currentExposure_, false); assert to_mathint(ghoBacked_ + 1)>= to_mathint(ghoMinted_) ,"not enough currentExposure to back the ghoMinted"; } // @title solvency rule: (ghoBacked + 1>= ghoMinted) – (insolvency, rescue more funds than should, allocate more funds to allocator) - buyAsset Function // STATUS: PASSED // https://prover.certora.com/output/11775/23a09796cb7442679f790c6760b303e1?anonymousKey=c672c1acf218250e313b3d165820544582ce366a rule enoughULtoBackGhoNonBuySell(method f) filtered { f -> !f.isView && !harnessOnlyMethods(f) && !buySellAssetsFunctions(f) }{ uint256 _currentExposure = getAvailableLiquidity(); uint256 _ghoMinted = getGhoMinted(); uint256 _ghoBacked = _priceStrategy.getAssetPriceInGho(_currentExposure,false); require _ghoBacked >= _ghoMinted; env e; calldataarg args; f(e, args); uint256 ghoMinted_ = getGhoMinted(); uint256 currentExposure_ = getAvailableLiquidity(); uint256 ghoBacked_ = _priceStrategy.getAssetPriceInGho(_currentExposure,false); assert ghoBacked_ >= ghoMinted_,"not enough currentExposure to back the ghoMinted"; } // // @title property#2 If feePercentage > 0 – (Fees are being charged) ) // if fee > 0: // 1. gho received by user is less than assetPriceInGho(underlying amount) in sell asset function // 2. gho paid by user is more than assetPriceInGho(underlying amount received) // 3. gho balance of contract goes up // STATUS: PASSED // https://prover.certora.com/output/11775/23a09796cb7442679f790c6760b303e1?anonymousKey=c672c1acf218250e313b3d165820544582ce366a rule NonZeroFeeCheckSellAsset(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals; require underlyingAssetDecimals <78; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; address receiver; uint256 _receiverGhoBalance = _ghoToken.balanceOf(receiver); uint256 _GSMGhoBalance = _ghoToken.balanceOf(currentContract); uint256 _accruedFee = getAccruedFees(); uint256 amount; uint256 amountInGho = _priceStrategy.getAssetPriceInGho(amount, false); require _FixedFeeStrategy.getSellFee(amountInGho) > 0; env e; basicBuySellSetup(e, receiver); sellAsset(e, amount, receiver); uint256 receiverGhoBalance_ = _ghoToken.balanceOf(receiver); uint256 GSMGhoBalance_ = _ghoToken.balanceOf(currentContract); mathint GSMGhoBalanceIncrease = GSMGhoBalance_ - _GSMGhoBalance; uint256 accruedFee_ = getAccruedFees(); mathint accruedFeeIncrease = accruedFee_ - _accruedFee; mathint ghoReceived = receiverGhoBalance_ - _receiverGhoBalance; assert ghoReceived < to_mathint(amountInGho),"fee not deducted from gho minted for the given UL amount"; assert GSMGhoBalance_ > _GSMGhoBalance ,"GMS gho balance should increase on account of fee collected"; assert accruedFee_ > _accruedFee,"accruedFee should increase in a sell asset transaction"; assert accruedFeeIncrease == GSMGhoBalanceIncrease,"accrued fee should increase by the same amount as the GSM gho balance"; } // @title property#2 If feePercentage > 0 – (Fees are being charged) ) // for buyAsset function // STATUS: PASSED // https://prover.certora.com/output/11775/23a09796cb7442679f790c6760b303e1?anonymousKey=c672c1acf218250e313b3d165820544582ce366a rule NonZeroFeeCheckBuyAsset(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals; require underlyingAssetDecimals <78; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; address receiver; uint256 _receiverGhoBalance = _ghoToken.balanceOf(receiver); uint256 _GSMGhoBalance = _ghoToken.balanceOf(currentContract); uint256 _accruedFee = getAccruedFees(); uint256 amount; uint256 amountInGho = _priceStrategy.getAssetPriceInGho(amount, true); uint256 fee = _FixedFeeStrategy.getBuyFee(amountInGho); require fee > 0; env e; basicBuySellSetup(e, receiver); buyAsset(e, amount, receiver); uint256 receiverGhoBalance_ = _ghoToken.balanceOf(receiver); uint256 GSMGhoBalance_ = _ghoToken.balanceOf(currentContract); mathint GSMGhoBalanceIncrease = GSMGhoBalance_ - _GSMGhoBalance; uint256 accruedFee_ = getAccruedFees(); mathint accruedFeeIncrease = accruedFee_ - _accruedFee; mathint ghoReceived = receiverGhoBalance_ - _receiverGhoBalance; assert ghoReceived < to_mathint(amountInGho),"fee not deducted from gho minted for the given UL amount"; assert GSMGhoBalance_ > _GSMGhoBalance ,"GMS gho balance should increase on account of fee collected"; assert accruedFee_ > _accruedFee,"accruedFee should increase in a sell asset transaction"; assert accruedFeeIncrease == GSMGhoBalanceIncrease,"accrued fee should increase by the same amount as the GSM gho balance"; } ================================================ FILE: certora/gsm/specs/gsm/gho-gsm_inverse.spec ================================================ import "../GsmMethods/methods_base.spec"; import "../GsmMethods/methods_divint_summary.spec"; // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/ae736e76b281420493006752c3f952f6/?anonymousKey=8b267ac5c59ebb69e767810cb01808f9182daf57 rule buySellInverse5(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 5; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse6(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 6; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse7(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 7; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse8(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 8; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse9(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 9; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse10(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 10; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse11(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 11; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse12(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 12; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse13(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 13; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse14(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 14; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse15(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 15; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse16(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 16; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse17(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 17; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse18(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 18; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse19(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 19; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse20(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 20; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse21(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 21; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse22(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 22; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse23(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 23; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse24(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 24; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse25(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 25; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse26(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 26; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // STATUS: PASSED // https://prover.certora.com/output/11775/a6e4d092b5c4424e883ab5810f92ce68/?anonymousKey=ca318da3a72834395fdd1198c23cd189d9d6c988 rule buySellInverse27(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 27; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => ghoBought == ghoSold,"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } ================================================ FILE: certora/gsm/specs/gsm/optimality.spec ================================================ import "../GsmMethods/methods_base.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; import "../GsmMethods/methods_divint_summary.spec"; // @Title For values given by `getAssetAmountForBuyAsset`, the user can only get more by paying more // This rule proves the optimality of getAssetAmountForBuyAsset with respect to // buyAsset in the following sense: // // User wants to buy as much asset as possible while paying at most maxGho. // User asks how much they should provide to buyAsset: // - a, _, _, _ = getAssetAmountForBuyAsset(maxGho) // This results in the user buying Da assets: // - Da, Dx = buyAsset(a) // Is it possible that by not doing as `getAssetAmountForBuyAsset(maxGho)` says, the user would have // gotten a better deal, i.e., paying still less than maxGho, but getting more assets. If this is the // case, then the following holds: // There is a value `a'` such that // - Da', Dx' = buyAsset(a) // - Dx' <= Dx // - Da' > Da // (1) // STATUS: PASS // https://prover.certora.com/output/11775/62c193bbbb484f3d9323986743fd368b?anonymousKey=982afbad05d5b144a84b530bbe8bb4c2f2b4b6af rule R1_optimalityOfBuyAsset_v1() { env e; feeLimits(e); priceLimits(e); address recipient; uint maxGho; uint a; a, _, _, _ = getAssetAmountForBuyAsset(e, maxGho); uint Da; uint Dx; Da, Dx = buyAsset(e, a, recipient); uint ap; uint Dap; uint Dxp; Dap, Dxp = buyAsset(e, ap, recipient); require Dxp <= Dx; assert Dap <= Da; } // @Title User cannot buy more assets for same `maxGho` by providing a lower asset value than the one given by `getAssetAmountForBuyAsset(maxGho)` // This rule proves the optimality of getAssetAmountForBuyAsset with respect to // buyAsset in the following sense: // // User wants to buy as much asset as possible while paying at most maxGho. // User asks how much they should provide to buyAsset: // - a, _, _, _ = getAssetAmountForBuyAsset(maxGho) // This results in the user buying Da assets: // - Da, _ = buyAsset(a) // Is it possible that by not doing as `getAssetAmountForBuyAsset(maxGho)` says, the user would have // gotten a better deal, i.e., paying still less than maxGho, but getting more assets. If this is the // case, then the following holds: // There is a value `a'` such that // - Da', Dx' = buyAsset(a) // - Dx' <= maxGho // - Da' > Da // (2) // STATUS: TIMEOUT // https://prover.certora.com/output/11775/29f5cb2aeb7f4937b70d5e013c5e0648?anonymousKey=6952c53b357275d706fed39ccd6509ffd73228bf rule R2_optimalityOfBuyAsset_v2() { env e; feeLimits(e); priceLimits(e); address recipient; uint maxGho; uint a; a, _, _, _ = getAssetAmountForBuyAsset(e, maxGho); uint Da; Da, _ = buyAsset(e, a, recipient); uint ap; uint Dap; uint Dxp; Dap, Dxp = buyAsset(e, ap, recipient); require Dxp <= maxGho; assert Dap <= Da; } // @Title For values given by `getAssetAmountForSellAsset`, the user can only get more by paying more // This rule proves the optimality of getAssetAmountForSellAsset with respect to // sellAsset in the following sense: // // User wants to sell as little assets as possible while receiving at least `minGho`. // User asks how much should they provide to sellAsset: // - a, _, _, _ = getAssetAmountForSellAsset(minGho) // This results in the user selling Da assets and receiving Dx GHO: // - Da, Dx = sellAsset(a) // Is it possible that by not doing as `getAssetAmountForSellAsset(minGho)` says, the user would have // gotten a better deal, i.e., receiving at least Dx GHO, but selling less assets. If this is the // case, then the following holds: // There is a value `a'` such that // - Da', Dx'= sellAsset(a') // - Dx' >= Dx // - Da' < Da // (3) // STATUS: PASS // https://prover.certora.com/output/11775/62c193bbbb484f3d9323986743fd368b?anonymousKey=982afbad05d5b144a84b530bbe8bb4c2f2b4b6af rule R3_optimalityOfSellAsset_v1 { env e; feeLimits(e); priceLimits(e); address recipient; uint minGho; uint a; a, _, _, _ = getAssetAmountForSellAsset(e, minGho); uint Da; uint Dx; Da, Dx = sellAsset(e, a, recipient); uint ap; uint Dap; uint Dxp; Dap, Dxp = sellAsset(e, ap, recipient); require Dxp >= Dx; assert Dap >= Da; } // @Title User cannot sell less assets for same `minGho` by providing a lower asset value than the one given by `getAssetAmountForSellAsset(minGho)` // This rule proves the optimality of getAssetAmountForSellAsset with respect to // sellAsset in the following sense: // // User wants to sell as little assets as possible while receiving at least `minGho`. // User asks how much should they provide to sellAsset: // - a, _, _, _ = getAssetAmountForSellAsset(minGho) // This results in the user selling DaT assets: // - Da, _ = sellAsset(a) // Is it possible that by not doing as `getAssetAmountForSellAsset(minGho)` says, the user would have // gotten a better deal, i.e., receiving still at least minGho, but selling less assets. If this is the // case, then the following holds: // There is a value `a'` such that // - Da', Dx' = sellAsset(a') // - Dx' >= minGho // - Da' < Da // Solved for UAU = 13, 20, 21, 22, 23, 24, 25, 26, 27: // - https://prover.certora.com/output/40748/0cf26723d13f4a2aa084966deea053f8/?anonymousKey=221ab8180f66732e66c134e43e43d3876041b625 // Solved for UAU = 5, 6, 9, 14, 18: // - https://prover.certora.com/output/40748/c6bf16d3af2e4831a5421e8babb30474/?anonymousKey=41203cbc99be1097c60331f76c185c794ad89868 // Solved for UAU = 8, 19: // - https://prover.certora.com/output/40748/f8ee082aae5e46bda756ef9066569674/?anonymousKey=16e823180102505c4077e7941ff87ca97c1cf87e // (4) // STATUS: TIMEOUT // https://prover.certora.com/output/11775/be619ce4ffde4523acbbc6f3024f9edd?anonymousKey=77da7c4fe1ee9aed6e86e10ec1b0df360929bed4 // rule R4_optimalityOfSellAsset_v2() { // env e; // feeLimits(e); // priceLimits(e); // address recipient; // uint minGho; // uint a; // a, _, _, _ = getAssetAmountForSellAsset(e, minGho); // uint Da; // Da, _ = sellAsset(e, a, recipient); // uint ap; // uint Dap; // uint Dxp; // Dap, Dxp = sellAsset(e, ap, recipient); // require Dxp >= minGho; // assert Dap >= Da; // } // @Title The GHO received by selling asset using values from `getAssetAmountForSellAsset(minGho)` is upper bounded by `minGho` + oneAssetinGho - 1 // External optimality of sellAsset. Shows that the received amount is as close as it can be to target // (5) // STATUS: TIMEOUT // https://prover.certora.com/output/11775/a651d5a5e6b24350ba0e0e5be743e2b7?anonymousKey=047a56cb5c6d3a738002371e7a1ae38b6caea6f3 // rule R5_externalOptimalityOfSellAsset { // env e; // feeLimits(e); // priceLimits(e); // uint256 minGhoToReceive; // uint256 ghoToReceive; // _, ghoToReceive, _, _ = getAssetAmountForSellAsset(e, minGhoToReceive); // uint256 oneAssetInGho = getAssetPriceInGho(e, 1, true); // // assert to_mathint(ghoToReceive) <= minGhoToReceive + oneAssetInGho; // holds: https://prover.certora.com/output/40748/03f0bd8a9323437195fc69871a573197/?anonymousKey=5453059e7056b0f7f5ee583bb0840ab448ec5ac7 // assert to_mathint(ghoToReceive) < minGhoToReceive + oneAssetInGho; // times out: https://prover.certora.com/output/40748/20c45a372ff649c38ed2a728f0c5772a/?anonymousKey=1178900a97ca29ef45a37f981c5dd000227bb43d // // assert to_mathint(ghoToReceive) != minGhoToReceive + oneAssetInGho; // Holds with uau-trick: https://prover.certora.com/output/40748/691739be52a84a2f906f9e99d8a63bee/?anonymousKey=5f873a94d5f0fcf78365ebbee82fca1eff046b0c // } // @Title The GHO received by selling asset using values from `getAssetAmountForSellAsset(minGho)` can be equal to `minGho` + oneAssetInGho - 1 // External optimality of sellAsset. Show the tightness of (5) // (5a) // STATUS: PASS // https://prover.certora.com/output/11775/62c193bbbb484f3d9323986743fd368b?anonymousKey=982afbad05d5b144a84b530bbe8bb4c2f2b4b6af // (The tightness is almost trivial: when oneAssetInGho == 1 and minGhoToReceive == ghoToReceive) rule R5a_externalOptimalityOfSellAsset { env e; feeLimits(e); priceLimits(e); uint256 minGhoToReceive; uint256 ghoToReceive; _, ghoToReceive, _, _ = getAssetAmountForSellAsset(e, minGhoToReceive); uint256 oneAssetInGho = getAssetPriceInGho(e, 1, true); satisfy to_mathint(ghoToReceive) == minGhoToReceive + oneAssetInGho - 1; } // @Title The GHO sold by buying asset using values from `getAssetAmountForBuyAsset(maxGho)` is at least `maxGho - 2*oneAssetInGho + 1 // External optimality of buyAsset. Shows that the received amount is as close as it can be to target // (6) // STATUS: TIMEOUT // https://prover.certora.com/output/11775/23cdf5b0484f4068a9befce7ba094925?anonymousKey=90e8f8254d55e9496c321819d49607337015c877 // rule R6_externalOptimalityOfBuyAsset { // env e; // feeLimits(e); // priceLimits(e); // uint256 maxGhoToSpend; // uint256 ghoToSpend; // _, ghoToSpend, _, _ = getAssetAmountForBuyAsset(e, maxGhoToSpend); // uint256 oneAssetInGho = getAssetPriceInGho(e, 1, true); // assert to_mathint(maxGhoToSpend) <= ghoToSpend + 2*oneAssetInGho - 1; // Holds: https://prover.certora.com/output/40748/2790587d75684f88a232c5898aff9a10/?anonymousKey=893c32307119c35e8d6679db2a05ca1087b38e36 // } // @Title The GHO sold by buying asset using values from `getAssetAmountForBuyAsset(maxGho)` can be equal to `maxGho - 2*oneAssetInGho + 1 // External optimality of buyAsset. Show the tightness of (6) // (6a) // STATUS: PASS // https://prover.certora.com/output/11775/62c193bbbb484f3d9323986743fd368b?anonymousKey=982afbad05d5b144a84b530bbe8bb4c2f2b4b6af // Counterexample is buy fee = 1 BP, maxGhoToSpend = 1, oneAssetInGho = 1, ghoToSpend = 0 rule R6a_externalOptimalityOfBuyAsset { env e; feeLimits(e); priceLimits(e); uint256 maxGhoToSpend; uint256 ghoToSpend; _, ghoToSpend, _, _ = getAssetAmountForBuyAsset(e, maxGhoToSpend); uint256 oneAssetInGho = getAssetPriceInGho(e, 1, true); satisfy to_mathint(maxGhoToSpend) == ghoToSpend + 2*oneAssetInGho - 1; } ================================================ FILE: certora/gsm/specs/gsm4626/AssetToGhoInvertibility4626.spec ================================================ import "../GsmMethods/methods4626_base.spec"; import "../GsmMethods/erc4626.spec"; methods { function _.mulDiv(uint256 x, uint256 y, uint256 denominator) internal => mulDivSummary(x, y, denominator) expect (uint256); function _.mulDiv(uint256 x, uint256 y, uint256 denominator, Math.Rounding rounding) internal => mulDivSummaryRounding(x, y, denominator, rounding) expect (uint256); } function mulDivSummary(uint256 x, uint256 y, uint256 denominator) returns uint256 { require denominator > 0; return require_uint256((x * y) / denominator); } function mulDivSummaryRounding(uint256 x, uint256 y, uint256 denominator, Math.Rounding rounding) returns uint256 { require denominator > 0; if (rounding == Math.Rounding.Up) { return require_uint256((x * y + denominator - 1) / denominator); } else return require_uint256((x * y) / denominator); } // // FULL REPORT AT: https://prover.certora.com/output/17512/a9aea9e11c56465d8714999a162bfdfa?anonymousKey=441316ec25aa2588abfca22582854f51dda2f339 // // @title actual gho amount returned getAssetAmountForBuyAsset should be less than max gho amount specified by the user // // STATUS: VIOLATED // // https://prover.certora.com/output/11775/c75e493e2c494c2a8915efa5db311c6c?anonymousKey=04dc391cd1e3719c2302f38c2e045bcfa7907b76 // rule basicProperty_getAssetAmountForBuyAsset() { // env e; // require getPriceRatio(e) > 0; // require _FixedFeeStrategy.getBuyFeeBP(e) <= 10000; // uint256 maxGhoAmount; // uint256 actualGhoAmount; // _, actualGhoAmount, _, _ = getAssetAmountForBuyAsset(e, maxGhoAmount); // assert actualGhoAmount <= maxGhoAmount; // } // // @title getAssetAmountForBuyAsset should return the same asset and gho amount for an amount of gho suggested as the selling amount // // STATUS: VIOLATED // // https://prover.certora.com/output/11775/c75e493e2c494c2a8915efa5db311c6c?anonymousKey=04dc391cd1e3719c2302f38c2e045bcfa7907b76 // rule basicProperty2_getAssetAmountForBuyAsset() { // env e; // mathint priceRatio = getPriceRatio(e); // require priceRatio == 9*10^17 || priceRatio == 10^18 || priceRatio == 5*10^18; // mathint uau = _priceStrategy.getUnderlyingAssetUnits(e); // uint8 underlyingAssetDecimals; // require underlyingAssetDecimals < 25 && underlyingAssetDecimals > 5; // require uau == 10^underlyingAssetDecimals; // mathint buyFee = _FixedFeeStrategy.getBuyFeeBP(e); // require buyFee == 0 || buyFee == 1000 || buyFee == 357 || buyFee == 9000 || buyFee == 10000; // uint256 maxGhoAmount; // uint256 assetsBought; uint256 assetsBought2; // uint256 actualGhoAmount; uint256 actualGhoAmount2; // uint256 grossAmount; uint256 grossAmount2; // uint256 fee; uint256 fee2; // assetsBought, actualGhoAmount, grossAmount, fee = getAssetAmountForBuyAsset(e, maxGhoAmount); // assetsBought2, actualGhoAmount2, grossAmount2, fee2 = getAssetAmountForBuyAsset(e, actualGhoAmount); // assert assetsBought == assetsBought2 && actualGhoAmount == actualGhoAmount2 && grossAmount == grossAmount2 && fee == fee2; // } // // @title actual gho amount returned getGhoAmountForBuyAsset should be more than the min amount specified by the user // // STATUS: VIOLATED // // https://prover.certora.com/output/11775/c75e493e2c494c2a8915efa5db311c6c?anonymousKey=04dc391cd1e3719c2302f38c2e045bcfa7907b76 // rule basicProperty_getGhoAmountForBuyAsset() { // env e; // require getPriceRatio(e) > 0; // require _FixedFeeStrategy.getBuyFeeBP(e) < 10000; // uint256 minAssetAmount; // uint256 actualAssetAmount; // actualAssetAmount, _, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount); // assert minAssetAmount <= actualAssetAmount; // } // // @title actual gho amount returned getAssetAmountForSellAsset should be more than the min amount specified by the user // // STATUS: VIOLATED // // https://prover.certora.com/output/11775/c75e493e2c494c2a8915efa5db311c6c?anonymousKey=04dc391cd1e3719c2302f38c2e045bcfa7907b76 // rule basicProperty_getAssetAmountForSellAsset() { // env e; // require getPriceRatio(e) > 0; // require _FixedFeeStrategy.getSellFeeBP(e) < 10000; // uint256 minGhoAmount; // uint256 actualGhoAmount; // _, actualGhoAmount, _, _ = getAssetAmountForSellAsset(e, minGhoAmount); // assert minGhoAmount <= actualGhoAmount; // } // // @title actual asset amount returned getGhoAmountForSellAsset should be less than the max amount specified by the user // // STATUS: VIOLATED // // https://prover.certora.com/output/11775/c75e493e2c494c2a8915efa5db311c6c?anonymousKey=04dc391cd1e3719c2302f38c2e045bcfa7907b76 // rule basicProperty_getGhoAmountForSellAsset() { // env e; // require getPriceRatio(e) > 0; // require _FixedFeeStrategy.getSellFeeBP(e) < 10000; // uint256 maxAssetAmount; // uint256 actualAssetAmount; // actualAssetAmount, _, _, _ = getGhoAmountForSellAsset(e, maxAssetAmount); // assert actualAssetAmount <= maxAssetAmount; // } // // @title getGhoAmountForBuyAsset should return the same amount for an asset amount suggested by it // // STATUS: VIOLATED // // https://prover.certora.com/output/11775/c75e493e2c494c2a8915efa5db311c6c?anonymousKey=04dc391cd1e3719c2302f38c2e045bcfa7907b76 // rule basicProperty2_getGhoAmountForBuyAsset() { // env e; // mathint priceRatio = getPriceRatio(e); // require priceRatio == 9*10^17 || priceRatio == 10^18 || priceRatio == 5*10^18; // mathint uau = _priceStrategy.getUnderlyingAssetUnits(e); // uint8 underlyingAssetDecimals; // require underlyingAssetDecimals < 25 && underlyingAssetDecimals > 5; // require uau == 10^underlyingAssetDecimals; // mathint buyFee = _FixedFeeStrategy.getBuyFeeBP(e); // require buyFee == 0 || buyFee == 1000 || buyFee == 357 || buyFee == 9000 || buyFee == 9999; // uint256 minAssetAmount; // uint256 assetsBought; uint256 assetsBought2; // uint256 actualGhoAmount; uint256 actualGhoAmount2; // uint256 grossAmount; uint256 grossAmount2; // uint256 fee; uint256 fee2; // assetsBought, actualGhoAmount, grossAmount, fee = getGhoAmountForBuyAsset(e, minAssetAmount); // assetsBought2, actualGhoAmount2, grossAmount2, fee2 = getGhoAmountForBuyAsset(e, assetsBought); // assert assetsBought == assetsBought2 && actualGhoAmount == actualGhoAmount2 && grossAmount == grossAmount2 && fee == fee2; // } // /** // *********************************** // ***** BUY ASSET INVERSE RULES ***** // *********************************** // */ // // @title getAssetAmountForBuyAsset is inverse of getGhoAmountForBuyAsset // // STATUS: VIOLATED // // https://prover.certora.com/output/11775/c75e493e2c494c2a8915efa5db311c6c?anonymousKey=04dc391cd1e3719c2302f38c2e045bcfa7907b76 // rule buyAssetInverse_asset() { // env e; // mathint priceRatio = getPriceRatio(e); // require priceRatio >= 10^16 && priceRatio <= 10^20; // mathint uau = _priceStrategy.getUnderlyingAssetUnits(e); // uint8 underlyingAssetDecimals; // require underlyingAssetDecimals <= 27 && underlyingAssetDecimals >= 5; // require uau == 10^underlyingAssetDecimals; // require _FixedFeeStrategy.getBuyFeeBP(e) < 5000; // uint256 maxGhoAmount; // uint256 assetAmount; // uint256 assetAmount2; // assetAmount, _, _, _ = getAssetAmountForBuyAsset(e, maxGhoAmount); // assetAmount2, _, _, _ = getGhoAmountForBuyAsset(e, assetAmount); // assert assetAmount == assetAmount2; // } // // @title getAssetAmountForSellAsset is inverse of getGhoAmountForSellAsset // // STATUS: PASSING // // https://prover.certora.com/output/11775/c75e493e2c494c2a8915efa5db311c6c?anonymousKey=04dc391cd1e3719c2302f38c2e045bcfa7907b76 rule buyAssetInverse_all() { env e; mathint priceRatio = getPriceRatio(e); require priceRatio >= 10^16 && priceRatio <= 10^20; mathint uau = _priceStrategy.getUnderlyingAssetUnits(e); uint8 underlyingAssetDecimals; require underlyingAssetDecimals <= 27 && underlyingAssetDecimals >= 5; require uau == 10^underlyingAssetDecimals; require _FixedFeeStrategy.getBuyFeeBP(e) < 5000; uint256 maxGhoAmount; uint256 assetAmount; uint256 assetAmount2; uint256 ghoAmount; uint256 ghoAmount2; uint256 grossAmount; uint256 grossAmount2; uint256 fee; uint256 fee2; assetAmount, ghoAmount, grossAmount, fee = getAssetAmountForBuyAsset(e, maxGhoAmount); assetAmount2, ghoAmount2, grossAmount2, fee2 = getGhoAmountForBuyAsset(e, assetAmount); mathint maxAssetError = (3*uau)/(5*getPriceRatio(e)) + 2; assert assetAmount <= assetAmount2 && to_mathint(assetAmount2) <= assetAmount + maxAssetError, "asset amount error bound"; assert ghoAmount == ghoAmount2, "gho amount"; assert grossAmount == grossAmount2, "gross amount"; assert fee == fee2, "fee"; } // /** // ************************************ // ***** SELL ASSET INVERSE RULES ***** // ************************************ // */ // // @title getAssetAmountForBuyAsset is inverse of getGhoAmountForBuyAsset // // STATUS: VIOLATED // // https://prover.certora.com/output/11775/c75e493e2c494c2a8915efa5db311c6c?anonymousKey=04dc391cd1e3719c2302f38c2e045bcfa7907b76 // rule sellAssetInverse_gross() { // env e; // mathint priceRatio = getPriceRatio(e); // require 10^16 <= priceRatio && priceRatio <= 10^20; // mathint uau = _priceStrategy.getUnderlyingAssetUnits(e); // uint8 underlyingAssetDecimals; // require underlyingAssetDecimals <= 27 && underlyingAssetDecimals >= 5; // require uau == 10^underlyingAssetDecimals; // require _FixedFeeStrategy.getSellFeeBP(e) < 5000; // uint256 minGhoAmount; // uint256 assetAmount; // uint256 grossAmount; // uint256 grossAmount2; // assetAmount, _, grossAmount, _ = getAssetAmountForSellAsset(e, minGhoAmount); // _, _, grossAmount2, _ = getGhoAmountForSellAsset(e, assetAmount); // assert grossAmount == grossAmount2; // } // // @title getAssetAmountForSellAsset is inverse of getGhoAmountForSellAsset // // STATUS: VIOLATED // // https://prover.certora.com/output/11775/c75e493e2c494c2a8915efa5db311c6c?anonymousKey=04dc391cd1e3719c2302f38c2e045bcfa7907b76 // /* Takes 7000 seconds, the counterexample may be required directly // underlyingAssetDecimals = 11 // sellFee = 1 // minGhoAmount = 9 // getAssetAmountForSellAsset(minGhoAmount=9) = (1, 0x1ada5, 0x1adb1, 12) // getGhoAmountForSellAsset(maxAssetAmount=1) = (1, 0x1ada5, 0x1adb0, 11) // */ // rule sellAssetInverse_fee() { // env e; // mathint priceRatio = getPriceRatio(e); // require 10^16 <= priceRatio && priceRatio <= 10^20; // mathint uau = _priceStrategy.getUnderlyingAssetUnits(e); // uint8 underlyingAssetDecimals; // require underlyingAssetDecimals <= 27 && underlyingAssetDecimals >= 5; // require uau == 10^underlyingAssetDecimals; // require _FixedFeeStrategy.getSellFeeBP(e) < 5000; // uint256 minGhoAmount; // uint256 assetAmount; // uint256 fee; // uint256 fee2; // assetAmount, _, _, fee = getAssetAmountForSellAsset(e, minGhoAmount); // _, _, _, fee2 = getGhoAmountForSellAsset(e, assetAmount); // assert fee == fee2; // } // @title getAssetAmountForSellAsset is inverse of getGhoAmountForSellAsset // STATUS: PASSING rule sellAssetInverse_all() { env e; require 10^16 <= getPriceRatio(e) && getPriceRatio(e) <= 10^20; mathint uau = _priceStrategy.getUnderlyingAssetUnits(e); uint8 underlyingAssetDecimals; require underlyingAssetDecimals <= 30 && underlyingAssetDecimals >= 1; require uau == 10^underlyingAssetDecimals; require _FixedFeeStrategy.getSellFeeBP(e) < 5000; uint256 minGhoAmount; uint256 assetAmount; uint256 assetAmount2; uint256 ghoAmount; uint256 ghoAmount2; uint256 grossAmount; uint256 grossAmount2; uint256 fee; uint256 fee2; assetAmount, ghoAmount, grossAmount, fee = getAssetAmountForSellAsset(e, minGhoAmount); assetAmount2, ghoAmount2, grossAmount2, fee2 = getGhoAmountForSellAsset(e, assetAmount); assert assetAmount == assetAmount2, "asset amount"; assert ghoAmount == ghoAmount2, "gho amount"; assert grossAmount2 <= grossAmount && to_mathint(grossAmount) <= grossAmount2 + 1, "gross amount off by at most 1"; assert fee2 <= fee && to_mathint(fee) <= fee2 + 1, "fee by at most 1"; assert (fee == fee2) <=> (grossAmount == grossAmount2), "fee off by 1 iff gross amount off by 1"; } ================================================ FILE: certora/gsm/specs/gsm4626/FixedPriceStrategy4626.spec ================================================ // import "../GsmMethods/methods_base.spec"; import "../GsmMethods/erc4626.spec"; methods { function getAssetPriceInGho(uint256, bool) external returns (uint256) envfree; function getGhoPriceInAsset(uint256, bool) external returns (uint256) envfree; function _.mulDiv(uint256 x, uint256 y, uint256 denominator) internal => mulDivSummary(x, y, denominator) expect (uint256); function _.mulDiv(uint256 x, uint256 y, uint256 denominator, Math.Rounding rounding) internal => mulDivSummaryRounding(x, y, denominator, rounding) expect (uint256); } function mulDivSummary(uint256 x, uint256 y, uint256 denominator) returns uint256 { require denominator > 0; return require_uint256((x * y) / denominator); } function mulDivSummaryRounding(uint256 x, uint256 y, uint256 denominator, Math.Rounding rounding) returns uint256 { require denominator > 0; if (rounding == Math.Rounding.Up) { return require_uint256((x * y + denominator - 1) / denominator); } else return require_uint256((x * y) / denominator); } // https://prover.certora.com/output/17512/4273175adeae4a289be8401c82ab9e14?anonymousKey=3dd87914a5a95f469b25a2666ffa484f4b734c34 rule assetToGhoAndBackAllErrorBounds() { env e; uint256 originalAssetAmount; mathint underlyingAssetUnits = getUnderlyingAssetUnits(e); require underlyingAssetUnits > 0; // safe as this number should be equal to 10 ** underlyingAssetDecimals uint256 priceRatio = getPriceRatio(e); require priceRatio > 0; mathint maxError = (3*underlyingAssetUnits)/(5*priceRatio) + 2; assert to_mathint(getGhoPriceInAsset(getAssetPriceInGho(originalAssetAmount, false), false)) >= originalAssetAmount - (maxError) && originalAssetAmount >= getGhoPriceInAsset(getAssetPriceInGho(originalAssetAmount, false), false) , "rounding down then down"; assert to_mathint(getGhoPriceInAsset(getAssetPriceInGho(originalAssetAmount, false), true)) >= originalAssetAmount - (maxError - 1) && originalAssetAmount >= getGhoPriceInAsset(getAssetPriceInGho(originalAssetAmount, false), true) , "rounding down then up"; assert to_mathint(getGhoPriceInAsset(getAssetPriceInGho(originalAssetAmount, true), false)) <= originalAssetAmount + (maxError - 1) && originalAssetAmount <= getGhoPriceInAsset(getAssetPriceInGho(originalAssetAmount, true), false) , "rounding up then down"; assert to_mathint(getGhoPriceInAsset(getAssetPriceInGho(originalAssetAmount, true), true)) <= originalAssetAmount + maxError && originalAssetAmount <= getGhoPriceInAsset(getAssetPriceInGho(originalAssetAmount, true), true) , "rounding up then up"; } rule ghoToAssetAndBackAllErrorBounds() { env e; uint256 originalAmountOfGho; mathint underlyingAssetUnits = getUnderlyingAssetUnits(e); require underlyingAssetUnits > 0; // safe as this number should be equal to 10 ** underlyingAssetDecimals uint256 priceRatio = getPriceRatio(e); require priceRatio > 0; mathint maxError = 11*priceRatio/(3*underlyingAssetUnits) + 1; // Notice that even when we round down, we can increase the amount of gho due to rounding in preview withdraw. assert to_mathint(getAssetPriceInGho(getGhoPriceInAsset(originalAmountOfGho, false), false)) >= originalAmountOfGho - maxError && originalAmountOfGho + priceRatio/underlyingAssetUnits >= to_mathint(getAssetPriceInGho(getGhoPriceInAsset(originalAmountOfGho, false), false)) , "rounding down then down"; assert to_mathint(getAssetPriceInGho(getGhoPriceInAsset(originalAmountOfGho, false), true)) >= originalAmountOfGho - maxError && originalAmountOfGho + priceRatio/underlyingAssetUnits + 1 >= to_mathint(getAssetPriceInGho(getGhoPriceInAsset(originalAmountOfGho, false), true)) , "rounding down then up"; assert to_mathint(getAssetPriceInGho(getGhoPriceInAsset(originalAmountOfGho, true), false)) <= originalAmountOfGho + maxError && originalAmountOfGho <= getAssetPriceInGho(getGhoPriceInAsset(originalAmountOfGho, true), false) , "rounding up then down"; assert to_mathint(getAssetPriceInGho(getGhoPriceInAsset(originalAmountOfGho, true), true)) <= originalAmountOfGho + maxError && originalAmountOfGho <= getAssetPriceInGho(getGhoPriceInAsset(originalAmountOfGho, true), true) , "rounding up then up"; } rule getAssetPriceIsMonotone() { env e; uint256 amount1; uint256 amount2; assert amount1 > amount2 => getAssetPriceInGho(amount1, false) >= getAssetPriceInGho(amount2, false); assert amount1 > amount2 => getAssetPriceInGho(amount1, true) >= getAssetPriceInGho(amount2, true); } rule getGhoPriceIsMonotone() { env e; uint256 amount1; uint256 amount2; assert amount1 > amount2 => getGhoPriceInAsset(amount1, false) >= getGhoPriceInAsset(amount2, false); assert amount1 > amount2 => getGhoPriceInAsset(amount1, true) >= getGhoPriceInAsset(amount2, true); } ================================================ FILE: certora/gsm/specs/gsm4626/balances-buy-4626.spec ================================================ import "../GsmMethods/erc20.spec"; import "../GsmMethods/methods_divint_summary.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; import "../GsmMethods/erc4626.spec"; using DiffHelper as diffHelper; methods { function distributeFeesToTreasury() external; } // Issue: // The exact GHO return by `getAssetAmountForBuyAsset(max)` can be greater than `max` in 4626 // Description: // The user may ask the amount of assets to provide for `buyAsset` by calling // `getAssetAmountForBuyAsset(max)`, where `max` is the maximum amount of GHO // user is willing to pay. One of the return values of // `getAssetAmountForBuyAsset` is the exact amount of GHO that will be deducted. // This value can be higher than `max`. // Note: From https://github.com/Certora/gho-gsm/pull/18 // ========================= Buying ============================== // // @title 4626: The exact amount of GHO returned by `getAssetAmountForBuyAsset(maxGho)` is less than or equal to `maxGho` // . -[getAssetAmountForBuyAsset(x)]-> . // exactGHO <= goWithFee // where exactGHO is the 2nd return value of getAssetAmountForBuyAsset // Holds: https://prover.certora.com/output/40748/0146aff66f2a492886c6dd89724b92ba?anonymousKey=32b3789b362a27460edce2d9bc86870646e65c52 // (1) rule R1_getAssetAmountForBuyAssetRV2 { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation require GHO_TOKEN(e) != UNDERLYING_ASSET(e); // This is inflation prevention (and also avoids an overflow) uint256 ghoWithFee; uint256 assetsToBuy; uint256 exactGHO; address receiver; // For debugging: uint256 priceRatio = getPriceRatio(e); uint256 underlyingAssetUnits = getUnderlyingAssetUnits(e); _, exactGHO, _, _ = getAssetAmountForBuyAsset(e, ghoWithFee); assert exactGHO <= ghoWithFee; } // @title 4626: The exact amount of GHO returned by `getAssetAmountForBuyAsset(maxGho)` can be less than `maxGho` // (1a) // Holds: https://prover.certora.com/output/40748/0146aff66f2a492886c6dd89724b92ba?anonymousKey=32b3789b362a27460edce2d9bc86870646e65c52 rule R1a_getAssetAmountForBuyAssetRV2_LT { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation require GHO_TOKEN(e) != UNDERLYING_ASSET(e); // This is inflation prevention (and also avoids an overflow) uint256 ghoWithFee; uint256 assetsToBuy; uint256 exactGHO; address receiver; // For debugging: uint256 priceRatio = getPriceRatio(e); uint256 underlyingAssetUnits = getUnderlyingAssetUnits(e); _, exactGHO, _, _ = getAssetAmountForBuyAsset(e, ghoWithFee); satisfy exactGHO < ghoWithFee; } // @title 4626: The exact amount of GHO returned by `getAssetAmountForBuyAsset(x)` matches the GHO amount deduced from user at `buyAsset` // . -[getAssetAmountForBuyAsset(x)]-> . -[buyAsset(exactGHO)]-> . // ghoBalance_1 - ghoBalance_2 = exactGHO // where exactGHO is the 2nd return value of getAssetAmountForBuyAsset // Holds: https://prover.certora.com/output/40748/0146aff66f2a492886c6dd89724b92ba?anonymousKey=32b3789b362a27460edce2d9bc86870646e65c52 // (2) rule R2_getAssetAmountForBuyAssetRV_vs_GhoBalance { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation require GHO_TOKEN(e) != UNDERLYING_ASSET(e); // This is inflation prevention (and also avoids an overflow) uint256 ghoWithFee; uint256 assetsToBuy; uint256 exactGHO; address receiver; // For debugging: uint256 priceRatio = getPriceRatio(e); uint256 underlyingAssetUnits = getUnderlyingAssetUnits(e); assetsToBuy, exactGHO, _, _ = getAssetAmountForBuyAsset(e, ghoWithFee); uint256 buyerGhoBalanceBefore = balanceOfGho(e, e.msg.sender); require assetsToBuy <= max_uint128; buyAsset(e, assert_uint128(assetsToBuy), receiver); uint256 buyerGhoBalanceAfter = balanceOfGho(e, e.msg.sender); mathint balanceDiff = buyerGhoBalanceBefore - buyerGhoBalanceAfter; assert to_mathint(exactGHO) == balanceDiff; } // @title 4626: The asset amount deduced from user's account at `buyAsset(minAssets)` is at least `minAssets` // -[buyAsset]-> // assetsToBuy <= |buyerAssetBalanceAfter - buyerAssetBalanceBefore| // (3) // STATUS: TIMEOUT // https://prover.certora.com/output/33050/56571f50dd3f4f5ead1c1ee7520b7619?anonymousKey=9b0e61ce85c892c5bf093508ee8a03d6d91fda53 rule R3_buyAssetUpdatesAssetBuyerAssetBalanceLe { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation require GHO_TOKEN(e) != UNDERLYING_ASSET(e); // This is inflation prevention (and also avoids an overflow) uint256 assetsToBuy; address receiver; require receiver != currentContract; // Otherwise GHO is burned but asset value doesn't increase. (This is only a problem for my bookkeeping) // For debugging: uint256 priceRatio = getPriceRatio(e); uint256 underlyingAssetUnits = getUnderlyingAssetUnits(e); require assetsToBuy <= max_uint128; uint256 receiverAssetBalanceBefore = balanceOfUnderlying(e, receiver); buyAsset(e, assert_uint128(assetsToBuy), receiver); uint256 receiverAssetBalanceAfter = balanceOfUnderlying(e, receiver); uint256 balanceDiff = require_uint256(receiverAssetBalanceAfter - receiverAssetBalanceBefore); assert assetsToBuy <= balanceDiff; } // @title 4626: The asset amount deduced from user's account at `buyAsset(minAssets)` can be more than `minAssets` // -[buyAsset]-> // assetsToBuy < |buyerAssetBalanceAfter - buyerAssetBalanceBefore| // (3a) // Holds: https://prover.certora.com/output/40748/0146aff66f2a492886c6dd89724b92ba?anonymousKey=32b3789b362a27460edce2d9bc86870646e65c52 rule R3a_buyAssetUpdatesAssetBuyerAssetBalanceLt { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation require GHO_TOKEN(e) != UNDERLYING_ASSET(e); // This is inflation prevention (and also avoids an overflow) uint256 assetsToBuy; address receiver; require receiver != currentContract; // Otherwise GHO is burned but asset value doesn't increase. (This only a problem for my bookkeeping) // For debugging: uint256 priceRatio = getPriceRatio(e); uint256 underlyingAssetUnits = getUnderlyingAssetUnits(e); require assetsToBuy <= max_uint128; uint256 receiverAssetBalanceBefore = balanceOfUnderlying(e, receiver); buyAsset(e, assert_uint128(assetsToBuy), receiver); uint256 receiverAssetBalanceAfter = balanceOfUnderlying(e, receiver); uint256 balanceDiff = require_uint256(receiverAssetBalanceAfter - receiverAssetBalanceBefore); satisfy assetsToBuy < balanceDiff; } // @title 4626: The amount of GHO deduced from user's account at `buyAsset` is less than or equal to the value passed to `getAssetAmountForBuyAsset` // . -[getAssetAmountForBuyAsset(x)]-> . -[buyAsset]-> . // buyerGhoBalanceBefore - buyerGhoBalanceAfter <= goWithFee // (4) // Holds: https://prover.certora.com/output/40748/0146aff66f2a492886c6dd89724b92ba?anonymousKey=32b3789b362a27460edce2d9bc86870646e65c52 rule R4_sellGhoUpdatesAssetBuyerGhoBalanceGe { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation require GHO_TOKEN(e) != UNDERLYING_ASSET(e); // This is inflation prevention (and also avoids an overflow) uint256 ghoWithFee; uint256 assetsToBuy; address receiver; // For debugging: uint256 priceRatio = getPriceRatio(e); uint256 underlyingAssetUnits = getUnderlyingAssetUnits(e); assetsToBuy, _, _, _ = getAssetAmountForBuyAsset(e, ghoWithFee); require assetsToBuy <= max_uint128; uint256 buyerGhoBalanceBefore = balanceOfGho(e, e.msg.sender); buyAsset(e, assert_uint128(assetsToBuy), receiver); uint256 buyerGhoBalanceAfter = balanceOfGho(e, e.msg.sender); mathint balanceDiff = buyerGhoBalanceBefore - buyerGhoBalanceAfter; satisfy to_mathint(ghoWithFee) >= balanceDiff; } // @title 4626: The amount of GHO deduced from user's account at `buyAsset` can be less than the value passed to `getAssetAmountForBuyAsset` // . -[getAssetAmountForBuyAsset(x)]-> . -[buyAsset]-> . // buyerGhoBalanceBefore - buyerGhoBalanceAfter < goWithFee // Expected to hold in current implementation // (4a) // Holds: https://prover.certora.com/output/40748/0146aff66f2a492886c6dd89724b92ba?anonymousKey=32b3789b362a27460edce2d9bc86870646e65c52 rule R4a_sellGhoUpdatesAssetBuyerGhoBalanceGt { env e; feeLimits(e); priceLimits(e); uint256 ghoWithFee; uint256 assetsToBuy; address receiver; require receiver != e.msg.sender; // Otherwise the sold GHO will just come back to me. assetsToBuy, _, _, _ = getAssetAmountForBuyAsset(e, ghoWithFee); require assetsToBuy <= max_uint128; uint256 buyerGhoBalanceBefore = balanceOfGho(e, e.msg.sender); buyAsset(e, assert_uint128(assetsToBuy), receiver); uint256 buyerGhoBalanceAfter = balanceOfGho(e, e.msg.sender); mathint balanceDiff = buyerGhoBalanceBefore - buyerGhoBalanceAfter; satisfy to_mathint(ghoWithFee) > balanceDiff; } ================================================ FILE: certora/gsm/specs/gsm4626/balances-sell-4626.spec ================================================ import "../GsmMethods/erc20.spec"; import "../GsmMethods/methods_divint_summary.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; import "../GsmMethods/erc4626.spec"; using DiffHelper as diffHelper; methods { function distributeFeesToTreasury() external; } // ========================= Selling ============================== // The user wants to buy GHO and asks how much asset should be sold. Fees are // not included in user's GHO buying order. // @Title 4626: The exact amount of GHO returned by `getAssetAmountForSellAsset(minGho)` is at least `minGho` // Check that recipient's GHO balance is updated correctly // User wants to buy `minGhoToSend` GHO. // User asks for the assets required: `(assetsToSpend, ghoToReceive, ghoToSpend, fee) := getAssetAmountForSellAsset(minGhoToReceive)` // Let balance difference of the recipient be balanceDiff. // (1): ghoToReceive >= minGhoToReceive Expected to hold. // User wants to receive at least minGhoAmount. Is the amount of GHO reported by getAssetAmountForSellAsset at least minGhoAmount // (1) // Holds: https://prover.certora.com/output/40748/c4b0691393f4416dbe328f383093ffad?anonymousKey=83439124b153fd20f61457ff3c63da877c6770c3 rule R1_getAssetAmountForSellAsset_arg_vs_return { env e; feeLimits(e); priceLimits(e); uint256 minGhoToReceive; uint256 ghoToReceive; _, ghoToReceive, _, _ = getAssetAmountForSellAsset(e, minGhoToReceive); assert minGhoToReceive <= ghoToReceive; } // @Title 4626: The exact amount of GHO returned by `getAssetAmountForSellAsset(minGho)` can be greater than `minGho` // Shows != // (1a) // Holds: https://prover.certora.com/output/40748/c4b0691393f4416dbe328f383093ffad?anonymousKey=83439124b153fd20f61457ff3c63da877c6770c3 rule R1a_buyGhoUpdatesGhoBalanceCorrectly1 { env e; feeLimits(e); priceLimits(e); uint256 minGhoToReceive; uint256 ghoToReceive; _, _, ghoToReceive, _ = getAssetAmountForSellAsset(e, minGhoToReceive); satisfy minGhoToReceive != ghoToReceive; } // @Title 4626: The exact amount of GHO returned by `getAssetAmountForSellAsset` is equal to the amount obtained after `sellAsset` // getAssetAmountForSellAsset returns exactGhoToReceive. Does this match the exact GHO received after the corresponding sellAsset? // Holds: https://prover.certora.com/output/40748/c4b0691393f4416dbe328f383093ffad?anonymousKey=83439124b153fd20f61457ff3c63da877c6770c3 // (2) rule R2_getAssetAmountForSellAsset_sellAsset_eq { env e; feeLimits(e); priceLimits(e); uint256 minGhoToReceive; uint256 ghoToReceive; uint256 assetsToSell; require currentContract.UNDERLYING_ASSET(e) != currentContract.GHO_TOKEN(e); // Otherwise we only measure the fee. address recipient; require recipient != currentContract; // Otherwise the balance grows because of the fees. assetsToSell, ghoToReceive, _, _ = getAssetAmountForSellAsset(e, minGhoToReceive); uint256 ghoBalanceBefore = balanceOfGho(e, recipient); sellAsset(e, assetsToSell, recipient); uint256 ghoBalanceAfter = balanceOfGho(e, recipient); uint256 balanceDiff = require_uint256(ghoBalanceAfter - ghoBalanceBefore); assert balanceDiff == ghoToReceive; } // @Title 4626: The asset amount deduced from the user's account at `sellAsset(_, maxAsset, _)` is at most `maxAsset` // Check that user's asset balance is decreased correctly. // assets >= balanceDiff // Expected to hold in current implementation. // STATUS: TIMEOUT // https://prover.certora.com/output/33050/9ef597b1a6424528ae96871f69b5d735?anonymousKey=97dcbde8fc3a574d6a23635dfc6ca227d4e145fc rule R3_sellAssetUpdatesAssetBalanceCorrectlyGe { env e; feeLimits(e); priceLimits(e); uint128 assets; address seller = e.msg.sender; address recipient; require e.msg.sender != currentContract; require currentContract.UNDERLYING_ASSET(e) != currentContract.GHO_TOKEN(e); // Inflation prevention! uint256 balanceBefore = balanceOfUnderlying(e, seller); sellAsset(e, assets, recipient); uint256 balanceAfter = balanceOfUnderlying(e, seller); require balanceBefore >= balanceAfter; // To avoid overflows mathint balanceDiff = balanceBefore - balanceAfter; assert to_mathint(assets) >= balanceDiff; } // @Title 4626: The asset amount deduced from the user's account at `sellAsset(_, maxAsset, _)` can be less than `maxAsset` // Check that user's asset balance difference can differ from the assets provided // holds: https://prover.certora.com/output/40748/c4b0691393f4416dbe328f383093ffad?anonymousKey=83439124b153fd20f61457ff3c63da877c6770c3 // (3a) // rule R3a_sellAssetUpdatesAssetBalanceCorrectly { env e; feeLimits(e); priceLimits(e); uint128 assets; address seller = e.msg.sender; address recipient; require e.msg.sender != currentContract; require currentContract.UNDERLYING_ASSET(e) != currentContract.GHO_TOKEN(e); // Inflation prevention! uint256 balanceBefore = balanceOfUnderlying(e, seller); sellAsset(e, assets, recipient); uint256 balanceAfter = balanceOfUnderlying(e, seller); require balanceBefore >= balanceAfter; // To avoid overflows mathint balanceDiff = balanceBefore - balanceAfter; satisfy balanceDiff != to_mathint(assets); } // // @Title 4626: The GHO amount added to the user's account at `sellAsset` is at least the value `x` passed to `getAssetAmountForSellAsset(x)` // // (4) // // Timeout: https://prover.certora.com/output/11775/b2a7e3687b504f3dbe03457b4b5ed3be?anonymousKey=0e6938a302b565c3d5e7b158d4b20a23d2605db1 rule R4_buyGhoUpdatesGhoBalanceCorrectly { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; require currentContract.UNDERLYING_ASSET(e) != currentContract.GHO_TOKEN(e); // Inflation prevention address seller = e.msg.sender; address recipient; require recipient != currentContract; // Otherwise the balance grows because of the fees. uint256 minGhoToSend; uint256 assetsToSpend; assetsToSpend, _, _, _ = getAssetAmountForSellAsset(e, minGhoToSend); require assetsToSpend < max_uint128; uint256 balanceBefore = balanceOfGho(e, recipient); sellAsset(e, assert_uint128(assetsToSpend), recipient); uint256 balanceAfter = balanceOfGho(e, recipient); require balanceAfter >= balanceBefore; // No overflow uint256 balanceDiff = require_uint256(balanceAfter - balanceBefore); assert minGhoToSend <= balanceDiff; } // @Title 4626: The GHO amount added to the user's account at `sellAsset` can be greater than the value `x` passed to `getAssetAmountForSellAsset(x)` // Show that the GHO amount requested by the user to be transferred to the // recipient can be less than what the recipient receives, even when fees are considered. // Holds: https://prover.certora.com/output/40748/c4b0691393f4416dbe328f383093ffad?anonymousKey=83439124b153fd20f61457ff3c63da877c6770c3 // (4a) rule R4a_buyGhoAmountGtGhoBalanceChange { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; require currentContract.UNDERLYING_ASSET(e) != currentContract.GHO_TOKEN(e); // Inflation prevention address seller = e.msg.sender; address recipient; require recipient != currentContract; // Otherwise the balance grows because of the fees. uint256 minGhoToSend; uint256 assetsToSpend; assetsToSpend, _, _, _ = getAssetAmountForSellAsset(e, minGhoToSend); require assetsToSpend < max_uint128; uint256 balanceBefore = balanceOfGho(e, recipient); sellAsset(e, assert_uint128(assetsToSpend), recipient); uint256 balanceAfter = balanceOfGho(e, recipient); require balanceAfter >= balanceBefore; // No overflow uint256 balanceDiff = require_uint256(balanceAfter - balanceBefore); satisfy minGhoToSend < balanceDiff; } ================================================ FILE: certora/gsm/specs/gsm4626/fees-buy-4626.spec ================================================ import "../GsmMethods/erc20.spec"; import "../GsmMethods/methods_divint_summary.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; import "../GsmMethods/erc4626.spec"; using DiffHelper as diffHelper; // ========================= Buying ============================== // @Title 4626: The fee reported by `getBuyFee` is greater than or equal to the fee reported by `getAssetAmountForBuyAsset` // getBuyFee -(>=)-> getAssetAmountForBuyAsset // Shows >= // Holds: https://prover.certora.com/output/40748/b8b526129e114ca9b3e7dcdcdf3d2fd4?anonymousKey=d1a47509f71c924af60b0b38ec1b3dcd9fe0ae63 // (1) rule R1_getBuyFeeGeGetAssetAmountForBuyAsset { env e; feeLimits(e); priceLimits(e); uint128 ghoAmount; uint256 estimatedBuyFee = getBuyFee(e, ghoAmount); require estimatedBuyFee + ghoAmount <= max_uint256; uint256 amountOfGhoToSell = assert_uint256(estimatedBuyFee + ghoAmount); uint256 fee; _, _, _, fee = getAssetAmountForBuyAsset(e, amountOfGhoToSell); assert fee <= estimatedBuyFee; } // @Title 4626: The fee reported by `getBuyFee` can be greater than the fee reported by `getAssetAmountForBuyAsset` // getBuyFee -(>=)-> getAssetAmountForBuyAsset // Shows > // Holds: https://prover.certora.com/output/40748/b8b526129e114ca9b3e7dcdcdf3d2fd4?anonymousKey=d1a47509f71c924af60b0b38ec1b3dcd9fe0ae63 // (1a) // Expected to hold in the current implementation rule R1a_getBuyFeeNeGetAssetAmountForBuyAsset { env e; feeLimits(e); priceLimits(e); require e.msg.sender != currentContract; // Otherwise the fee in GHO will come back to me, messing up the balance calculation uint128 ghoAmount; uint256 estimatedBuyFee = getBuyFee(e, ghoAmount); require estimatedBuyFee + ghoAmount <= max_uint256; uint256 amountOfGhoToSell = assert_uint256(estimatedBuyFee + ghoAmount); uint256 fee; _, _, _, fee = getAssetAmountForBuyAsset(e, amountOfGhoToSell); satisfy fee < estimatedBuyFee; } // @Title 4626: The fee reported by `getAssetAmountForBuyAsset` is equal to the fee accrued by `buyAsset` // getAssetAmountForBuyAsset -(==)-> buyAsset // Show == // (2) // holds: https://prover.certora.com/output/40748/b8b526129e114ca9b3e7dcdcdf3d2fd4?anonymousKey=d1a47509f71c924af60b0b38ec1b3dcd9fe0ae63 rule R2_getAssetAmountForBuyAssetNeBuyAssetFee { env e; feeLimits(e); priceLimits(e); address receiver; uint256 preAccruedFees = currentContract._accruedFees; uint256 amountOfGhoToSell; uint256 estimatedFee; uint256 assetAmount; assetAmount, _, _, estimatedFee = getAssetAmountForBuyAsset(e, amountOfGhoToSell); require assetAmount <= max_uint128; // No overflow require getExcess(e) == 0; // Are we blocking important executions? buyAsset(e, assert_uint128(assetAmount), receiver); uint256 postAccruedFees = currentContract._accruedFees; uint256 actualFee = assert_uint256(postAccruedFees - preAccruedFees); assert estimatedFee == actualFee; } // @Title 4626: The fee reported by `getAssetAmountForBuyAsset` is equal to the fee accrued by `getBuyFee` // getAssetAmountForBuyAssetFee -(==)-> getBuyFee // Shows == // Holds. https://prover.certora.com/output/40748/b8b526129e114ca9b3e7dcdcdf3d2fd4?anonymousKey=d1a47509f71c924af60b0b38ec1b3dcd9fe0ae63 // (3) rule R3_getAssetAmountForBuyAssetFeeEqGetBuyFee { env e; feeLimits(e); priceLimits(e); uint256 estimatedFee; uint256 grossGho; uint256 amountOfGhoToSellWithFee; _, _, grossGho, estimatedFee = getAssetAmountForBuyAsset(e, amountOfGhoToSellWithFee); uint256 fee = getBuyFee(e, grossGho); assert fee == estimatedFee; } // @Title 4626: The fee reported by `getBuyFee` is greater than or equal to the fee accrued by `buyAsset` // getBuyFee -(>=)-> buyAsset // shows that estimatedBuyFee >= actualFee. // Holds: https://prover.certora.com/output/40748/b8b526129e114ca9b3e7dcdcdf3d2fd4?anonymousKey=d1a47509f71c924af60b0b38ec1b3dcd9fe0ae63 // (4) rule R4_estimatedBuyFeeLtActualBuyFee { env e; feeLimits(e); priceLimits(e); uint128 ghoAmount; address receiver; uint256 preAccruedFees = currentContract._accruedFees; uint256 estimatedBuyFee = getBuyFee(e, ghoAmount); require estimatedBuyFee + ghoAmount <= max_uint256; uint256 amountOfGhoToSell = assert_uint256(estimatedBuyFee + ghoAmount); uint256 assetAmount; assetAmount, _, _, _ = getAssetAmountForBuyAsset(e, amountOfGhoToSell); require assetAmount <= max_uint128; // No overflow require getExcess(e) == 0; // Are we blocking important executions? buyAsset(e, assert_uint128(assetAmount), receiver); uint256 postAccruedFees = currentContract._accruedFees; uint256 actualFee = assert_uint256(postAccruedFees - preAccruedFees); assert estimatedBuyFee >= actualFee; } // @Title 4626: The fee reported by `getBuyFee` can be greater than the fee deduced by `buyAsset` // getBuyFee -(>=)-> buyAsset // shows that the estimated fee can be > than actual fee (but isn't necessarily always) // Holds: https://prover.certora.com/output/40748/b8b526129e114ca9b3e7dcdcdf3d2fd4?anonymousKey=d1a47509f71c924af60b0b38ec1b3dcd9fe0ae63 // (4a) rule R4a_estimatedBuyFeeGtActualBuyFee { env e; feeLimits(e); priceLimits(e); uint256 priceRatio = getPriceRatio(e); uint128 ghoAmount; address receiver; uint256 preAccruedFees = currentContract._accruedFees; uint256 estimatedBuyFee = getBuyFee(e, ghoAmount); require estimatedBuyFee + ghoAmount <= max_uint256; uint256 amountOfGhoToSell = assert_uint256(estimatedBuyFee + ghoAmount); uint256 assetAmount; assetAmount, _, _, _ = getAssetAmountForBuyAsset(e, amountOfGhoToSell); require assetAmount <= max_uint128; // No overflow require getExcess(e) == 0; // Are we blocking important executions? buyAsset(e, assert_uint128(assetAmount), receiver); uint256 postAccruedFees = currentContract._accruedFees; uint256 actualFee = assert_uint256(postAccruedFees - preAccruedFees); satisfy estimatedBuyFee > actualFee; } // @Title 4626: The fee reported by `getBuyFee` can be equal to the fee reported by `buyAsset` // getBuyFee -(>=)-> buyAsset // shows that the fee can be correct (but isn't necessarily always) // (4b) // Holds: https://prover.certora.com/output/40748/b8b526129e114ca9b3e7dcdcdf3d2fd4?anonymousKey=d1a47509f71c924af60b0b38ec1b3dcd9fe0ae63 rule R4b_estimatedBuyFeeEqActualBuyFee { env e; feeLimits(e); priceLimits(e); uint128 ghoAmount; address receiver; uint256 preAccruedFees = currentContract._accruedFees; uint256 estimatedBuyFee = getBuyFee(e, ghoAmount); require estimatedBuyFee + ghoAmount <= max_uint256; uint256 amountOfGhoToSell = assert_uint256(estimatedBuyFee + ghoAmount); uint256 assetAmount; assetAmount, _, _, _ = getAssetAmountForBuyAsset(e, amountOfGhoToSell); require assetAmount <= max_uint128; // No overflow require getExcess(e) == 0; // Are we blocking important executions? buyAsset(e, assert_uint128(assetAmount), receiver); uint256 postAccruedFees = currentContract._accruedFees; uint256 actualFee = assert_uint256(postAccruedFees - preAccruedFees); satisfy estimatedBuyFee == actualFee; } ================================================ FILE: certora/gsm/specs/gsm4626/fees-sell-4626.spec ================================================ import "../GsmMethods/erc20.spec"; import "../GsmMethods/methods_divint_summary.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; import "../GsmMethods/erc4626.spec"; using DiffHelper as diffHelper; // Study how well the estimated fees match the actual fees. // ========================= Selling ============================== // @Title 4626: The fee reported by `getAssetAmountForSellAsset` is greater than or equal to the fee reported by `getSellFee` // getAssetAmountForSellAssetFee -(>=)-> getSellFee // Shows >= // (1) // holds: https://prover.certora.com/output/40748/423580bb38c141b983906c061c39313a?anonymousKey=c1f615e893cdc4549b5b00138550cb8921d7703c rule R1_getAssetAmountForSellAssetFeeGeGetSellFee { env e; feeLimits(e); priceLimits(e); uint256 estimatedFee; uint256 amountOfGhoToBuy; uint256 exactAmountOfGhoToReceive; _, exactAmountOfGhoToReceive, _, estimatedFee = getAssetAmountForSellAsset(e, amountOfGhoToBuy); uint256 fee = getSellFee(e, amountOfGhoToBuy); assert estimatedFee >= fee; } // @Title 4626: The fee reported by `getAssetAmountForSellAsset` can be greater than the fee reported by `getSellFee` // getAssetAmountForSellAssetFee -(>=)-> getSellFee // Shows != // (1a) // Holds: https://prover.certora.com/output/40748/423580bb38c141b983906c061c39313a?anonymousKey=c1f615e893cdc4549b5b00138550cb8921d7703c rule R1a_getAssetAmountForSellAssetFeeNeGetSellFee { env e; feeLimits(e); priceLimits(e); uint256 estimatedFee; uint256 amountOfGhoToBuy; uint256 exactAmountOfGhoToReceive; _, exactAmountOfGhoToReceive, _, estimatedFee = getAssetAmountForSellAsset(e, amountOfGhoToBuy); uint256 fee = getSellFee(e, exactAmountOfGhoToReceive); satisfy fee != estimatedFee; } // @Title 4626: The fee reported by `getAssetAmountForSellAsset` can be greater than or equal to the fee deducted by `sellAsset` // getAssetAmountForSellAsset -(>=)-> sellAsset // Shows >= // (2) // holds: https://prover.certora.com/output/40748/423580bb38c141b983906c061c39313a?anonymousKey=c1f615e893cdc4549b5b00138550cb8921d7703c rule R2_getAssetAmountForSellAssetVsActualSellFee { env e; feeLimits(e); priceLimits(e); uint256 assetAmount; uint256 estimatedFee; uint256 amountOfGhoToBuy; address receiver; uint256 preAccruedFees = currentContract._accruedFees; assetAmount, _, _, estimatedFee = getAssetAmountForSellAsset(e, amountOfGhoToBuy); sellAsset(e, require_uint128(assetAmount), receiver); uint256 postAccruedFees = currentContract._accruedFees; uint256 actualFee = require_uint256(postAccruedFees - preAccruedFees); assert estimatedFee >= actualFee; } // @Title 4626: The fee reported by `getAssetAmountForSellAsset` may differ from the fee deducted by `sellAsset` // getAssetAmountForSellAsset -(>=)-> sellAsset // Shows != // (2a) // Holds: https://prover.certora.com/output/40748/423580bb38c141b983906c061c39313a?anonymousKey=c1f615e893cdc4549b5b00138550cb8921d7703c rule R2a_getAssetAmountForSellAssetNeActualSellFee { env e; feeLimits(e); priceLimits(e); uint256 assetAmount; uint256 estimatedFee; uint256 amountOfGhoToBuy; address receiver; uint256 preAccruedFees = currentContract._accruedFees; assetAmount, _, _, estimatedFee = getAssetAmountForSellAsset(e, amountOfGhoToBuy); sellAsset(e, require_uint128(assetAmount), receiver); uint256 postAccruedFees = currentContract._accruedFees; uint256 actualFee = require_uint128(postAccruedFees - preAccruedFees); satisfy estimatedFee != actualFee; } // @Title 4626: The fee reported by `getSellFee` is less than or equal to the fee deduced by `sellAsset` // getSellFee -(<=)-> sellAsset // shows <= // (3) // Times out // Solved for 6, 8, 9, 10, 11, 14, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27 in // https://prover.certora.com/output/40748/0e599978d9a2421ab3bb9d8590136afb/?anonymousKey=0da77eb239ceb2c4c30b330b50e61769e5168644 // Solved for 5, 13, 15 in // https://prover.certora.com/output/40748/a022ef5dd25d40aa9baecf9d14866007/?anonymousKey=a07d940634ade0c0004dc30cee0375ad5ac36759 // Solved for 16 in // https://prover.certora.com/output/40748/f18e0f09d7044d4e847dffc601e08299/?anonymousKey=1c83f325ae45de355f204caed9b67cf99e18bc06 // Solved for 7, 12: // https://prover.certora.com/output/40748/a79ba1aab3794e3a82f8671ab7a69f0e/?anonymousKey=dcb36001e071fd323ca66dcba6872b7102e301d0 // STATUS: TIMEOUT // https://prover.certora.com/output/33050/e73527d566564185904c2359fc1c06ac?anonymousKey=9dbb56ece4c3d87b617bcabd9819a794c0bcacbf // rule R3_estimatedSellFeeCanBeHigherThanActualSellFee { // env e; // feeLimits(e); // priceLimits(e); // uint128 ghoAmount; // address receiver; // uint256 preAccruedFees = currentContract._accruedFees; // uint256 estimatedSellFee = getSellFee(e, ghoAmount); // require ghoAmount <= max_uint128; // require estimatedSellFee <= max_uint128; // uint256 assetAmount; // assetAmount, _, _, _ = getAssetAmountForSellAsset(e, ghoAmount); // sellAsset(e, require_uint128(assetAmount), receiver); // uint256 postAccruedFees = currentContract._accruedFees;` // uint256 actualFee = require_uint256(postAccruedFees - preAccruedFees); // assert estimatedSellFee <= actualFee; // } // @Title 4626: The fee reported by `getSellFee` can be less than the fee deduced by `sellAsset` // getSellFee -(<=)-> sellAsset // shows < // (3a) // Holds: https://prover.certora.com/output/40748/423580bb38c141b983906c061c39313a?anonymousKey=c1f615e893cdc4549b5b00138550cb8921d7703c rule R3a_estimatedSellFeeCanBeLowerThanActualSellFee { env e; feeLimits(e); priceLimits(e); uint128 ghoAmount; address receiver; uint256 preAccruedFees = currentContract._accruedFees; uint256 estimatedSellFee = getSellFee(e, ghoAmount); require ghoAmount <= max_uint128; require estimatedSellFee <= max_uint128; uint256 assetAmount; assetAmount, _, _, _ = getAssetAmountForSellAsset(e, ghoAmount); sellAsset(e, require_uint128(assetAmount), receiver); uint256 postAccruedFees = currentContract._accruedFees; uint256 actualFee = require_uint256(postAccruedFees - preAccruedFees); satisfy estimatedSellFee < actualFee; } // @Title 4626: The fee reported by `getSellFee` can be equal to the fee deduced by `sellAsset` // getSellFee -(<=>)-> sellAsset // shows == // (3b) // Holds: https://prover.certora.com/output/40748/423580bb38c141b983906c061c39313a?anonymousKey=c1f615e893cdc4549b5b00138550cb8921d7703c rule R3b_estimatedSellFeeEqActualSellFee { env e; feeLimits(e); priceLimits(e); uint128 ghoAmount; address receiver; uint256 preAccruedFees = currentContract._accruedFees; uint256 estimatedSellFee = getSellFee(e, ghoAmount); require ghoAmount <= max_uint128; require estimatedSellFee <= max_uint128; uint256 assetAmount; assetAmount, _, _, _ = getAssetAmountForSellAsset(e, ghoAmount); sellAsset(e, require_uint128(assetAmount), receiver); uint256 postAccruedFees = currentContract._accruedFees; uint256 actualFee = require_uint256(postAccruedFees - preAccruedFees); satisfy estimatedSellFee == actualFee; } // @Title 4626: the fee reported by `getSellFee` is less than or equal to the fee reported by `getAssetAmountForSellAsset` // getSellFee -(<=)-> getAssetAmountForSellAsset // (4) // Holds: https://prover.certora.com/output/40748/423580bb38c141b983906c061c39313a?anonymousKey=c1f615e893cdc4549b5b00138550cb8921d7703c rule R4_getSellFeeVsgetAssetAmountForSellAsset { env e; feeLimits(e); priceLimits(e); uint256 ghoAmount; uint256 estimatedSellFee; uint256 sellFee; estimatedSellFee = getSellFee(e, ghoAmount); _, _, _, sellFee = getAssetAmountForSellAsset(e, ghoAmount); assert estimatedSellFee <= sellFee; } // @Title 4626: the fee reported by `getSellFee` can be less than the fee reported by `getAssetAmountForSellAsset` // getSellFee -(<=)-> getAssetAmountForSellAsset // (4a) // Shows < // Holds: https://prover.certora.com/output/40748/423580bb38c141b983906c061c39313a?anonymousKey=c1f615e893cdc4549b5b00138550cb8921d7703c rule R4a_getSellFeeVsgetAssetAmountForSellAsset { env e; feeLimits(e); priceLimits(e); uint256 ghoAmount; uint256 estimatedSellFee; uint256 sellFee; estimatedSellFee = getSellFee(e, ghoAmount); _, _, _, sellFee = getAssetAmountForSellAsset(e, ghoAmount); satisfy estimatedSellFee < sellFee; } // @Title 4626: the fee reeported by `getSellFee` can be equal to to the fee reported by `getAssetAmountForSellAsset` // getSellFee -(<=)-> getAssetAmountForSellAsset // (4b) // Shows = // Holds: https://prover.certora.com/output/40748/423580bb38c141b983906c061c39313a?anonymousKey=c1f615e893cdc4549b5b00138550cb8921d7703c rule R4b_getSellFeeVsgetAssetAmountForSellAsset { env e; feeLimits(e); priceLimits(e); uint256 ghoAmount; uint256 estimatedSellFee; uint256 sellFee; estimatedSellFee = getSellFee(e, ghoAmount); _, _, _, sellFee = getAssetAmountForSellAsset(e, ghoAmount); satisfy estimatedSellFee == sellFee; } ================================================ FILE: certora/gsm/specs/gsm4626/getAmount_4626_properties.spec ================================================ import "../GsmMethods/methods4626_base.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; import "../GsmMethods/methods_divint_summary.spec"; import "../GsmMethods/erc4626.spec"; // @title The amount of asset returned is less than or equal to given param // STATUS: PASS // https://prover.certora.com/output/11775/66c7c0a501d04b7e815fcd13680c087d?anonymousKey=6c44ce466f01c24f3e7d5432b4ddd2b8170da571 rule getAssetAmountForBuyAsset_correctness() { env e; feeLimits(e); priceLimits(e); uint maxToGive; require maxToGive > 0; uint suggestedAssetToBuy; suggestedAssetToBuy, _, _, _ = getAssetAmountForBuyAsset(e, maxToGive); uint reallyPaid; _, reallyPaid, _, _ = getGhoAmountForBuyAsset(e, suggestedAssetToBuy); assert reallyPaid <= maxToGive; } // @title the amount given should be at most 1 more than the max amount specified // STATUS: PASS // https://prover.certora.com/output/11775/66c7c0a501d04b7e815fcd13680c087d?anonymousKey=6c44ce466f01c24f3e7d5432b4ddd2b8170da571 rule getAssetAmountForBuyAsset_correctness_bound1() { env e; feeLimits(e); priceLimits(e); uint maxToGive; uint suggestedAssetToBuy; suggestedAssetToBuy, _, _, _ = getAssetAmountForBuyAsset(e, maxToGive); uint reallyPaid; _, reallyPaid, _, _ = getGhoAmountForBuyAsset(e, suggestedAssetToBuy); assert reallyPaid <= require_uint256(maxToGive + 1); } // @title the amount given should be at most 1 more than the max amount specified // STATUS: PASS // https://prover.certora.com/output/11775/66c7c0a501d04b7e815fcd13680c087d?anonymousKey=6c44ce466f01c24f3e7d5432b4ddd2b8170da571 rule getAssetAmountForBuyAsset_correctness_bound2() { env e; feeLimits(e); priceLimits(e); uint maxToGive; uint suggestedAssetToBuy; suggestedAssetToBuy, _, _, _ = getAssetAmountForBuyAsset(e, maxToGive); uint reallyPaid; _, reallyPaid, _, _ = getGhoAmountForBuyAsset(e, suggestedAssetToBuy); assert reallyPaid <= require_uint256(maxToGive + 2); } // @title The amount of gho returned is greater than or equal to given param // STATUS: PASS // https://prover.certora.com/output/6893/9b3b580e82f8497f87ab1f7f169715b8/?anonymousKey=e6c627441f4110e51467815149500a78d8f3765a rule getGhoAmountForBuyAsset_correctness() { env e; feeLimits(e); priceLimits(e); uint256 minAssetAmount; uint suggestedAssetToBuy; suggestedAssetToBuy, _, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount); assert suggestedAssetToBuy >= minAssetAmount; } // @title suggested asset amount is upto 1 less than the miss asset amount // STATUS: PASS // https://prover.certora.com/output/11775/66c7c0a501d04b7e815fcd13680c087d?anonymousKey=6c44ce466f01c24f3e7d5432b4ddd2b8170da571 rule getGhoAmountForBuyAsset_correctness_bound1() { env e; feeLimits(e); priceLimits(e); uint256 minAssetAmount; uint suggestedAssetToBuy; suggestedAssetToBuy, _, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount); assert require_uint256(suggestedAssetToBuy + 1) >= minAssetAmount; } // @title The amount of asset returned is greater than or equal to given param. // STATUS: PASS // // https://prover.certora.com/output/11775/66c7c0a501d04b7e815fcd13680c087d?anonymousKey=6c44ce466f01c24f3e7d5432b4ddd2b8170da571 rule getAssetAmountForSellAsset_correctness() { env e; feeLimits(e); priceLimits(e); uint minimumToReceive; require minimumToReceive > 0; uint suggestedAssetToSell; suggestedAssetToSell, _, _, _ = getAssetAmountForSellAsset(e, minimumToReceive); uint reallyReceived; _, reallyReceived, _, _ = getGhoAmountForSellAsset(e, suggestedAssetToSell); assert reallyReceived >= minimumToReceive; } // @title The amount of gho returned is less than or equal to given param. // STATUS: PASS // https://prover.certora.com/output/11775/66c7c0a501d04b7e815fcd13680c087d?anonymousKey=6c44ce466f01c24f3e7d5432b4ddd2b8170da571 rule getGhoAmountForSellAsset_correctness() { env e; feeLimits(e); priceLimits(e); uint maxAssetAmount; uint suggestedAssetToSell; suggestedAssetToSell, _, _, _ = getGhoAmountForSellAsset(e, maxAssetAmount); assert suggestedAssetToSell <= maxAssetAmount; } // @title getAssetAmountForBuyAsset returns value that is as close as possible to user specified amount. // STATUS: PASS // https://prover.certora.com/output/11775/66c7c0a501d04b7e815fcd13680c087d?anonymousKey=6c44ce466f01c24f3e7d5432b4ddd2b8170da571 rule getAssetAmountForBuyAsset_optimality() { env e; feeLimits(e); priceLimits(e); uint maxToGive; uint suggestedAssetToBuy; suggestedAssetToBuy, _, _, _ = getAssetAmountForBuyAsset(e, maxToGive); uint suggestedGhoToPay; _, suggestedGhoToPay, _, _ = getGhoAmountForBuyAsset(e, suggestedAssetToBuy); uint maxCouldBuy; uint couldBuy; uint couldPay; couldBuy, couldPay, _, _ = getGhoAmountForBuyAsset(e, maxCouldBuy); require couldPay <= maxToGive; require couldPay >= suggestedGhoToPay; assert couldBuy <= suggestedAssetToBuy; } // @title getGhoAmountForBuyAsset returns value that is as close as possible to user specified amount. // STATUS: PASS // https://prover.certora.com/output/11775/c3036f0fb1c344e2ab8c3f38bf9438af?anonymousKey=f0fd891d0add2cf779b3473b67296b97dd769a8a rule getGhoAmountForBuyAsset_optimality() { env e; feeLimits(e); priceLimits(e); uint minAssetToBuy; uint suggestedAssetToBuy; uint suggestedGhoToSpend; suggestedAssetToBuy, suggestedGhoToSpend, _, _ = getGhoAmountForBuyAsset(e, minAssetToBuy); uint min2AssetsToBuy; uint couldBuy; uint couldPay; couldBuy, couldPay, _, _ = getGhoAmountForBuyAsset(e, min2AssetsToBuy); require couldBuy >= minAssetToBuy; //require couldPay >= suggestedGhoToPay; assert couldPay >= suggestedGhoToSpend; } // @title getGhoAmountForSellAsset returns value that is as close as possible to user specified amount. // STATUS: PASS // https://prover.certora.com/output/6893/9b3b580e82f8497f87ab1f7f169715b8/?anonymousKey=e6c627441f4110e51467815149500a78d8f3765a rule getGhoAmountForSellAsset_optimality() { env e; feeLimits(e); priceLimits(e); uint maxAssetToSell; uint suggestedAssetToSell; uint suggestedGhoToGain; suggestedAssetToSell, suggestedGhoToGain, _, _ = getGhoAmountForSellAsset(e, maxAssetToSell); uint maxAssetToSell2; uint couldSell; uint couldGain; couldSell, couldGain, _, _ = getGhoAmountForSellAsset(e, maxAssetToSell2); require couldSell <= maxAssetToSell; //require couldPay >= suggestedGhoToPay; assert suggestedGhoToGain >= couldGain; } // @title getAssetAmountForSellAsset returns value that is as close as possible to user specified amount. // STATUS: PASS // https://prover.certora.com/output/11775/1c7f7d0151f04b2c9a68f12f161a7a3f?anonymousKey=7efd045107e4779246295b692ecaf169c5b2c280 rule getAssetAmountForSellAsset_optimality() { // proves that if user wants to receive at least X gho // and the system tells them to sell Y assets, // then there is no amount W < Y that would already provide X gho. env e; feeLimits(e); priceLimits(e); uint wantsToReceive; uint suggestedAssetToSell; suggestedAssetToSell, _, _, _ = getAssetAmountForSellAsset(e, wantsToReceive); uint reallySold; uint reallyReceived; _, reallyReceived, _, _ = getGhoAmountForSellAsset(e, reallySold); require reallyReceived >= wantsToReceive; assert suggestedAssetToSell <= reallySold; } // @title getAssetAmountForBuyAsset returns value that is as close as possible to user specified amount. // STATUS: PASS // https://prover.certora.com/output/33050/f360ab36c2564a069784bc859d6d4c7e?anonymousKey=e0c9610f8e7d6c2e1c78d70708b8fec9b04ee505 rule getAssetAmountForBuyAsset_funcProperty() { // if (A, B, _, _) = getAssetAmountForBuyAsset(X) then B is function of A env e; feeLimits(e); priceLimits(e); uint256 amount1; uint suggestedAssetToBuy1; uint totalPay1; suggestedAssetToBuy1, totalPay1, _, _ = getAssetAmountForBuyAsset(e, amount1); uint256 amount2; uint suggestedAssetToBuy2; uint totalPay2; suggestedAssetToBuy2, totalPay2, _, _ = getAssetAmountForBuyAsset(e, amount2); assert (suggestedAssetToBuy1 == suggestedAssetToBuy2) == (totalPay1 == totalPay2); } // @title The first two return values of getGhoAmountForBuyAsset are univalent (https://en.wikipedia.org/wiki/Binary_relation#Specific_types_of_binary_relations) // STATUS: TIMEOUT // https://prover.certora.com/output/11775/740d89f59d5b4bd689d5e71742b9014e?anonymousKey=fdd7be2db7b1db552afc7fa7bcbbd89983bd6bd1 // rule getGhoAmountForBuyAsset_funcProperty() // { // // if (A, B, _, _) = getGhoAmountForBuyAsset(X) then B is function of A // env e; // feeLimits(e); // priceLimits(e); // uint256 amount1; // uint suggestedAssetToBuy1; // uint totalPay1; // suggestedAssetToBuy1, totalPay1, _, _ = getGhoAmountForBuyAsset(e, amount1); // uint256 amount2; // uint suggestedAssetToBuy2; // uint totalPay2; // suggestedAssetToBuy2, totalPay2, _, _ = getGhoAmountForBuyAsset(e, amount2); // assert (suggestedAssetToBuy1 == suggestedAssetToBuy2) == // (totalPay1 == totalPay2); // } // @title The first two return values of getAssetAmountForSellAsset are univalent (https://en.wikipedia.org/wiki/Binary_relation#Specific_types_of_binary_relations) // STATUS: TIMEOUT // https://prover.certora.com/output/11775/bde7981ff4f64a04b995ddff49b4b153?anonymousKey=cf1b5e409d9d9e37dc6320d5382c562bc4144664 // rule getAssetAmountForSellAsset_funcProperty() // { // // if (A, B, _, _) = getAssetAmountForSellAsset(X) then B is function of A // env e; // feeLimits(e); // priceLimits(e); // uint256 amount1; // uint suggestedAsset1; // uint totalPay1; // suggestedAsset1, totalPay1, _, _ = getAssetAmountForSellAsset(e, amount1); // uint256 amount2; // uint suggestedAsset2; // uint totalPay2; // suggestedAsset2, totalPay2, _, _ = getAssetAmountForSellAsset(e, amount2); // assert (suggestedAsset1 == suggestedAsset2) == // (totalPay1 == totalPay2); // } // @title The first two return values of getGhoAmountForSellAsset are univalent (https://en.wikipedia.org/wiki/Binary_relation#Specific_types_of_binary_relations) // STATUS: TIMEOUT // https://prover.certora.com/output/11775/291150d123e04ee29541a3cd0763eb9c?anonymousKey=cee781030122979f034823769c6705c26869f5b8 // rule getGhoAmountForSellAsset_funcProperty() // { // // if (A, B, _, _) = getGhoAmountForSellAsset(X) then B is function of A // env e; // feeLimits(e); // priceLimits(e); // uint256 amount1; // uint suggestedAsset1; // uint totalPay1; // suggestedAsset1, totalPay1, _, _ = getGhoAmountForSellAsset(e, amount1); // uint256 amount2; // uint suggestedAsset2; // uint totalPay2; // suggestedAsset2, totalPay2, _, _ = getGhoAmountForSellAsset(e, amount2); // assert (suggestedAsset1 == suggestedAsset2) == // (totalPay1 == totalPay2); // } // @title getGhoAmountForBuyAsset is additive. Making two small transactions x1, x2, is less favourable for the user than making (x1+x2) // STATUS: TIMEOUT // https://prover.certora.com/output/11775/ebb8f639ebb74796802fe08c55ddfd6c?anonymousKey=be2f94647809d3c634f1e653f572385902452b07 // rule getGhoAmountForBuyAsset_aditivity() // { // env e; // feeLimits(e); // priceLimits(e); // uint256 minAssetAmount1; // uint bought1; // uint paid1; // bought1, paid1, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount1); // uint256 minAssetAmount2; // uint bought2; // uint paid2; // bought2, paid2, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount2); // require require_uint256(bought1 + bought2) > 0; // uint256 minAssetAmount3; // uint bought3; // uint paid3; // bought3, paid3, _, _ = getGhoAmountForBuyAsset(e, minAssetAmount3); // assert require_uint256(bought1 + bought2) >= bought3 => // require_uint256(paid1 + paid2) >= paid3; // } // @title getAssetAmountForBuyAsset is additive. Making two small transactions x1, x2, is less favourable for the user than making (x1+x2) // STATUS: TIMEOUT // https://prover.certora.com/output/11775/c5216b2a5ae54598a471c536f368501f?anonymousKey=1bfd46b0d930b3860ddf12f3f2450eadecd6d482 // rule getAssetAmountForBuyAsset_aditivity() // { // env e; // feeLimits(e); // priceLimits(e); // uint256 maxGhoAmount1; // uint bought1; // uint paid1; // bought1, paid1, _, _ = getAssetAmountForBuyAsset(e, maxGhoAmount1); // uint256 maxGhoAmount2; // uint bought2; // uint paid2; // bought2, paid2, _, _ = getAssetAmountForBuyAsset(e, maxGhoAmount2); // require require_uint256(bought1 + bought2) > 0; // uint256 maxGhoAmount3; // uint bought3; // uint paid3; // bought3, paid3, _, _ = getAssetAmountForBuyAsset(e, maxGhoAmount3); // assert require_uint256(bought1 + bought2) >= bought3 => // require_uint256(paid1 + paid2) >= paid3; // } // @title getGhoAmountForSellAsset is additive. Making two small transactions x1, x2, is less favourable for the user than making (x1+x2) // STATUS: TIMEOUT // https://prover.certora.com/output/11775/4eb683e5162640f599f80f5afb59fdb9?anonymousKey=da8944168ada87b4d556dccb77f240a62f481ece // rule getGhoAmountForSellAsset_aditivity() // { // env e; // feeLimits(e); // priceLimits(e); // uint256 amount1; // uint suggestedAsset1; // uint totalGained1; // suggestedAsset1, totalGained1, _, _ = getGhoAmountForSellAsset(e, amount1); // uint256 amount2; // uint suggestedAsset2; // uint totalGained2; // suggestedAsset2, totalGained2, _, _ = getGhoAmountForSellAsset(e, amount2); // require require_uint256(suggestedAsset1 + suggestedAsset2) > 0; // uint256 amount3; // uint suggestedAsset3; // uint totalGained3; // suggestedAsset3, totalGained3, _, _ = getGhoAmountForSellAsset(e, amount3); // assert require_uint256(suggestedAsset1 + suggestedAsset2) <= suggestedAsset3 => // require_uint256(totalGained1 + totalGained2) <= totalGained3; // } // @title getAssetAmountForSellAsset is additive. Making two small transactions x1, x2, is less favourable for the user than making (x1+x2) // STATUS: TIMEOUT // https://prover.certora.com/output/11775/8ee8e360d1c64478961c9ba80565c5cd?anonymousKey=4ed0353a58d71ae7f863097cbb25884ace721234 // rule getAssetAmountForSellAsset_aditivity() // { // env e; // feeLimits(e); // priceLimits(e); // uint256 amount1; // uint suggestedAsset1; // uint totalGained1; // suggestedAsset1, totalGained1, _, _ = getAssetAmountForSellAsset(e, amount1); // uint256 amount2; // uint suggestedAsset2; // uint totalGained2; // suggestedAsset2, totalGained2, _, _ = getAssetAmountForSellAsset(e, amount2); // require require_uint256(suggestedAsset1 + suggestedAsset2) > 0; // uint256 amount3; // uint suggestedAsset3; // uint totalGained3; // suggestedAsset3, totalGained3, _, _ = getAssetAmountForSellAsset(e, amount3); // assert require_uint256(suggestedAsset1 + suggestedAsset2) <= suggestedAsset3 => // require_uint256(totalGained1 + totalGained2) <= totalGained3; // } ================================================ FILE: certora/gsm/specs/gsm4626/gho-gsm-finishedRules4626.spec ================================================ import "../GsmMethods/methods4626_base.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; import "../GsmMethods/methods_divint_summary.spec"; import "../GsmMethods/erc4626.spec"; // @title Rescuing GHO never lefts less GHO available than _accruedFees. // STATUS: PASSED // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule rescuingGhoKeepsAccruedFees() { address token; address to; uint256 amount; env e; feeLimits(e); priceLimits(e); require token == GHO_TOKEN(e); rescueTokens(e, token, to, amount); assert getCurrentGhoBalance(e) >= getAccruedFee(e); } // @title Rescuing underlying never lefts less underlying available than _currentExposure. //Rescuing the underlying asset should never result in there being less of the underlying (as an ERC-20 balance) than the combined total of the _currentExposure and _tokenizedAssets. // STATUS: PASSED // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule rescuingAssetKeepsAccruedFees() { address token; address to; uint256 amount; env e; feeLimits(e); priceLimits(e); require token == UNDERLYING_ASSET(e); rescueTokens(e, token, to, amount); assert getCurrentUnderlyingBalance(e) >= assert_uint256(getCurrentExposure(e)); // + getTokenizedAssets(e)); } // @title buyAsset decreases _currentExposure //When calling buyAsset successfully (i.e., no revert), the _currentExposure should always decrease. // STATUS: PASSED // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule buyAssetDecreasesExposure() { env e; feeLimits(e); priceLimits(e); uint128 amount; address receiver; uint exposureBefore = getCurrentExposure(e); require amount > 0; buyAsset(e, amount, receiver); assert getCurrentExposure(e) < exposureBefore; } // @title sellAsset increases _currentExposure //When calling sellAsset successfully (i.e., no revert), the _currentExposure should always increase. // STATUS: PASSED // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule sellAssetIncreasesExposure() { env e; feeLimits(e); priceLimits(e); uint128 amount; address receiver; uint exposureBefore = getCurrentExposure(e); require amount > 0; sellAsset(e, amount, receiver); assert getCurrentExposure(e) > exposureBefore; } // @title If _currentExposure exceeds _exposureCap, sellAsset reverts. // STATUS: VIOLATED // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e // rule cantSellIfExposureTooHigh() // { // env e; // feeLimits(e); // priceLimits(e); // uint128 amount; // address receiver; // require require_uint256(getCurrentExposure(e) + amount) > getExposureCap(e); // sellAsset@withrevert(e, amount, receiver); // assert lastReverted; // } definition canChangeExposureCap(method f) returns bool = f.selector == sig:updateExposureCap(uint128).selector || f.selector == sig:initialize(address,address,uint128).selector|| f.selector == sig:seize().selector; // @title Only updateExposureCap, initialize, seize can change exposureCap. // STATUS: PASS // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule whoCanChangeExposureCap(method f) { env e; feeLimits(e); priceLimits(e); uint256 exposureCapBefore = getExposureCap(e); calldataarg args; f(e, args); uint256 exposureCapAfter = getExposureCap(e); assert exposureCapAfter != exposureCapBefore => canChangeExposureCap(f), "should not change exposure cap"; } // @title Cannot buy or sell if the GSM is frozen. // STATUS: PASS // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule cantBuyOrSellWhenFrozen() { env e; feeLimits(e); priceLimits(e); uint128 amount; address receiver; require getIsFrozen(e); buyAsset@withrevert(e, amount, receiver); assert lastReverted; sellAsset@withrevert(e, amount, receiver); assert lastReverted; } // @title Cannot buy or sell if the GSM is seized. // STATUS: PASS // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule cantBuyOrSellWhenSeized() { env e; feeLimits(e); priceLimits(e); uint128 amount; address receiver; require getIsSeized(e); buyAsset@withrevert(e, amount, receiver); assert lastReverted; sellAsset@withrevert(e, amount, receiver); assert lastReverted; } definition canIncreaseExposure(method f) returns bool = //f.selector == sig:backWithGho(uint128).selector || f.selector == sig:backWithUnderlying(uint256).selector || f.selector == sig:sellAsset(uint256,address).selector || f.selector == sig:sellAssetWithSig(address,uint256,address,uint256,bytes).selector; definition canDecreaseExposure(method f) returns bool = f.selector == sig:buyAsset(uint256, address).selector || f.selector == sig:seize().selector || f.selector == sig:buyAssetWithSig(address,uint256,address,uint256,bytes).selector; // @title Only specific methods can change exposure. // STATUS: PASS rule whoCanChangeExposure(method f) { env e; feeLimits(e); priceLimits(e); uint256 exposureBefore = getCurrentExposure(e); calldataarg args; f(e, args); uint256 exposureAfter = getCurrentExposure(e); assert exposureAfter > exposureBefore => canIncreaseExposure(f), "should not increase exposure"; assert exposureAfter < exposureBefore => canDecreaseExposure(f), "should not decrease exposure"; } definition canIncreaseAccruedFees(method f) returns bool = f.selector == sig:sellAsset(uint256,address).selector || f.selector == sig:sellAssetWithSig(address,uint256,address,uint256,bytes).selector || f.selector == sig:buyAsset(uint256, address).selector || f.selector == sig:buyAssetWithSig(address,uint256,address,uint256,bytes).selector || f.selector == sig:cumulateYieldInGho().selector ; definition canDecreaseAccruedFees(method f) returns bool = f.selector == sig:distributeFeesToTreasury().selector; // @title Only specific methods can increase / decrease acrued fees // STATUS: VIOLATED // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule whoCanChangeAccruedFees(method f) { env e; feeLimits(e); priceLimits(e); uint256 accruedFeesBefore = getAccruedFee(e); calldataarg args; f(e, args); uint256 accruedFeesAfter = getAccruedFee(e); assert accruedFeesAfter > accruedFeesBefore => canIncreaseAccruedFees(f), "should not increase accrued fees"; assert accruedFeesAfter < accruedFeesBefore => canDecreaseAccruedFees(f), "should not decrease accrued fees"; } // @title It's not possible for _currentExposure to exceed _exposureCap as a result of a call to sellAsset. // STATUS: PASS // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule sellingDoesntExceedExposureCap() { env e; feeLimits(e); priceLimits(e); uint128 amount; address receiver; require getCurrentExposure(e) <= getExposureCap(e); sellAsset(e, amount, receiver); assert getCurrentExposure(e) <= getExposureCap(e); } // @title The buy fee actually collected (after rounding) is at least the required percentage. // STATUS: PASS // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule collectedBuyFeeIsAtLeastAsRequired() { env e; feeLimits(e); priceLimits(e); uint256 assetAmount; uint256 ghoTotal; uint256 ghoGross; uint256 ghoFee; _, ghoTotal, ghoGross, ghoFee = getGhoAmountForBuyAsset(e, assetAmount); assert getPercMathPercentageFactor(e) * ghoFee >= getBuyFeeBP(e) * ghoGross; } // @title The buy fee actually collected (after rounding) is at least the required percentage. // STATUS: PASS // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule collectedBuyFeePlus1IsAtLeastAsRequired() { env e; feeLimits(e); priceLimits(e); uint256 assetAmount; uint256 ghoTotal; uint256 ghoGross; uint256 ghoFee; _, ghoTotal, ghoGross, ghoFee = getGhoAmountForBuyAsset(e, assetAmount); assert getPercMathPercentageFactor(e) * require_uint256(ghoFee + 1) >= getBuyFeeBP(e) * ghoGross; } // @title The buy fee actually collected (after rounding) is at least the required percentage. // STATUS: PASS // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule collectedBuyFeePlus2IsAtLeastAsRequired() { env e; feeLimits(e); priceLimits(e); uint256 assetAmount; uint256 ghoTotal; uint256 ghoGross; uint256 ghoFee; _, ghoTotal, ghoGross, ghoFee = getGhoAmountForBuyAsset(e, assetAmount); assert getPercMathPercentageFactor(e) * require_uint256(ghoFee + 2) >= getBuyFeeBP(e) * ghoGross; } // @title The sell fee actually collected (after rounding) is at least the required percentage. // STATUS: PASS // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule collectedSellFeeIsAtLeastAsRequired() { env e; feeLimits(e); priceLimits(e); uint256 ghoAmount; uint256 ghoTotal; uint256 ghoGross; uint256 ghoFee; _, ghoTotal, ghoGross, ghoFee = getGhoAmountForSellAsset(e, ghoAmount); assert getPercMathPercentageFactor(e) * ghoFee >= getSellFeeBP(e) * ghoGross; } // @title getAssetAmountForSellAsset never exceeds the given bound // STATUS: PASS // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule getAssetAmountForSellAsset_correctness() { env e; feeLimits(e); priceLimits(e); uint minimumToReceive; uint suggestedAssetToSell; suggestedAssetToSell, _, _, _ = getAssetAmountForSellAsset(e, minimumToReceive); uint reallyReceived; _, reallyReceived, _, _ = getGhoAmountForSellAsset(e, suggestedAssetToSell); assert reallyReceived >= minimumToReceive; } // @title backWithGho doesn't create excess // STATUS: PASS // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule backWithGhoDoesntCreateExcess() { env e; feeLimits(e); priceLimits(e); uint128 amount; uint256 excess; uint256 dearth; require getCurrentExposure(e) + amount < max_uint256; excess, dearth = getCurrentBacking(e); backWithGho(e, amount); assert dearth > 0; //if not reverted, dearth must be > 0 uint256 excessAfter; uint256 dearthAfter; excessAfter, dearthAfter = getCurrentBacking(e); assert excessAfter == 0; } // @title gifting Gho doesn't create excess or dearth // STATUS: PASS // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule giftingGhoDoesntCreateExcessOrDearth() { env e; feeLimits(e); priceLimits(e); address sender; uint128 amount; uint256 excess; uint256 dearth; excess, dearth = getCurrentBacking(e); giftGho(e, sender, amount); uint256 excessAfter; uint256 dearthAfter; excessAfter, dearthAfter = getCurrentBacking(e); assert excessAfter == excess && dearthAfter == dearth; } // @title gifting Underlying doesn't create excess or dearth // STATUS: PASS // https://prover.certora.com/output/6893/1a85cb3aac6942abad66e5508f7d37f7/?anonymousKey=4cff2c39342d22aac51f08bb6fdbb375c0f025c6 rule giftingUnderlyingDoesntCreateExcessOrDearth() { env e; feeLimits(e); priceLimits(e); address sender; uint128 amount; uint256 excess; uint256 dearth; excess, dearth = getCurrentBacking(e); giftUnderlyingAsset(e, sender, amount); uint256 excessAfter; uint256 dearthAfter; excessAfter, dearthAfter = getCurrentBacking(e); assert excessAfter == excess && dearthAfter == dearth; } // @title exposure bellow cap is preserved by all methods except updateExposureCap and initialize // STATUS: PASS // https://prover.certora.com/output/6893/ada8f51ae4f7440b86c51e44b0848c45/?anonymousKey=6d86bdd46fd01d54e4d129bc12358b790450b57c rule exposureBelowCap(method f) filtered { f -> f.selector != sig:initialize(address,address,uint128).selector && f.selector != sig:updateExposureCap(uint128).selector && f.selector != sig:backWithUnderlying(uint256).selector } { env e; calldataarg args; feeLimits(e); priceLimits(e); require getCurrentExposure(e) <= getExposureCap(e); f(e, args); assert getCurrentExposure(e) <= getExposureCap(e); } // @title backWithUnderlying doesn't create excess // STATUS: TIMEOUT // https://prover.certora.com/output/11775/41f89457d28046fd8337b785be0f7083?anonymousKey=11cb2f7a6900010275a89d0be8f6af8245bfce3d // rule backWithUnderlyingDoesntCreateExcess() // { // env e; // feeLimits(e); // priceLimits(e); // uint128 amount; // uint256 excess; uint256 dearth; // require getCurrentExposure(e) + amount < max_uint256; // backWithUnderlying(e, amount); // Reverts if there is no deficit // uint256 excessAfter; uint256 dearthAfter; // excessAfter, dearthAfter = getCurrentBacking(e); // assert excessAfter <= 1; // } // @title gifting underlying doesn't change storage // STATUS: PASS // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule giftingUnderlyingDoesntAffectStorageSIMPLE() { env e; feeLimits(e); priceLimits(e); address sender; uint128 amount; calldataarg args; storage initialStorage = lastStorage; giftUnderlyingAsset(e, sender, amount); storage storageAfter = lastStorage; assert storageAfter[currentContract] == initialStorage[currentContract]; } // @title gifting underlying doesn't change storage // STATUS: PASS // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule giftingGhoDoesntAffectStorageSIMPLE() { env e; feeLimits(e); priceLimits(e); address sender; uint128 amount; storage initialStorage = lastStorage; giftGho(e, sender, amount) at initialStorage; storage storageAfter = lastStorage; assert storageAfter[currentContract] == initialStorage[currentContract]; } // @title Return values of sellAsset are monotonically inreasing // STATUS: TIMEOUT // https://prover.certora.com/output/11775/a6b2635ff0d7405daa361c732e2a519e?anonymousKey=506bbd9b65e9cf3e32f27606e38fd713cedfe2df // rule monotonicityOfSellAsset() { // env e; // feeLimits(e); // priceLimits(e); // address recipient; // uint amount1; // uint a1; // uint g1; // //a1, g1 = sellAsset(e, amount1, recipient); // a1, g1, _, _ = getGhoAmountForSellAsset(e, amount1); // uint amount2; // uint a2; // uint g2; // //a2, g2 = sellAsset(e, amount2, recipient); // a2, g2, _, _ = getGhoAmountForSellAsset(e, amount2); // assert a1 <= a2 <=> g1 <= g2; // } // @title Return values of buyAsset are monotonically inreasing // STATUS: TIMEOUT // https://prover.certora.com/output/11775/614332d4a677432d988bfd371653a23b?anonymousKey=cd30246db79a8237b02d83f1d390c4832cd1f970 // rule monotonicityOfBuyAsset() { // env e; // feeLimits(e); // priceLimits(e); // address recipient; // uint amount1; // uint a1; // uint g1; // a1, g1 = buyAsset(e, amount1, recipient); // uint amount2; // uint a2; // uint g2; // a2, g2 = buyAsset(e, amount2, recipient); // assert a1 <= a2 <=> g1 <= g2; // } // @title Return values of sellAsset are the same as of getGhoAmountForSellAsset // STATUS: PASS // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule sellAssetSameAsGetGhoAmountForSellAsset() { env e; feeLimits(e); priceLimits(e); address recipient; uint amount; uint a1; uint g1; uint a2; uint g2; a1, g1, _, _ = getGhoAmountForSellAsset(e, amount); a2, g2 = sellAsset(e, amount, recipient); assert a1 == a2 && g1 == g2; } // @title buyAsset never returns value lower than the argument // STATUS: PASS // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule correctnessOfBuyAsset() { env e; feeLimits(e); priceLimits(e); address recipient; uint amount; uint a; uint g; a, g = buyAsset(e, amount, recipient); assert a >= amount; } // @title sellAsset never returns value greater than the argument // STATUS: PASS // https://prover.certora.com/output/11775/d325dd52f7a4416984e3b9b3188d81c4?anonymousKey=2db655a9466ae77e610d3b8f6229bd4752643f1e rule correctnessOfSellAsset() { env e; feeLimits(e); priceLimits(e); address recipient; uint amount; uint a; uint g; a, g = sellAsset(e, amount, recipient); assert a <= amount; } ================================================ FILE: certora/gsm/specs/gsm4626/gho-gsm4626-2.spec ================================================ import "../GsmMethods/shared.spec"; import "../GsmMethods/erc4626.spec"; using GhoToken as _ghoTokenHook; using DummyERC20B as UNDERLYING_ASSET; using FixedPriceStrategy4626Harness as _priceStrategy; using FixedFeeStrategyHarness as _FixedFeeStrategy; methods { // priceStrategy function _priceStrategy.getAssetPriceInGho(uint256, bool) external returns(uint256) envfree; function _priceStrategy.getUnderlyingAssetUnits() external returns(uint256) envfree; // feeStrategy function _FixedFeeStrategy.getBuyFeeBP() external returns(uint256) envfree; function _FixedFeeStrategy.getSellFeeBP() external returns(uint256) envfree; } // @title Rule checks that In the event the underlying asset increases in value relative // to the amount of GHO minted, excess yield harvesting should never result // in previously-minted GHO having less backing (i.e., as new GHO is minted backed // by the excess, it should not result in the GSM becoming under-backed in the same block). // STATUS: VIOLATED // Run: https://prover.certora.com/output/11775/de602da1d4cc426bb067f9a0aa4a9a05?anonymousKey=a6365b8a651e118c4ccdfb59df46c26a4d3d32b4 rule yieldNeverDecreasesBacking() { env e; require(getExceed(e) > 0); cumulateYieldInGho(e); assert getDearth(e) == 0; } // @title Rule checks that _accruedFees should be <= ghotoken.balanceof(this) with an exception of the function distributeFeesToTreasury(). // STATUS: PASS // Run: https://prover.certora.com/output/11775/d3603bd8c03942df80d02a2043b171ca?anonymousKey=0d708c3d21d302cfad1eba8deac83f6eb919cbe2 rule accruedFeesLEGhoBalanceOfThis(method f) { env e; calldataarg args; require(getAccruedFee(e) <= getGhoBalanceOfThis(e)); require(e.msg.sender != currentContract); require(UNDERLYING_ASSET(e) != GHO_TOKEN(e)); if (f.selector == sig:buyAssetWithSig(address,uint256,address,uint256,bytes).selector) { address originator; uint256 amount; address receiver; uint256 deadline; bytes signature; require(originator != currentContract); buyAssetWithSig(e, originator, amount, receiver, deadline, signature); } else { f(e,args); } assert getAccruedFee(e) <= getGhoBalanceOfThis(e); } // @title _accruedFees should never decrease, unless fees are being harvested by Treasury // STATUS: PASS // Run: https://prover.certora.com/output/31688/1c8ec1e853e849c5aa4fd26914d0acf3?anonymousKey=30813ba939a055af5f0a09f097782c9805b980a8 rule accruedFeesNeverDecrease(method f) filtered {f -> f.selector != sig:distributeFeesToTreasury().selector} { env e; calldataarg args; uint256 feesBefore = getAccruedFee(e); f(e,args); assert feesBefore <= getAccruedFee(e); } // @title For price ratio == 1, the total assets of a user should not increase. // STATUS: VIOLATED // https://prover.certora.com/output/11775/8448c89e18e94cb9a9ba21eb95b2efb0?anonymousKey=6f9f80c71040f75b35dece32a73442f84140e6ce // https://prover.certora.com/output/31688/4f70640081d6419fa999271d91a4ba89?anonymousKey=877a8c262875da9a8c04bda11d0c36facf5aa390 // Passing with Antti's model of 4626 (with some timeouts) https://prover.certora.com/output/31688/7c83d14232934b349d17569688a741fe?anonymousKey=0b7f3177ea39762c6d9fa1be1f7b969bda29f233 // // For price ratio == 1, the total assets of a user should not increase rule totalAssetsNotIncrease(method f) filtered {f -> f.selector != sig:seize().selector && f.selector != sig:rescueTokens(address, address, uint256).selector && f.selector != sig:distributeFeesToTreasury().selector && f.selector != sig:giftGho(address, uint256).selector && f.selector != sig:giftUnderlyingAsset(address, uint256).selector && f.selector != sig:buyAssetWithSig(address, uint256, address, uint256, bytes).selector && f.selector != sig:sellAssetWithSig(address, uint256, address, uint256, bytes).selector} { env e; // we focuse on a user so remove address of contracts require e.msg.sender != currentContract; require(getPriceRatio() == 10^18); // uint8 underlyingAssetDecimals; // require underlyingAssetDecimals <= 36; // require to_mathint(_priceStrategy.getUnderlyingAssetUnits()) == 10^underlyingAssetDecimals; feeLimits(e); priceLimits(e); mathint underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); address other; address receiver; uint256 amount; address originator; // This is here due to FixedPriceStrategy4626 since we need // to say that previewRedeem respects price ratio == 1, i.e., // you still buy same amount of shares for the given gho. require(getAssetPriceInGho(e, amount, false) * underlyingAssetUnits/getPriceRatio() == to_mathint(amount)); require receiver != currentContract; // && receiver != originator && receiver != e.msg.sender; require originator != currentContract; // && originator != e.msg.sender; require other != e.msg.sender && other != receiver && other != originator && other != currentContract; mathint totalAssetOtherBefore = getTotalAsset(e, other, getPriceRatio(), underlyingAssetUnits); mathint totalAssetBefore = assetOfUsers(e, e.msg.sender, receiver, originator, getPriceRatio(), underlyingAssetUnits); functionDispatcher(f, e, receiver, originator, amount); mathint totalAssetAfter = assetOfUsers(e, e.msg.sender, receiver, originator, getPriceRatio(), underlyingAssetUnits); assert totalAssetBefore >= totalAssetAfter; assert totalAssetOtherBefore == getTotalAsset(e, other, getPriceRatio(), underlyingAssetUnits); } // @title Rule checks that an overall asset of the system (UA - minted gho) stays same. // STATUS: VIOLATED // https://prover.certora.com/output/11775/de602da1d4cc426bb067f9a0aa4a9a05?anonymousKey=a6365b8a651e118c4ccdfb59df46c26a4d3d32b4 // The attempts to solve the timeout: // For the general condition: // - general limits + standard timeout - https://prover.certora.com/output/31688/a49f76f4578b4b4ab70b72576bbb0189?anonymousKey=bc3a2e3aae14596c9ba1adc5c566b718c4d02e96 // - 1000 fees && fixed price ratio + standard timeout - https://prover.certora.com/output/31688/08d21e1c60a546cda151d762d3e6acf2?anonymousKey=50f50e1fc767bae84a3b44c9d4a92aad03cdcc4e // - 1000 fees && fixed price ratio + 10000 smt solving timeout - https://prover.certora.com/output/31688/0f520b4cf02e4770a804a94bc49120ec?anonymousKey=5581daad6a74234f25bc80a170fd92ace68f4f4c // - Rule is split to individual ones with fixed UA decimal units https://prover.certora.com/output/31688/5b6cd5108e544841bb30c48852827007?anonymousKey=0a0aa495023d36ecceeb386fe5b170392da2627b // Provd that no underbacking happes, i.e. diff >= 0 // - general limits + standard timeout https://prover.certora.com/output/31688/caa6714046234cd18e4f09c397dfeec4?anonymousKey=00dc26cf5a0b355c09092650aae7e1f1adf48136 rule systemBalanceStabilitySell() { uint256 amount; address receiver; env e; require currentContract != e.msg.sender; require currentContract != receiver; feeLimits(e); priceLimits(e); require(getAssetPriceInGho(e, amount, false) * _priceStrategy.getUnderlyingAssetUnits()/getPriceRatio() == to_mathint(amount)); mathint ghoMintedBefore = getGhoMinted(e); mathint balanceBefore = balanceOfUnderlyingDirect(e, currentContract); sellAsset(e, amount, receiver); mathint ghoMintedAfter = getGhoMinted(e); mathint balanceAfter = balanceOfUnderlyingDirect(e, currentContract); mathint diff = getAssetPriceInGho(e, assert_uint256(balanceAfter - balanceBefore), false) - ghoMintedAfter + ghoMintedBefore; //assert diff >= 0; // no underbacking assert diff >= 0 && diff <= 1; } // @title Rule checks that an overall asset of the system (UA - minted gho) stays same. // STATUS: TIMEOUT // https://prover.certora.com/output/31688/905f225066a04f9394d8ea5adee5274d?anonymousKey=5c95ad70db18bf9b3dcdc74f7f781e01e50d0550 // No underbacking happens, i.e. diff <= 1 - proved https://prover.certora.com/output/31688/16161fec79664619a9a72c52a58cb36a/?anonymousKey=80739ecd169b7e28964092556cb66c0e9aa42ebc rule systemBalanceStabilityBuy() { uint256 amount; address receiver; env e; require currentContract != e.msg.sender; require currentContract != receiver; feeLimits(e); priceLimits(e); require(getAssetPriceInGho(e, amount, false) * _priceStrategy.getUnderlyingAssetUnits()/getPriceRatio() == to_mathint(amount)); uint256 ghoBucketCapacity; uint256 ghoMintedBefore; ghoBucketCapacity, ghoMintedBefore = getFacilitatorBucket(e); mathint balanceBefore = balanceOfUnderlyingDirect(e, currentContract); mathint ghoExceedBefore = getExceed(e); require ghoBucketCapacity - ghoMintedBefore > ghoExceedBefore; buyAsset(e, amount, receiver); mathint ghoMintedAfter = getGhoMinted(e); mathint balanceAfter = balanceOfUnderlyingDirect(e, currentContract); mathint diff = getAssetPriceInGho(e, assert_uint256(balanceBefore - balanceAfter), true) - ghoMintedBefore + ghoMintedAfter - ghoExceedBefore; // assert diff <= 1; // No underbacking happens. assert -1 <= diff && diff <= 1; } ================================================ FILE: certora/gsm/specs/gsm4626/gho-gsm4626.spec ================================================ import "../GsmMethods/methods4626_base.spec"; import "../GsmMethods/methods_divint_summary.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; import "../GsmMethods/erc4626.spec"; // @title solvency rule - buyAsset Function // STATUS: VIOLATED // https://prover.certora.com/output/11775/0b04906c237b4a1e8ac5b7ffc1e9f449?anonymousKey=cf620b132aaadb33116c93025269fbbe5258070c // rule enoughULtoBackGhoBuyAsset() // { // uint256 _currentExposure = getAvailableLiquidity(); // uint256 _ghoMinted = getGhoMinted(); // uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); // uint8 underlyingAssetDecimals; // // require underlyingAssetDecimals == 18; // require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; // // uint256 priceRatio = _priceStrategy.PRICE_RATIO(); // // require priceRatio >= 10^16 && priceRatio <= 10^20; // // uint256 buyFeeBP = getBuyFeeBP(); // // require buyFeeBP == 4000; // // rounding up for over-approximation // uint256 _ghoBacked = _priceStrategy.getAssetPriceInGho(_currentExposure, true); // require _ghoBacked >= _ghoMinted; // env e; // feeLimits(e); // priceLimits(e); // uint256 amount; // address receiver; // buyAsset(e, amount, receiver); // uint256 ghoMinted_ = getGhoMinted(); // uint256 currentExposure_ = getAvailableLiquidity(); // // rounding down for over-approximation // uint256 ghoBacked_ = _priceStrategy.getAssetPriceInGho(currentExposure_, false); // assert to_mathint(ghoBacked_+1)>= to_mathint(ghoMinted_) // ,"not enough currentExposure to back the ghoMinted"; // } // @title solvency rule - sellAsset function // STATUS: TIMEOUT // https://prover.certora.com/output/11775/bfafe4ddbb6947a8ae86635dd14a6eb8?anonymousKey=4e0a75d10aaadeba18ea4d3a9ecfcfdb0c1f2188 // rule enoughUnderlyingToBackGhoRuleSellAsset() // { // uint256 _currentExposure = getAvailableLiquidity(); // uint256 _ghoMinted = getGhoMinted(); // // uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); // // uint8 underlyingAssetDecimals; // // require underlyingAssetDecimals == 18; // // require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; // // uint256 priceRatio = _priceStrategy.PRICE_RATIO(); // // require priceRatio >= 10^16 && priceRatio <= 10^20; // // uint256 sellFeeBP = getSellFeeBP(); // // require sellFeeBP == 5000; // uint256 _ghoBacked = _priceStrategy.getAssetPriceInGho(_currentExposure,false); // require _ghoBacked >= _ghoMinted; // uint128 amount; // address receiver; // env e; // feeLimits(e); // priceLimits(e); // sellAsset(e, amount, receiver); // uint256 ghoMinted_ = getGhoMinted(); // uint256 currentExposure_ = getAvailableLiquidity(); // uint256 ghoBacked_ = _priceStrategy.getAssetPriceInGho(currentExposure_, false); // assert to_mathint(ghoBacked_+1)>= to_mathint(ghoMinted_) ,"not enough currentExposure to back the ghoMinted"; // } // @title solvency rule for non buy sell functions // STATUS: PASSED // https://prover.certora.com/output/11775/434fcceaf67349e19568b66d7457a35f?anonymousKey=6570aa08aa061ffe7bcf4328ff64714d08764215 rule enoughULtoBackGhoNonBuySell(method f) filtered { f -> !f.isView && !harnessOnlyMethods(f) && !buySellAssetsFunctions(f) }{ uint256 _currentExposure = getAvailableLiquidity(); uint256 _ghoMinted = getGhoMinted(); uint256 _ghoBacked = _priceStrategy.getAssetPriceInGho(_currentExposure,true); require _ghoBacked >= _ghoMinted; env e; calldataarg args; f(e, args); uint256 ghoMinted_ = getGhoMinted(); uint256 currentExposure_ = getAvailableLiquidity(); uint256 ghoBacked_ = _priceStrategy.getAssetPriceInGho(_currentExposure,true); assert ghoBacked_ >= ghoMinted_,"not enough currentExposure to back the ghoMinted"; } // @title if fee > 0: // 1. gho received by user is less than assetPriceInGho(underlying amount) in sell asset function // 2. gho paid by user is more than assetPriceInGho(underlying amount received) // 3. gho balance of contract goes up // STATUS: PASSED // https://prover.certora.com/output/11775/434fcceaf67349e19568b66d7457a35f?anonymousKey=6570aa08aa061ffe7bcf4328ff64714d08764215 rule NonZeroFeeCheckSellAsset(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals; require underlyingAssetDecimals <78; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; address receiver; uint256 _receiverGhoBalance = _ghoToken.balanceOf(receiver); uint256 _GSMGhoBalance = _ghoToken.balanceOf(currentContract); uint256 _accruedFee = getAccruedFees(); uint256 amount; uint256 amountInGho = _priceStrategy.getAssetPriceInGho(amount, false); require _FixedFeeStrategy.getSellFee(amountInGho) > 0; env e; basicBuySellSetup(e, receiver); sellAsset(e, amount, receiver); uint256 receiverGhoBalance_ = _ghoToken.balanceOf(receiver); uint256 GSMGhoBalance_ = _ghoToken.balanceOf(currentContract); mathint GSMGhoBalanceIncrease = GSMGhoBalance_ - _GSMGhoBalance; uint256 accruedFee_ = getAccruedFees(); mathint accruedFeeIncrease = accruedFee_ - _accruedFee; mathint ghoReceived = receiverGhoBalance_ - _receiverGhoBalance; assert ghoReceived < to_mathint(amountInGho),"fee not deducted from gho minted for the given UL amount"; assert GSMGhoBalance_ > _GSMGhoBalance ,"GMS gho balance should increase on account of fee collected"; assert accruedFee_ > _accruedFee,"accruedFee should increase in a sell asset transaction"; assert accruedFeeIncrease == GSMGhoBalanceIncrease,"accrued fee should increase by the same amount as the GSM gho balance"; } // STATUS: PASSED // https://prover.certora.com/output/11775/434fcceaf67349e19568b66d7457a35f?anonymousKey=6570aa08aa061ffe7bcf4328ff64714d08764215 rule NonZeroFeeCheckBuyAsset(){ uint256 _underlyingAssetUnits = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals; require underlyingAssetDecimals <78; require to_mathint(_underlyingAssetUnits) == 10^underlyingAssetDecimals; address receiver; uint256 _receiverGhoBalance = _ghoToken.balanceOf(receiver); uint256 _GSMGhoBalance = _ghoToken.balanceOf(currentContract); uint256 _accruedFee = getAccruedFees(); uint256 amount; uint256 amountInGho = _priceStrategy.getAssetPriceInGho(amount, true); uint256 fee = _FixedFeeStrategy.getBuyFee(amountInGho); require fee > 0; env e; basicBuySellSetup(e, receiver); buyAsset(e, amount, receiver); uint256 receiverGhoBalance_ = _ghoToken.balanceOf(receiver); uint256 GSMGhoBalance_ = _ghoToken.balanceOf(currentContract); mathint GSMGhoBalanceIncrease = GSMGhoBalance_ - _GSMGhoBalance; uint256 accruedFee_ = getAccruedFees(); mathint accruedFeeIncrease = accruedFee_ - _accruedFee; mathint ghoReceived = receiverGhoBalance_ - _receiverGhoBalance; assert ghoReceived < to_mathint(amountInGho),"fee not deducted from gho minted for the given UL amount"; assert GSMGhoBalance_ > _GSMGhoBalance ,"GMS gho balance should increase on account of fee collected"; assert accruedFee_ > _accruedFee,"accruedFee should increase in a sell asset transaction"; assert accruedFeeIncrease == GSMGhoBalanceIncrease,"accrued fee should increase by the same amount as the GSM gho balance"; } ================================================ FILE: certora/gsm/specs/gsm4626/gho-gsm_4626_inverse.spec ================================================ import "../GsmMethods/methods4626_base.spec"; import "../GsmMethods/methods_divint_summary.spec"; import "../GsmMethods/erc4626.spec"; // // @title Buy/sell invariants property #6: In case of using a 1:1 ratio and 0 fees, the inverse action of buyAsset must be sellAsset. (e.g. if buyAsset(x assets) needs y GHO, sellAsset(x assets) gives y GHO). // // STATUS: TIMEOUT // // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c // rule buySellInverse5(){ // uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); // uint8 underlyingAssetDecimals = 5; // require to_mathint(UAU) == 10^underlyingAssetDecimals; // uint256 priceRatio = _priceStrategy.PRICE_RATIO(); // require priceRatio == 10^18; // uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); // uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); // require buyFee == 0 && sellFee == 0; // uint256 assetsBuy; // address receiver1; // uint256 assetsBought; // uint256 ghoSold; // env e1; // assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); // uint256 assetsSell; // address receiver2; // uint256 assetsSold; // uint256 ghoBought; // env e2; // assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); // assert assetsBought == assetsSold => to_mathint(ghoBought + 1) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; // } // // STATUS: TIMEOUT // // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c // rule buySellInverse6(){ // uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); // uint8 underlyingAssetDecimals = 6; // require to_mathint(UAU) == 10^underlyingAssetDecimals; // uint256 priceRatio = _priceStrategy.PRICE_RATIO(); // require priceRatio == 10^18; // uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); // uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); // require buyFee == 0 && sellFee == 0; // uint256 assetsBuy; // address receiver1; // uint256 assetsBought; // uint256 ghoSold; // env e1; // assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); // uint256 assetsSell; // address receiver2; // uint256 assetsSold; // uint256 ghoBought; // env e2; // assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); // assert assetsBought == assetsSold => to_mathint(ghoBought +priceRatio/UAU) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; // } // // STATUS: TIMEOUT // // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c // rule buySellInverse7(){ // uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); // uint8 underlyingAssetDecimals = 7; // require to_mathint(UAU) == 10^underlyingAssetDecimals; // uint256 priceRatio = _priceStrategy.PRICE_RATIO(); // require priceRatio == 10^18; // uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); // uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); // require buyFee == 0 && sellFee == 0; // uint256 assetsBuy; // address receiver1; // uint256 assetsBought; // uint256 ghoSold; // env e1; // assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); // uint256 assetsSell; // address receiver2; // uint256 assetsSold; // uint256 ghoBought; // env e2; // assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); // assert assetsBought == assetsSold => to_mathint(ghoBought +priceRatio/UAU) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; // } // // STATUS: TIMEOUT // // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c // rule buySellInverse8(){ // uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); // uint8 underlyingAssetDecimals = 8; // require to_mathint(UAU) == 10^underlyingAssetDecimals; // uint256 priceRatio = _priceStrategy.PRICE_RATIO(); // require priceRatio == 10^18; // uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); // uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); // require buyFee == 0 && sellFee == 0; // uint256 assetsBuy; // address receiver1; // uint256 assetsBought; // uint256 ghoSold; // env e1; // assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); // uint256 assetsSell; // address receiver2; // uint256 assetsSold; // uint256 ghoBought; // env e2; // assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); // assert assetsBought == assetsSold => to_mathint(ghoBought +priceRatio/UAU) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; // } // // STATUS: TIMEOUT // // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c // rule buySellInverse9(){ // uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); // uint8 underlyingAssetDecimals = 9; // require to_mathint(UAU) == 10^underlyingAssetDecimals; // uint256 priceRatio = _priceStrategy.PRICE_RATIO(); // require priceRatio == 10^18; // uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); // uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); // require buyFee == 0 && sellFee == 0; // uint256 assetsBuy; // address receiver1; // uint256 assetsBought; // uint256 ghoSold; // env e1; // assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); // uint256 assetsSell; // address receiver2; // uint256 assetsSold; // uint256 ghoBought; // env e2; // assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); // assert assetsBought == assetsSold => to_mathint(ghoBought +priceRatio/UAU) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; // } // // STATUS: TIMEOUT // // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c // rule buySellInverse10(){ // uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); // uint8 underlyingAssetDecimals = 10; // require to_mathint(UAU) == 10^underlyingAssetDecimals; // uint256 priceRatio = _priceStrategy.PRICE_RATIO(); // require priceRatio == 10^18; // uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); // uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); // require buyFee == 0 && sellFee == 0; // uint256 assetsBuy; // address receiver1; // uint256 assetsBought; // uint256 ghoSold; // env e1; // assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); // uint256 assetsSell; // address receiver2; // uint256 assetsSold; // uint256 ghoBought; // env e2; // assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); // assert assetsBought == assetsSold => to_mathint(ghoBought +priceRatio/UAU) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; // } // // STATUS: TIMEOUT // // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c // rule buySellInverse11(){ // uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); // uint8 underlyingAssetDecimals = 11; // require to_mathint(UAU) == 10^underlyingAssetDecimals; // uint256 priceRatio = _priceStrategy.PRICE_RATIO(); // require priceRatio == 10^18; // uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); // uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); // require buyFee == 0 && sellFee == 0; // uint256 assetsBuy; // address receiver1; // uint256 assetsBought; // uint256 ghoSold; // env e1; // assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); // uint256 assetsSell; // address receiver2; // uint256 assetsSold; // uint256 ghoBought; // env e2; // assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); // assert assetsBought == assetsSold => to_mathint(ghoBought +priceRatio/UAU) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; // } // // STATUS: TIMEOUT // // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c // rule buySellInverse12(){ // uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); // uint8 underlyingAssetDecimals = 12; // require to_mathint(UAU) == 10^underlyingAssetDecimals; // uint256 priceRatio = _priceStrategy.PRICE_RATIO(); // require priceRatio == 10^18; // uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); // uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); // require buyFee == 0 && sellFee == 0; // uint256 assetsBuy; // address receiver1; // uint256 assetsBought; // uint256 ghoSold; // env e1; // assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); // uint256 assetsSell; // address receiver2; // uint256 assetsSold; // uint256 ghoBought; // env e2; // assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); // assert assetsBought == assetsSold => to_mathint(ghoBought +priceRatio/UAU) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; // } // // STATUS: TIMEOUT // // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c // rule buySellInverse13(){ // uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); // uint8 underlyingAssetDecimals = 13; // require to_mathint(UAU) == 10^underlyingAssetDecimals; // uint256 priceRatio = _priceStrategy.PRICE_RATIO(); // require priceRatio == 10^18; // uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); // uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); // require buyFee == 0 && sellFee == 0; // uint256 assetsBuy; // address receiver1; // uint256 assetsBought; // uint256 ghoSold; // env e1; // assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); // uint256 assetsSell; // address receiver2; // uint256 assetsSold; // uint256 ghoBought; // env e2; // assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); // assert assetsBought == assetsSold => to_mathint(ghoBought +priceRatio/UAU) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; // } // // STATUS: TIMEOUT // // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c // rule buySellInverse14(){ // uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); // uint8 underlyingAssetDecimals = 14; // require to_mathint(UAU) == 10^underlyingAssetDecimals; // uint256 priceRatio = _priceStrategy.PRICE_RATIO(); // require priceRatio == 10^18; // uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); // uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); // require buyFee == 0 && sellFee == 0; // uint256 assetsBuy; // address receiver1; // uint256 assetsBought; // uint256 ghoSold; // env e1; // assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); // uint256 assetsSell; // address receiver2; // uint256 assetsSold; // uint256 ghoBought; // env e2; // assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); // assert assetsBought == assetsSold => to_mathint(ghoBought +priceRatio/UAU) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; // } // // STATUS: TIMEOUT // // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c // rule buySellInverse15(){ // uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); // uint8 underlyingAssetDecimals = 15; // require to_mathint(UAU) == 10^underlyingAssetDecimals; // uint256 priceRatio = _priceStrategy.PRICE_RATIO(); // require priceRatio == 10^18; // uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); // uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); // require buyFee == 0 && sellFee == 0; // uint256 assetsBuy; // address receiver1; // uint256 assetsBought; // uint256 ghoSold; // env e1; // assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); // uint256 assetsSell; // address receiver2; // uint256 assetsSold; // uint256 ghoBought; // env e2; // assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); // assert assetsBought == assetsSold => to_mathint(ghoBought +priceRatio/UAU) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; // } // // STATUS: TIMEOUT // // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c // rule buySellInverse16(){ // uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); // uint8 underlyingAssetDecimals = 16; // require to_mathint(UAU) == 10^underlyingAssetDecimals; // uint256 priceRatio = _priceStrategy.PRICE_RATIO(); // require priceRatio == 10^18; // uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); // uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); // require buyFee == 0 && sellFee == 0; // uint256 assetsBuy; // address receiver1; // uint256 assetsBought; // uint256 ghoSold; // env e1; // assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); // uint256 assetsSell; // address receiver2; // uint256 assetsSold; // uint256 ghoBought; // env e2; // assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); // assert assetsBought == assetsSold => to_mathint(ghoBought +priceRatio/UAU) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; // } // // STATUS: TIMEOUT // // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c // rule buySellInverse17(){ // uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); // uint8 underlyingAssetDecimals = 17; // require to_mathint(UAU) == 10^underlyingAssetDecimals; // uint256 priceRatio = _priceStrategy.PRICE_RATIO(); // require priceRatio == 10^18; // uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); // uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); // require buyFee == 0 && sellFee == 0; // uint256 assetsBuy; // address receiver1; // uint256 assetsBought; // uint256 ghoSold; // env e1; // assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); // uint256 assetsSell; // address receiver2; // uint256 assetsSold; // uint256 ghoBought; // env e2; // assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); // assert assetsBought == assetsSold => to_mathint(ghoBought +priceRatio/UAU) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; // } // // STATUS: TIMEOUT // // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c // rule buySellInverse18(){ // uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); // uint8 underlyingAssetDecimals = 18; // require to_mathint(UAU) == 10^underlyingAssetDecimals; // uint256 priceRatio = _priceStrategy.PRICE_RATIO(); // require priceRatio == 10^18; // uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); // uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); // require buyFee == 0 && sellFee == 0; // uint256 assetsBuy; // address receiver1; // uint256 assetsBought; // uint256 ghoSold; // env e1; // assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); // uint256 assetsSell; // address receiver2; // uint256 assetsSold; // uint256 ghoBought; // env e2; // assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); // assert assetsBought == assetsSold => to_mathint(ghoBought +1) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; // } // STATUS: PASS // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c rule buySellInverse19(){ uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 19; require to_mathint(UAU) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => to_mathint(ghoBought +1) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // STATUS: PASS // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c rule buySellInverse20(){ uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 20; require to_mathint(UAU) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => to_mathint(ghoBought +1) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // STATUS: PASS // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c rule buySellInverse21(){ uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 21; require to_mathint(UAU) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => to_mathint(ghoBought +1) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // STATUS: PASS // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c rule buySellInverse22(){ uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 22; require to_mathint(UAU) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => to_mathint(ghoBought +1) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // STATUS: PASS // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c rule buySellInverse23(){ uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 23; require to_mathint(UAU) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => to_mathint(ghoBought +1) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // STATUS: PASS // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c rule buySellInverse24(){ uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 24; require to_mathint(UAU) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => to_mathint(ghoBought +1) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // STATUS: PASS // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c rule buySellInverse25(){ uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 25; require to_mathint(UAU) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => to_mathint(ghoBought +1) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // STATUS: PASS // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c rule buySellInverse26(){ uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 26; require to_mathint(UAU) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => to_mathint(ghoBought +1) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } // STATUS: PASS // https://prover.certora.com/output/11775/8250b43937bb4c14a6468c51aa024e7a?anonymousKey=87764143874e8e012d1418e95780c6da3e7bf12c rule buySellInverse27(){ uint256 UAU = _priceStrategy.getUnderlyingAssetUnits(); uint8 underlyingAssetDecimals = 27; require to_mathint(UAU) == 10^underlyingAssetDecimals; uint256 priceRatio = _priceStrategy.PRICE_RATIO(); require priceRatio == 10^18; uint256 buyFee = _FixedFeeStrategy.getBuyFeeBP(); uint256 sellFee = _FixedFeeStrategy.getSellFeeBP(); require buyFee == 0 && sellFee == 0; uint256 assetsBuy; address receiver1; uint256 assetsBought; uint256 ghoSold; env e1; assetsBought, ghoSold = buyAsset(e1, assetsBuy, receiver1); uint256 assetsSell; address receiver2; uint256 assetsSold; uint256 ghoBought; env e2; assetsSold, ghoBought = sellAsset(e2, assetsSell, receiver2); assert assetsBought == assetsSold => to_mathint(ghoBought +1) >= to_mathint(ghoSold),"buying and selling should be inverse in case of 1:1 price ratio and 0 fees"; } ================================================ FILE: certora/gsm/specs/gsm4626/optimality4626.spec ================================================ import "../GsmMethods/methods4626_base.spec"; import "../GsmMethods/aave_price_fee_limits.spec"; import "../GsmMethods/methods_divint_summary.spec"; import "../GsmMethods/erc4626.spec"; // @Title 4626: For values given by `getAssetAmountForBuyAsset`, the user can only get more by paying more // STATUS: https://prover.certora.com/output/11775/e8e6630d5b58425d8c0b6a251ff080be?anonymousKey=900815aac4f3703ba08d4a8c64402ac6cc9979bf // This rule proves the optimality of getAssetAmountForBuyAsset with respect to // buyAsset in the following sense: // // User wants to buy as much asset as possible while paying at most maxGho. // User asks how much they should provide to buyAsset: // - a, _, _, _ = getAssetAmountForBuyAsset(maxGho) // This results in the user buying DaT assets: // - Da, Dx = buyAsset(a) // Is it possible that by not doing as `getAssetAmountForBuyAsset(maxGho)` says, the user would have // gotten a better deal, i.e., paying still less than maxGho, but getting more assets. If this is the // case, then the following holds: // There is a value `a'` such that // - Da', Dx' = buyAsset(a) // - Dx' <= Dx // - Da' > Da // Solved: https://prover.certora.com/output/40748/b6ded393db3441649a6969f207037e79?anonymousKey=840fde79dad71cfc241479f2856eb27c0aa446b9 // (1) rule R1_optimalityOfBuyAsset_v1() { env e; feeLimits(e); priceLimits(e); address recipient; uint maxGho; uint a; a, _, _, _ = getAssetAmountForBuyAsset(e, maxGho); uint Da; uint Dx; Da, Dx = buyAsset(e, a, recipient); uint ap; uint Dap; uint Dxp; Dap, Dxp = buyAsset(e, ap, recipient); require Dxp <= Dx; assert Dap <= Da; } // @Title 4626: User cannot buy more assets for same `maxGho` by providing a lower asset value than the one given by `getAssetAmountForBuyAsset(maxGho)` // STATUS: TIMEOUT // https://prover.certora.com/output/11775/2270a93b48984d0583c1334442bb11a5?anonymousKey=1655942848f2863b7612cbe27aa433868432fe8b // This rule proves the optimality of getAssetAmountForBuyAsset with respect to // buyAsset in the following sense: // // User wants to buy as much asset as possible while paying at most maxGho. // User asks how much they should provide to buyAsset: // - a, _, _, _ = getAssetAmountForBuyAsset(maxGho) // This results in the user buying Da assets: // - Da, _ = buyAsset(a) // Is it possible that by not doing as `getAssetAmountForBuyAsset(maxGho)` says, the user would have // gotten a better deal, i.e., paying still less than maxGho, but getting more assets. If this is the // case, then the following holds: // There is a value `a'` such that // - Da', Dx' = buyAsset(a) // - Dx' <= maxGho // - Da' > Da // Times out: https://prover.certora.com/output/40748/b6ded393db3441649a6969f207037e79?anonymousKey=840fde79dad71cfc241479f2856eb27c0aa446b9 // (2) // rule R2_optimalityOfBuyAsset_v2() { // env e; // feeLimits(e); // priceLimits(e); // address recipient; // uint maxGho; // uint a; // a, _, _, _ = getAssetAmountForBuyAsset(e, maxGho); // uint Da; // Da, _ = buyAsset(e, a, recipient); // uint ap; // uint Dap; // uint Dxp; // Dap, Dxp = buyAsset(e, ap, recipient); // require Dxp <= maxGho; // assert Dap <= Da; // } // @Title 4626: For values given by `getAssetAmountForSellAsset`, the user can only get more by paying more // STATUS: https://prover.certora.com/output/11775/f7389a715d5c4e8d88ad6f9666704adf?anonymousKey=cf8fa7dda6e2b9dedece7d13afae0f2ddc509258 // This rule proves the optimality of getAssetAmountForSellAsset with respect to // sellAsset in the following sense: // // User wants to sell as little assets as possible while receiving at least `minGho`. // User asks how much should they provide to sellAsset: // - a, _, _, _ = getAssetAmountForSellAsset(minGho) // This results in the user selling Da assets and receiving Dx GHO: // - Da, Dx = sellAsset(a) // Is it possible that by not doing as `getAssetAmountForSellAsset(minGho)` says, the user would have // gotten a better deal, i.e., receiving at least Dx GHO, but selling less assets. If this is the // case, then the following holds: // There is a value `a'` such that // - Da', Dx' = sellAsset(a') // - Dx' >= Dx // - Da' < Da // Solved: https://prover.certora.com/output/40748/b6ded393db3441649a6969f207037e79?anonymousKey=840fde79dad71cfc241479f2856eb27c0aa446b9 // (3) rule R3_optimalityOfSellAsset_v1 { env e; feeLimits(e); priceLimits(e); address recipient; uint minGho; uint a; a, _, _, _ = getAssetAmountForSellAsset(e, minGho); uint Da; uint Dx; Da, Dx = sellAsset(e, a, recipient); uint ap; uint Dap; uint Dxp; Dap, Dxp = sellAsset(e, ap, recipient); require Dxp >= Dx; assert Dap >= Da; } // @Title 4626: User cannot sell less assets for same `minGho` by providing a lower asset value than the one given by `getAssetAmountForSellAsset(minGho)` // STATUS: TIMEOUT // https://prover.certora.com/output/11775/f6ba80137c2e45458ec7c7f3fd54a5c3?anonymousKey=f21ea27b70d5c54e405794b70e5f6221466718f7 // This rule proves the optimality of getAssetAmountForSellAsset with respect to // sellAsset in the following sense: // // User wants to sell as little assets as possible while receiving at least `minGho`. // User asks how much should they provide to sellAsset: // - a, _, _, _ = getAssetAmountForSellAsset(minGho) // This results in the user selling DaT assets: // - Da, _ = sellAsset(a) // Is it possible that by not doing as `getAssetAmountForSellAsset(minGho)` says, the user would have // gotten a better deal, i.e., receiving still at least minGho, but selling less assets. If this is the // case, then the following holds: // There is a value `a'` such that // - Da', Dx' = sellAsset(a') // - Dx' >= minGho // - Da' < Da // Times out: https://prover.certora.com/output/40748/b6ded393db3441649a6969f207037e79?anonymousKey=840fde79dad71cfc241479f2856eb27c0aa446b9 // (4) // rule R4_optimalityOfSellAsset_v2() { // env e; // feeLimits(e); // priceLimits(e); // address recipient; // uint minGho; // uint a; // a, _, _, _ = getAssetAmountForSellAsset(e, minGho); // uint Da; // Da, _ = sellAsset(e, a, recipient); // uint ap; // uint Dap; // uint Dxp; // Dap, Dxp = sellAsset(e, ap, recipient); // require Dxp >= minGho; // assert Dap >= Da; // } // @Title 4626: The GHO received by selling asset using values from `getAssetAmountForSellAsset(minGho)` is upper bounded by `minGho` + oneAssetinGho - 1 // STATUS: TIMEOUT // https://prover.certora.com/output/11775/f4ebd94360be4faab6988ae46c11a488?anonymousKey=4a045705983f7d61295d79023c49d981793c1a36 // External optimality of sellAsset. Shows that the received amount is as close as it can be to target // Times out: https://prover.certora.com/output/40748/b6ded393db3441649a6969f207037e79?anonymousKey=840fde79dad71cfc241479f2856eb27c0aa446b9 // (5) // rule R5_externalOptimalityOfSellAsset { // env e; // feeLimits(e); // priceLimits(e); // uint256 minGhoToReceive; // uint256 ghoToReceive; // _, ghoToReceive, _, _ = getAssetAmountForSellAsset(e, minGhoToReceive); // uint256 oneAssetInGho = getAssetPriceInGho(e, 1, true); // // assert to_mathint(ghoToReceive) <= minGhoToReceive + oneAssetInGho; // assert to_mathint(ghoToReceive) < minGhoToReceive + oneAssetInGho; // // assert to_mathint(ghoToReceive) != minGhoToReceive + oneAssetInGho; // } // @Title 4626: The GHO received by selling asset using values from `getAssetAmountForSellAsset(minGho)` can be equal to `minGho` + oneAssetInGho - 1 // STATUS: PASS // https://prover.certora.com/output/11775/944a0631a18846e39fe519d7e0f631b8?anonymousKey=613fae239e703cd94f7b6c2c9081bfaca941bf0a // External optimality of sellAsset. Show the tightness of (5) // Holds: https://prover.certora.com/output/40748/b6ded393db3441649a6969f207037e79?anonymousKey=840fde79dad71cfc241479f2856eb27c0aa446b9 // (5a) // // rule R5a_externalOptimalityOfSellAsset { env e; feeLimits(e); priceLimits(e); uint256 minGhoToReceive; uint256 ghoToReceive; _, ghoToReceive, _, _ = getAssetAmountForSellAsset(e, minGhoToReceive); uint256 oneAssetInGho = getAssetPriceInGho(e, 1, true); satisfy to_mathint(ghoToReceive) == minGhoToReceive + oneAssetInGho - 1; } // @Title 4626: The GHO sold by buying asset using values from `getAssetAmountForBuyAsset(maxGho)` is at least `maxGho - 2*oneAssetInGho + 1 // STATUS: TIMEOUT // https://prover.certora.com/output/11775/d98963a792454a949ab81f99419bbb9b?anonymousKey=c9f93b1edf28e9c693c1adc0aeafef6cce912a1b // External optimality of buyAsset. Shows that the received amount is as close as it can be to target // Times out: https://prover.certora.com/output/40748/b6ded393db3441649a6969f207037e79?anonymousKey=840fde79dad71cfc241479f2856eb27c0aa446b9 // (6) // rule R6_externalOptimalityOfBuyAsset { // env e; // feeLimits(e); // priceLimits(e); // uint256 maxGhoToSpend; // uint256 ghoToSpend; // _, ghoToSpend, _, _ = getAssetAmountForBuyAsset(e, maxGhoToSpend); // uint256 oneAssetInGho = getAssetPriceInGho(e, 1, true); // assert to_mathint(maxGhoToSpend) <= ghoToSpend + 2*oneAssetInGho - 1; // } // @Title 4626: The GHO sold by buying asset using values from `getAssetAmountForBuyAsset(maxGho)` can be equal to `maxGho - 2*oneAssetInGho + 1 // STATUS: PASS // https://prover.certora.com/output/11775/944a0631a18846e39fe519d7e0f631b8?anonymousKey=613fae239e703cd94f7b6c2c9081bfaca941bf0a // External optimality of buyAsset. Show the tightness of (6) // (6a) // Holds: https://prover.certora.com/output/40748/b6ded393db3441649a6969f207037e79?anonymousKey=840fde79dad71cfc241479f2856eb27c0aa446b9 // Counterexample is buy fee = 1 BP, maxGhoToSpend = 1, oneAssetInGho = 1, ghoToSpend = 0 rule R6a_externalOptimalityOfBuyAsset { env e; feeLimits(e); priceLimits(e); uint256 maxGhoToSpend; uint256 ghoToSpend; _, ghoToSpend, _, _ = getAssetAmountForBuyAsset(e, maxGhoToSpend); uint256 oneAssetInGho = getAssetPriceInGho(e, 1, true); satisfy to_mathint(maxGhoToSpend) == ghoToSpend + 2*oneAssetInGho - 1; } ================================================ FILE: certora/steward/Makefile ================================================ default: help PATCH = applyHarness.patch CONTRACTS_DIR = ../../src MUNGED_DIR = munged help: @echo "usage:" @echo " make clean: remove all generated files (those ignored by git)" @echo " make $(MUNGED_DIR): create $(MUNGED_DIR) directory by applying the patch file to $(CONTRACTS_DIR)" @echo " make record: record a new patch file capturing the differences between $(CONTRACTS_DIR) and $(MUNGED_DIR)" munged: $(wildcard $(CONTRACTS_DIR)/*.sol) $(PATCH) rm -rf $@ mkdir $@ cp -r ../../src $@ patch -p0 -d $@ < $(PATCH) record: mkdir tmp cp -r ../../src tmp diff -ruN tmp $(MUNGED_DIR) | sed 's+tmp/++g' | sed 's+$(MUNGED_DIR)/++g' > $(PATCH) rm -rf tmp clean: git clean -fdX touch $(PATCH) ================================================ FILE: certora/steward/applyHarness.patch ================================================ diff -ruN .gitignore .gitignore --- .gitignore 1970-01-01 02:00:00.000000000 +0200 +++ .gitignore 2024-08-12 17:28:45.843705526 +0300 @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file ================================================ FILE: certora/steward/conf/GhoAaveSteward.conf ================================================ { "files": ["certora/steward/harness/GhoAaveSteward_Harness.sol"], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", "aave-stk-v1-5/=lib/aave-stk-v1-5", "ds-test/=lib/forge-std/lib/ds-test/src", "forge-std/=lib/forge-std/src", "aave-address-book/=lib/aave-address-book/src", "aave-helpers/=lib/aave-stk-v1-5/lib/aave-helpers", "aave-v3-core/=lib/aave-address-book/lib/aave-v3-core", "erc4626-tests/=lib/aave-stk-v1-5/lib/openzeppelin-contracts/lib/erc4626-tests", "openzeppelin-contracts/=lib/aave-stk-v1-5/lib/openzeppelin-contracts", "solidity-utils/=lib/solidity-utils/src" ], "build_cache": true, "optimistic_loop": true, "process": "emv", "prover_args": ["-depth 15","-mediumTimeout 1000"], "smt_timeout": "2000", "solc": "solc8.10", "verify": "GhoAaveSteward_Harness:certora/steward/specs/GhoAaveSteward.spec", "rule_sanity": "basic", "msg": "GhoAaveSteward: all rules" } ================================================ FILE: certora/steward/conf/GhoBucketSteward.conf ================================================ { "files": [ "src/contracts/misc/GhoBucketSteward.sol" ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", "aave-stk-v1-5/=lib/aave-stk-v1-5", "ds-test/=lib/forge-std/lib/ds-test/src", "forge-std/=lib/forge-std/src", "aave-address-book/=lib/aave-address-book/src", "aave-helpers/=lib/aave-stk-v1-5/lib/aave-helpers", "aave-v3-core/=lib/aave-address-book/lib/aave-v3-core", "erc4626-tests/=lib/aave-stk-v1-5/lib/openzeppelin-contracts/lib/erc4626-tests", "openzeppelin-contracts/=lib/aave-stk-v1-5/lib/openzeppelin-contracts", "solidity-utils/=lib/solidity-utils/src" ], "build_cache": true, "optimistic_loop": true, "process": "emv", "prover_args": ["-depth 15","-mediumTimeout 1000"], "smt_timeout": "2000", "solc": "solc8.10", "verify": "GhoBucketSteward:certora/steward/specs/GhoBucketSteward.spec", "rule_sanity": "basic", "msg": "GhoBucketSteward: all rules" } ================================================ FILE: certora/steward/conf/GhoCcipSteward.conf ================================================ { "files": ["certora/steward/harness/GhoCcipSteward_Harness.sol"], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", "aave-stk-v1-5/=lib/aave-stk-v1-5", "ds-test/=lib/forge-std/lib/ds-test/src", "forge-std/=lib/forge-std/src", "aave-address-book/=lib/aave-address-book/src", "aave-helpers/=lib/aave-stk-v1-5/lib/aave-helpers", "aave-v3-core/=lib/aave-address-book/lib/aave-v3-core", "erc4626-tests/=lib/aave-stk-v1-5/lib/openzeppelin-contracts/lib/erc4626-tests", "openzeppelin-contracts/=lib/aave-stk-v1-5/lib/openzeppelin-contracts", "solidity-utils/=lib/solidity-utils/src" ], "build_cache": true, "optimistic_loop": true, "process": "emv", "prover_args": ["-depth 15","-mediumTimeout 1000"], "smt_timeout": "2000", "solc": "solc8.10", "verify": "GhoCcipSteward_Harness:certora/steward/specs/GhoCcipSteward.spec", "rule_sanity": "basic", "msg": "GhoCcipSteward: all rules" } ================================================ FILE: certora/steward/conf/GhoGsmSteward.conf ================================================ { "files": [ "certora/steward/harness/GhoGsmSteward_Harness.sol", "src/contracts/facilitators/gsm/feeStrategy/FixedFeeStrategyFactory.sol" ], "link": [ "GhoGsmSteward_Harness:FIXED_FEE_STRATEGY_FACTORY=FixedFeeStrategyFactory", ], "packages": [ "@aave/core-v3/=lib/aave-v3-core", "@aave/periphery-v3/=lib/aave-v3-periphery", "@aave/=lib/aave-token", "@openzeppelin/=lib/openzeppelin-contracts", "aave-stk-v1-5/=lib/aave-stk-v1-5", "ds-test/=lib/forge-std/lib/ds-test/src", "forge-std/=lib/forge-std/src", "aave-address-book/=lib/aave-address-book/src", "aave-helpers/=lib/aave-stk-v1-5/lib/aave-helpers", "aave-v3-core/=lib/aave-address-book/lib/aave-v3-core", "erc4626-tests/=lib/aave-stk-v1-5/lib/openzeppelin-contracts/lib/erc4626-tests", "openzeppelin-contracts/=lib/aave-stk-v1-5/lib/openzeppelin-contracts", "solidity-utils/=lib/solidity-utils/src" ], "build_cache": true, "optimistic_loop": true, "process": "emv", "prover_args": ["-depth 15","-mediumTimeout 1000"], "smt_timeout": "2000", "solc": "solc8.10", "verify": "GhoGsmSteward_Harness:certora/steward/specs/GhoGsmSteward.spec", "rule_sanity": "basic", "msg": "GhoGsmSteward: all rules" } ================================================ FILE: certora/steward/harness/GhoAaveSteward_Harness.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {GhoAaveSteward} from '../munged/src/contracts/misc/GhoAaveSteward.sol'; contract GhoAaveSteward_Harness is GhoAaveSteward { constructor( address owner, address addressesProvider, address poolDataProvider, address ghoToken, address riskCouncil, BorrowRateConfig memory borrowRateConfig ) GhoAaveSteward( owner, addressesProvider, poolDataProvider, ghoToken, riskCouncil, borrowRateConfig ) {} } ================================================ FILE: certora/steward/harness/GhoCcipSteward_Harness.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {GhoCcipSteward} from '../../../src/contracts/misc/GhoCcipSteward.sol'; contract GhoCcipSteward_Harness is GhoCcipSteward { constructor( address ghoToken, address ghoTokenPool, address riskCouncil, bool bridgeLimitEnabled ) GhoCcipSteward(ghoToken, ghoTokenPool, riskCouncil, bridgeLimitEnabled) {} } ================================================ FILE: certora/steward/harness/GhoGsmSteward_Harness.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {GhoGsmSteward} from '../../../src/contracts/misc/GhoGsmSteward.sol'; contract GhoGsmSteward_Harness is GhoGsmSteward { constructor( address fixedRateStrategyFactory, address riskCouncil ) GhoGsmSteward(fixedRateStrategyFactory, riskCouncil) {} } ================================================ FILE: certora/steward/harness/GhoStewardV2_Harness.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {GhoStewardV2} from '../../../src/contracts/misc/GhoStewardV2.sol'; contract GhoStewardV2_Harness is GhoStewardV2 { constructor( address owner, address addressesProvider, address ghoToken, address fixedRateStrategyFactory, address riskCouncil ) GhoStewardV2(owner, addressesProvider, ghoToken, fixedRateStrategyFactory, riskCouncil) {} function get_gsmFeeStrategiesByRates( uint256 buyFee, uint256 sellFee ) external view returns (address) { return _gsmFeeStrategiesByRates[buyFee][sellFee]; } } ================================================ FILE: certora/steward/munged/.gitignore ================================================ * !.gitignore ================================================ FILE: certora/steward/specs/GhoAaveSteward.spec ================================================ /*=========================================================================== This is a specification file for the contract GhoAaveSteward. The rules were written base on the following: https://github.com/aave/gho-core/pull/388 We check the following aspects: - Limitations due to timelocks. - For the relevant functions, only autorized sender can call them. - When setting new paramethers they are in the correct range. - The new paramethers are indeed set. =============================================================================*/ methods { function _.getPool() external => NONDET; function _.getConfiguration(address) external => NONDET; function _.getPoolConfigurator() external => NONDET; function _.getBorrowCap(DataTypes.ReserveConfigurationMap memory) internal => get_BORROW_CAP_cvl() expect uint256 ; function _.setBorrowCap(address token, uint256 newCap) external => set_BORROW_CAP_cvl(token,newCap) expect void ALL; function _.getSupplyCap(DataTypes.ReserveConfigurationMap memory) internal => get_SUPPLY_CAP_cvl() expect uint256 ; function _.setSupplyCap(address token, uint256 newCap) external => set_SUPPLY_CAP_cvl(token,newCap) expect void ALL; function _._getInterestRatesForAsset(address) internal => get_INTEREST_RATE_cvl() expect (uint256,uint256,uint256,uint256); function getGhoTimelocks() external returns (IGhoAaveSteward.GhoDebounce) envfree; function MINIMUM_DELAY() external returns uint256 envfree; function RISK_COUNCIL() external returns address envfree; function owner() external returns address envfree; } ghost uint256 BORROW_CAP { axiom 1==1; } function get_BORROW_CAP_cvl() returns uint256 { return BORROW_CAP; } function set_BORROW_CAP_cvl(address token, uint256 newCap) { BORROW_CAP = newCap; } ghost uint256 SUPPLY_CAP { axiom 1==1; } function get_SUPPLY_CAP_cvl() returns uint256 { return SUPPLY_CAP; } function set_SUPPLY_CAP_cvl(address token, uint256 newCap) { SUPPLY_CAP = newCap; } ghost uint16 OPTIMAL_USAGE_RATIO; ghost uint32 BASE_VARIABLE_BORROW_RATE; ghost uint32 VARIABLE_RATE_SLOPE1; ghost uint32 VARIABLE_RATE_SLOPE2; function get_INTEREST_RATE_cvl() returns (uint16, uint32, uint32, uint32) { return (OPTIMAL_USAGE_RATIO,BASE_VARIABLE_BORROW_RATE,VARIABLE_RATE_SLOPE1,VARIABLE_RATE_SLOPE2); } /* ================================================================================= ================================================================================ Part 1: validity of the timelocks ================================================================================= ==============================================================================*/ // FUNCTION: updateGhoBorrowRate rule ghoBorrowRateLastUpdate__updated_only_by_updateGhoBorrowRate(method f) { env e; calldataarg args; uint40 ghoBorrowRateLastUpdate_before = getGhoTimelocks().ghoBorrowRateLastUpdate; f(e,args); uint40 ghoBorrowRateLastUpdate_after = getGhoTimelocks().ghoBorrowRateLastUpdate; assert (ghoBorrowRateLastUpdate_after != ghoBorrowRateLastUpdate_before) => f.selector == sig:updateGhoBorrowRate(uint16,uint32,uint32,uint32).selector; } rule updateGhoBorrowRate_update_correctly__ghoBorrowRateLastUpdate() { env e; uint16 optimalUsageRatio; uint32 baseVariableBorrowRate; uint32 variableRateSlope1; uint32 variableRateSlope2; updateGhoBorrowRate(e,optimalUsageRatio, baseVariableBorrowRate, variableRateSlope1, variableRateSlope2); assert getGhoTimelocks().ghoBorrowRateLastUpdate == require_uint40(e.block.timestamp); } rule updateGhoBorrowRate_timelock() { uint40 ghoBorrowRateLastUpdate_before = getGhoTimelocks().ghoBorrowRateLastUpdate; env e; uint16 optimalUsageRatio; uint32 baseVariableBorrowRate; uint32 variableRateSlope1; uint32 variableRateSlope2; updateGhoBorrowRate(e,optimalUsageRatio, baseVariableBorrowRate, variableRateSlope1, variableRateSlope2); assert to_mathint(e.block.timestamp) > ghoBorrowRateLastUpdate_before + MINIMUM_DELAY(); } // FUNCTION: updateGhoBorrowCap rule ghoBorrowCapLastUpdate__updated_only_by_updateGhoBorrowCap(method f) { env e; calldataarg args; uint40 ghoBorrowCapLastUpdate_before = getGhoTimelocks().ghoBorrowCapLastUpdate; f(e,args); uint40 ghoBorrowCapLastUpdate_after = getGhoTimelocks().ghoBorrowCapLastUpdate; assert (ghoBorrowCapLastUpdate_after != ghoBorrowCapLastUpdate_before) => f.selector == sig:updateGhoBorrowCap(uint256).selector; } rule updateGhoBorrowCap_update_correctly__ghoBorrowCapLastUpdate() { env e; uint256 newBorrowCap; updateGhoBorrowCap(e,newBorrowCap); assert getGhoTimelocks().ghoBorrowCapLastUpdate == require_uint40(e.block.timestamp); } rule updateGhoBorrowCap_timelock() { uint40 ghoBorrowCapLastUpdate_before = getGhoTimelocks().ghoBorrowCapLastUpdate; env e; uint256 newBorrowCap; updateGhoBorrowCap(e,newBorrowCap); assert to_mathint(e.block.timestamp) > ghoBorrowCapLastUpdate_before + MINIMUM_DELAY(); } // FUNCTION: updateGhoSupplyCap rule ghoSupplyCapLastUpdate__updated_only_by_updateGhoSupplyCap(method f) { env e; calldataarg args; uint40 ghoSupplyCapLastUpdate_before = getGhoTimelocks().ghoSupplyCapLastUpdate; f(e,args); uint40 ghoSupplyCapLastUpdate_after = getGhoTimelocks().ghoSupplyCapLastUpdate; assert (ghoSupplyCapLastUpdate_after != ghoSupplyCapLastUpdate_before) => f.selector == sig:updateGhoSupplyCap(uint256).selector; } rule updateGhoSupplyCap_update_correctly__ghoSupplyCapLastUpdate() { env e; uint256 newSupplyCap; updateGhoSupplyCap(e,newSupplyCap); assert getGhoTimelocks().ghoSupplyCapLastUpdate == require_uint40(e.block.timestamp); } rule updateGhoSupplyCap_timelock() { uint40 ghoSupplyCapLastUpdate_before = getGhoTimelocks().ghoSupplyCapLastUpdate; env e; uint256 newSupplyCap; updateGhoSupplyCap(e,newSupplyCap); assert to_mathint(e.block.timestamp) > ghoSupplyCapLastUpdate_before + MINIMUM_DELAY(); } /* ================================================================================= ================================================================================ Part 2: autorized message sender ================================================================================= ==============================================================================*/ rule only_RISK_COUNCIL_can_call__updateGhoBorrowCap() { env e; uint256 newBorrowCap; updateGhoBorrowCap(e,newBorrowCap); assert (e.msg.sender==RISK_COUNCIL()); } rule only_RISK_COUNCIL_can_call__updateGhoBorrowRate() { env e; uint16 optimalUsageRatio; uint32 baseVariableBorrowRate; uint32 variableRateSlope1; uint32 variableRateSlope2; updateGhoBorrowRate(e,optimalUsageRatio, baseVariableBorrowRate, variableRateSlope1, variableRateSlope2); assert (e.msg.sender==RISK_COUNCIL()); } rule only_RISK_COUNCIL_can_call__updateGhoSupplyCap() { env e; uint256 newSupplyCap; updateGhoSupplyCap(e,newSupplyCap); assert (e.msg.sender==RISK_COUNCIL()); } rule only_owner_can_call__setBorrowRateConfig() { env e; uint16 optimalUsageRatioMaxChange; uint32 baseVariableBorrowRateMaxChange; uint32 variableRateSlope1MaxChange; uint32 variableRateSlope2MaxChange; setBorrowRateConfig(e,optimalUsageRatioMaxChange, baseVariableBorrowRateMaxChange, variableRateSlope1MaxChange, variableRateSlope2MaxChange); assert (e.msg.sender==owner()); } /* ================================================================================= ================================================================================ Part 3: correctness of the main functions. We check the validity of the new paramethers values, and that are indeed set. ================================================================================= ==============================================================================*/ rule updateGhoBorrowCap__correctness() { env e; uint256 newBorrowCap; uint256 borrow_cap_before = BORROW_CAP; updateGhoBorrowCap(e,newBorrowCap); assert BORROW_CAP==newBorrowCap; uint256 borrow_cap_after = BORROW_CAP; assert to_mathint(borrow_cap_after) <= 2*borrow_cap_before; } rule updateGhoSupplyCap__correctness() { env e; uint256 newSupplyCap; uint256 supply_cap_before = SUPPLY_CAP; updateGhoSupplyCap(e,newSupplyCap); assert SUPPLY_CAP==newSupplyCap; uint256 supply_cap_after = SUPPLY_CAP; assert to_mathint(supply_cap_after) <= 2*supply_cap_before; } /* ================================================================================= Rule: sanity. Status: PASS. ================================================================================*/ rule sanity(method f) { env e; calldataarg args; f(e,args); satisfy true; } ================================================ FILE: certora/steward/specs/GhoBucketSteward.spec ================================================ //using FixedRateStrategyFactory as FAC; /*=========================================================================== This is a specification file for the contract GhoStewardV2. The rules were written base on the following: https://github.com/aave/gho-core/pull/388 We check the following aspects: - Limitations due to timelocks. - For the relevant functions, only autorized sender can call them. - When setting new paramethers they are in the correct range. - The new paramethers are indeed set. =============================================================================*/ methods { function _.getPool() external => NONDET; function _.getConfiguration(address) external => NONDET; function _.getPoolConfigurator() external => NONDET; function _.getFacilitatorBucket(address facilitator) external => get_BUCKET_CAPACITY_cvl() expect (uint256,uint256); function _.setFacilitatorBucketCapacity(address,uint128 newBucketCapacity) external => set_BUCKET_CAPACITY_cvl(newBucketCapacity) expect void; function owner() external returns (address) envfree; function getFacilitatorBucketCapacityTimelock(address) external returns (uint40) envfree; function MINIMUM_DELAY() external returns uint256 envfree; function RISK_COUNCIL() external returns address envfree; } ghost uint128 BUCKET_CAPACITY; function get_BUCKET_CAPACITY_cvl() returns (uint256,uint256) { uint256 ret; return (BUCKET_CAPACITY,ret); } function set_BUCKET_CAPACITY_cvl(uint128 newBucketCapacity) { BUCKET_CAPACITY = newBucketCapacity; } /* ================================================================================= ================================================================================ Part 1: validity of the timelocks ================================================================================= ==============================================================================*/ // FUNCTION: updateFacilitatorBucketCapacity rule timestamp__updated_only_by_updateFacilitatorBucketCapacity(method f) { env e; calldataarg args; address facilitator; uint40 timestamp_before = getFacilitatorBucketCapacityTimelock(facilitator); f(e,args); uint40 timestamp_after = getFacilitatorBucketCapacityTimelock(facilitator); assert (timestamp_before != timestamp_after) => f.selector == sig:updateFacilitatorBucketCapacity(address,uint128).selector; } rule updateFacilitatorBucketCapacity_update_correctly__timestamp() { env e; address facilitator; uint128 newBucketCapacity; updateFacilitatorBucketCapacity(e,facilitator,newBucketCapacity); assert getFacilitatorBucketCapacityTimelock(facilitator) == require_uint40(e.block.timestamp); } rule updateFacilitatorBucketCapacity_timelock() { env e; address facilitator; uint128 newBucketCapacity; uint40 timestamp_before = getFacilitatorBucketCapacityTimelock(facilitator); updateFacilitatorBucketCapacity(e,facilitator, newBucketCapacity); assert to_mathint(e.block.timestamp) > timestamp_before + MINIMUM_DELAY(); } /* ================================================================================= ================================================================================ Part 2: autorized message sender ================================================================================= ==============================================================================*/ rule only_RISK_COUNCIL_can_call__updateFacilitatorBucketCapacity() { env e; address facilitator; uint128 newBucketCapacity; updateFacilitatorBucketCapacity(e,facilitator,newBucketCapacity); assert (e.msg.sender==RISK_COUNCIL()); } rule only_owner_can_call__setControlledFacilitator() { env e; address[] facilitatorList; bool approve; setControlledFacilitator(e,facilitatorList,approve); assert (e.msg.sender==owner()); } /* ================================================================================= ================================================================================ Part 3: correctness of the main functions. We check the validity of the new paramethers values, and that are indeed set. ================================================================================= ==============================================================================*/ rule updateFacilitatorBucketCapacity__correctness() { env e; address facilitator; uint128 newBucketCapacity; uint256 bucket_capacity_before = BUCKET_CAPACITY; updateFacilitatorBucketCapacity(e,facilitator,newBucketCapacity); assert BUCKET_CAPACITY==newBucketCapacity; assert to_mathint(BUCKET_CAPACITY) <= 2*bucket_capacity_before; } /* ================================================================================= Rule: sanity. Status: PASS. ================================================================================*/ rule sanity(method f) { env e; calldataarg args; f(e,args); satisfy true; } ================================================ FILE: certora/steward/specs/GhoCcipSteward.spec ================================================ //using FixedFeeStrategyFactory as FAC; /*=========================================================================== This is a specification file for the contract GhoStewardV2. The rules were written base on the following: https://github.com/aave/gho-core/pull/388 We check the following aspects: - Limitations due to timelocks. - For the relevant functions, only autorized sender can call them. - When setting new paramethers they are in the correct range. - The new paramethers are indeed set. =============================================================================*/ methods { function _.getPool() external => NONDET; function _.getConfiguration(address) external => NONDET; function _.getPoolConfigurator() external => NONDET; function _.getCurrentOutboundRateLimiterState(uint64 remoteCS) external => OutboundRate(remoteCS) expect RateLimiter.TokenBucket; function _.getCurrentInboundRateLimiterState(uint64 remoteCS) external => InboundRate(remoteCS) expect RateLimiter.TokenBucket; function _.setChainRateLimiterConfig(uint64,RateLimiter.Config,RateLimiter.Config) external => NONDET; function getCcipTimelocks() external returns (IGhoCcipSteward.CcipDebounce) envfree; function MINIMUM_DELAY() external returns uint256 envfree; function RISK_COUNCIL() external returns address envfree; } ghost uint128 CAPACITY_OUT; ghost uint128 RATE_OUT; function OutboundRate(uint64 remoteCS) returns RateLimiter.TokenBucket { RateLimiter.TokenBucket ret; require ret.capacity == CAPACITY_OUT; require ret.rate == RATE_OUT; return ret; } ghost uint128 CAPACITY_IN; ghost uint128 RATE_IN; function InboundRate(uint64 remoteCS) returns RateLimiter.TokenBucket { RateLimiter.TokenBucket ret; require ret.capacity == CAPACITY_IN; require ret.rate == RATE_IN; return ret; } ghost uint128 BUY_FEE { axiom 1==1; } function get_BUY_FEE_cvl() returns uint128 { return BUY_FEE; } ghost uint128 SELL_FEE { axiom 1==1; } function get_SELL_FEE_cvl() returns uint128 { return SELL_FEE; } ghost address FEE_STRATEGY { axiom 1==1; } function set_FEE_STRATEGY(address strategy) { FEE_STRATEGY = strategy; } /* ================================================================================= ================================================================================ Part 1: validity of the timelocks ================================================================================= ==============================================================================*/ // FUNCTION: updateBridgeLimit rule bridgeLimitLastUpdate__updated_only_by_updateBridgeLimit(method f) { env e; calldataarg args; uint40 bridgeLimitLastUpdate_before = getCcipTimelocks().bridgeLimitLastUpdate; f(e,args); uint40 bridgeLimitLastUpdate_after = getCcipTimelocks().bridgeLimitLastUpdate; assert (bridgeLimitLastUpdate_before != bridgeLimitLastUpdate_after) => f.selector == sig:updateBridgeLimit(uint256).selector; } rule updateBridgeLimit_update_correctly__bridgeLimitLastUpdate() { env e; uint256 newBridgeLimit; updateBridgeLimit(e,newBridgeLimit); assert getCcipTimelocks().bridgeLimitLastUpdate == require_uint40(e.block.timestamp); } rule updateBridgeLimit_timelock() { env e; uint128 newBridgeLimit; uint40 before = getCcipTimelocks().bridgeLimitLastUpdate; updateBridgeLimit(e,newBridgeLimit); assert to_mathint(e.block.timestamp) > before + MINIMUM_DELAY(); } // FUNCTION: updateRateLimit rule rateLimitLastUpdate__updated_only_by_updateRateLimit(method f) { env e; calldataarg args; uint40 before = getCcipTimelocks().rateLimitLastUpdate; f(e,args); uint40 after = getCcipTimelocks().rateLimitLastUpdate; assert (before != after) => f.selector == sig:updateRateLimit(uint64,bool,uint128,uint128,bool,uint128,uint128).selector; } rule updateRateLimit_update_correctly__rateLimitLastUpdate() { env e; calldataarg args; updateRateLimit(e,args); assert getCcipTimelocks().rateLimitLastUpdate == require_uint40(e.block.timestamp); } rule updateRateLimit_timelock() { env e; calldataarg args; uint40 before = getCcipTimelocks().rateLimitLastUpdate; updateRateLimit(e,args); assert to_mathint(e.block.timestamp) > before + MINIMUM_DELAY(); } /* ================================================================================= ================================================================================ Part 2: autorized message sender ================================================================================= ==============================================================================*/ rule only_RISK_COUNCIL_can_call__updateBridgeLimit() { env e; calldataarg args; updateBridgeLimit(e,args); assert (e.msg.sender==RISK_COUNCIL()); } rule only_RISK_COUNCIL_can_call__updateRateLimit() { env e; calldataarg args; updateRateLimit(e,args); assert (e.msg.sender==RISK_COUNCIL()); } /* ================================================================================= ================================================================================ Part 3: correctness of the main functions. We check the validity of the new paramethers values. ================================================================================= ==============================================================================*/ rule updateBridgeLimit__correctness() { env e; uint64 remoteChainSelector; bool outboundEnabled; uint128 outboundCapacity; uint128 outboundRate; bool inboundEnabled; uint128 inboundCapacity; uint128 inboundRate; updateRateLimit(e, remoteChainSelector, outboundEnabled, outboundCapacity, outboundRate, inboundEnabled, inboundCapacity, inboundRate); assert to_mathint(outboundCapacity) <= 2*CAPACITY_OUT; assert to_mathint(outboundRate) <= 2*RATE_OUT; assert to_mathint(inboundCapacity) <= 2*CAPACITY_IN; assert to_mathint(inboundRate) <= 2*RATE_IN; } /* ================================================================================= Rule: sanity. Status: PASS. ================================================================================*/ rule sanity(method f) { env e; calldataarg args; f(e,args); satisfy true; } ================================================ FILE: certora/steward/specs/GhoGsmSteward.spec ================================================ using FixedFeeStrategyFactory as FAC; /*=========================================================================== This is a specification file for the contract GhoStewardV2. The rules were written base on the following: https://github.com/aave/gho-core/pull/388 We check the following aspects: - Limitations due to timelocks. - For the relevant functions, only autorized sender can call them. - When setting new paramethers they are in the correct range. - The new paramethers are indeed set. =============================================================================*/ methods { function _.getPool() external => NONDET; function _.getConfiguration(address) external => NONDET; function _.getPoolConfigurator() external => NONDET; function _.getExposureCap() external => get_EXPOSURE_CAP_cvl() expect uint256 ; function _.updateExposureCap(uint128 newCap) external => set_EXPOSURE_CAP_cvl(newCap) expect void ALL; function _.getBuyFee(uint256) external => get_BUY_FEE_cvl() expect uint256; function _.getSellFee(uint256) external => get_SELL_FEE_cvl() expect uint256; function _.updateFeeStrategy(address strategy) external => set_FEE_STRATEGY(strategy) expect void ALL; function getGsmTimelocks(address) external returns (IGhoGsmSteward.GsmDebounce) envfree; function GSM_FEE_RATE_CHANGE_MAX() external returns uint256 envfree; function MINIMUM_DELAY() external returns uint256 envfree; function RISK_COUNCIL() external returns address envfree; function FAC.getFixedFeeStrategy(uint256 buyFee, uint256 sellFee) external returns (address) envfree; } ghost uint128 EXPOSURE_CAP { axiom 1==1; } function get_EXPOSURE_CAP_cvl() returns uint128 { return EXPOSURE_CAP; } function set_EXPOSURE_CAP_cvl(uint128 newCap) { EXPOSURE_CAP = newCap; } ghost uint128 BUY_FEE { axiom 1==1; } function get_BUY_FEE_cvl() returns uint128 { return BUY_FEE; } ghost uint128 SELL_FEE { axiom 1==1; } function get_SELL_FEE_cvl() returns uint128 { return SELL_FEE; } ghost address FEE_STRATEGY { axiom 1==1; } function set_FEE_STRATEGY(address strategy) { FEE_STRATEGY = strategy; } /* ================================================================================= ================================================================================ Part 1: validity of the timelocks ================================================================================= ==============================================================================*/ // FUNCTION: updateGsmExposureCap rule gsmExposureCapLastUpdated__updated_only_by_updateGsmExposureCap(method f) { env e; calldataarg args; address gsm; uint40 gsmExposureCapLastUpdated_before = getGsmTimelocks(gsm).gsmExposureCapLastUpdated; f(e,args); uint40 gsmExposureCapLastUpdated_after = getGsmTimelocks(gsm).gsmExposureCapLastUpdated; assert (gsmExposureCapLastUpdated_after != gsmExposureCapLastUpdated_before) => f.selector == sig:updateGsmExposureCap(address,uint128).selector; } rule updateGsmExposureCap_update_correctly__gsmExposureCapLastUpdated() { env e; address gsm; uint128 newExposureCap; updateGsmExposureCap(e,gsm, newExposureCap); assert getGsmTimelocks(gsm).gsmExposureCapLastUpdated == require_uint40(e.block.timestamp); } rule updateGsmExposureCap_timelock() { env e; address gsm; uint128 newExposureCap; uint40 gsmExposureCapLastUpdated_before = getGsmTimelocks(gsm).gsmExposureCapLastUpdated; updateGsmExposureCap(e,gsm, newExposureCap); assert to_mathint(e.block.timestamp) > gsmExposureCapLastUpdated_before + MINIMUM_DELAY(); } // FUNCTION: updateGsmBuySellFees rule gsmFeeStrategyLastUpdated__updated_only_by_updateGsmBuySellFees(method f) { env e; calldataarg args; address gsm; uint40 gsmFeeStrategyLastUpdated_before = getGsmTimelocks(gsm).gsmFeeStrategyLastUpdated; f(e,args); uint40 gsmFeeStrategyLastUpdated_after = getGsmTimelocks(gsm).gsmFeeStrategyLastUpdated; assert (gsmFeeStrategyLastUpdated_after != gsmFeeStrategyLastUpdated_before) => f.selector == sig:updateGsmBuySellFees(address,uint256,uint256).selector; } rule updateGsmBuySellFees_update_correctly__gsmFeeStrategyLastUpdated() { env e; address gsm; uint256 buyFee; uint256 sellFee; updateGsmBuySellFees(e,gsm, buyFee, sellFee); assert getGsmTimelocks(gsm).gsmFeeStrategyLastUpdated == require_uint40(e.block.timestamp); } rule updateGsmBuySellFees_timelock() { env e; address gsm; uint256 buyFee; uint256 sellFee; uint40 gsmFeeStrategyLastUpdated_before = getGsmTimelocks(gsm).gsmFeeStrategyLastUpdated; updateGsmBuySellFees(e,gsm, buyFee, sellFee); assert to_mathint(e.block.timestamp) > gsmFeeStrategyLastUpdated_before + MINIMUM_DELAY(); } /* ================================================================================= ================================================================================ Part 2: autorized message sender ================================================================================= ==============================================================================*/ rule only_RISK_COUNCIL_can_call__updateGsmExposureCap() { env e; address gsm; uint128 newExposureCap; updateGsmExposureCap(e,gsm,newExposureCap); assert (e.msg.sender==RISK_COUNCIL()); } rule only_RISK_COUNCIL_can_call__updateGsmBuySellFees() { env e; address gsm; uint256 buyFee; uint256 sellFee; updateGsmBuySellFees(e,gsm,buyFee,sellFee); assert (e.msg.sender==RISK_COUNCIL()); } /* ================================================================================= ================================================================================ Part 3: correctness of the main functions. We check the validity of the new paramethers values, and that are indeed set. ================================================================================= ==============================================================================*/ rule updateGsmExposureCap__correctness() { env e; address gsm; uint128 newExposureCap; uint128 exposure_cap_before = EXPOSURE_CAP; updateGsmExposureCap(e,gsm,newExposureCap); assert EXPOSURE_CAP==newExposureCap; uint128 exposure_cap_after = EXPOSURE_CAP; assert to_mathint(exposure_cap_after) <= 2*exposure_cap_before; } rule updateGsmBuySellFees__correctness() { env e; address gsm; uint256 buyFee; uint256 sellFee; uint256 buyFee_before = BUY_FEE; uint256 sellFee_before = SELL_FEE; updateGsmBuySellFees(e,gsm,buyFee,sellFee); assert FAC.getFixedFeeStrategy(buyFee,sellFee)==FEE_STRATEGY; assert to_mathint(buyFee) <= buyFee_before + GSM_FEE_RATE_CHANGE_MAX(); assert to_mathint(sellFee) <= sellFee_before + GSM_FEE_RATE_CHANGE_MAX(); } /* ================================================================================= Rule: sanity. Status: PASS. ================================================================================*/ rule sanity(method f) { env e; calldataarg args; f(e,args); satisfy true; } ================================================ FILE: deploy/00_deploy_gho_token.ts ================================================ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { DeployFunction } from 'hardhat-deploy/types'; const func: DeployFunction = async function ({ getNamedAccounts, deployments, ...hre }: HardhatRuntimeEnvironment) { console.log(); console.log(`~~~~~~~ Beginning GHO Deployments ~~~~~~~`); const [_deployer, ...restSigners] = await hre.ethers.getSigners(); const { deploy } = deployments; const { deployer } = await getNamedAccounts(); const ghoResult = await deploy('GhoToken', { from: deployer, args: [deployer], log: true, }); console.log(`GHO Address: ${ghoResult.address}`); return true; }; func.id = 'GhoToken'; func.tags = ['GhoToken', 'full_gho_deploy']; export default func; ================================================ FILE: deploy/01_deploy_gho_oracle.ts ================================================ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { DeployFunction } from 'hardhat-deploy/types'; const func: DeployFunction = async function ({ getNamedAccounts, deployments, ...hre }: HardhatRuntimeEnvironment) { const { deploy } = deployments; const { deployer } = await getNamedAccounts(); const ghoOracle = await deploy('GhoOracle', { from: deployer, args: [], log: true, }); console.log(`Gho Oracle: ${ghoOracle.address}`); return true; }; func.id = 'GhoOracle'; func.tags = ['GhoOracle', 'full_gho_deploy']; export default func; ================================================ FILE: deploy/02_deploy_gho_atoken.ts ================================================ import { DeployFunction } from 'hardhat-deploy/types'; import { getPool } from '@aave/deploy-v3/dist/helpers/contract-getters'; import { ZERO_ADDRESS } from '../helpers/constants'; import { GhoAToken } from '../types'; const func: DeployFunction = async function ({ getNamedAccounts, deployments, ...hre }) { const { deploy } = deployments; const { deployer } = await getNamedAccounts(); const pool = await getPool(); const aTokenResult = await deploy('GhoAToken', { from: deployer, args: [pool.address], log: true, }); const aTokenImpl = (await hre.ethers.getContract('GhoAToken')) as GhoAToken; const initializeTx = await aTokenImpl.initialize( pool.address, // initializingPool ZERO_ADDRESS, // treasury ZERO_ADDRESS, // underlyingAsset ZERO_ADDRESS, // incentivesController 0, // aTokenDecimals 'GHO_ATOKEN_IMPL', // aTokenName 'GHO_ATOKEN_IMPL', // aTokenSymbol '0x10' // params ); await initializeTx.wait(); console.log(`AToken Implementation: ${aTokenResult.address}`); return true; }; func.id = 'GhoAToken'; func.tags = ['GhoAToken', 'full_gho_deploy']; export default func; ================================================ FILE: deploy/03_deploy_gho_stable_debt.ts ================================================ import { DeployFunction } from 'hardhat-deploy/types'; import { getPool } from '@aave/deploy-v3/dist/helpers/contract-getters'; import { ZERO_ADDRESS } from '../helpers/constants'; const func: DeployFunction = async function ({ getNamedAccounts, deployments, ...hre }) { const { deploy } = deployments; const { deployer } = await getNamedAccounts(); const pool = await getPool(); const stableDebtResult = await deploy('GhoStableDebtToken', { from: deployer, args: [pool.address], log: true, }); const stableDebtImpl = await hre.ethers.getContract('GhoStableDebtToken'); const initializeTx = await stableDebtImpl.initialize( pool.address, // initializingPool ZERO_ADDRESS, // underlyingAsset ZERO_ADDRESS, // incentivesController 0, // debtTokenDecimals 'GHO_STABLE_DEBT_TOKEN_IMPL', // debtTokenName 'GHO_STABLE_DEBT_TOKEN_IMPL', // debtTokenSymbol 0 // params ); await initializeTx.wait(); console.log(`Stable Debt Implementation: ${stableDebtResult.address}`); return true; }; func.id = 'GhoStableDebt'; func.tags = ['GhoStableDebt', 'full_gho_deploy']; export default func; ================================================ FILE: deploy/04_deploy_gho_variable_debt.ts ================================================ import { DeployFunction } from 'hardhat-deploy/types'; import { getPool } from '@aave/deploy-v3/dist/helpers/contract-getters'; import { ZERO_ADDRESS } from '../helpers/constants'; const func: DeployFunction = async function ({ getNamedAccounts, deployments, ...hre }) { const { deploy } = deployments; const { deployer } = await getNamedAccounts(); const pool = await getPool(); const variableDebtResult = await deploy('GhoVariableDebtToken', { from: deployer, args: [pool.address], log: true, }); const variableDebtImpl = await hre.ethers.getContract('GhoVariableDebtToken'); const initializeTx = await variableDebtImpl.initialize( pool.address, // initializingPool ZERO_ADDRESS, // underlyingAsset ZERO_ADDRESS, // incentivesController 0, // debtTokenDecimals 'GHO_VARIABLE_DEBT_TOKEN_IMPL', // debtTokenName 'GHO_VARIABLE_DEBT_TOKEN_IMPL', // debtTokenSymbol 0 // params ); await initializeTx.wait(); console.log(`Variable Debt Implementation: ${variableDebtResult.address}`); return true; }; func.id = 'GhoVariableDebt'; func.tags = ['GhoVariableDebt', 'full_gho_deploy']; export default func; ================================================ FILE: deploy/05_deploy_gho_interest_rate.ts ================================================ import { DeployFunction } from 'hardhat-deploy/types'; import { ghoReserveConfig } from '../helpers/config'; import { getPoolAddressesProvider } from '@aave/deploy-v3/dist/helpers/contract-getters'; const func: DeployFunction = async function ({ getNamedAccounts, deployments, ...hre }) { const { deploy } = deployments; const { deployer } = await getNamedAccounts(); const { INTEREST_RATE } = ghoReserveConfig; const addressesProvider = await getPoolAddressesProvider(); const interestRateStrategy = await deploy('GhoInterestRateStrategy', { from: deployer, args: [ addressesProvider.address, // addressesProvider INTEREST_RATE, // variableBorrowRate ], log: true, }); console.log(`Interest Rate Strategy: ${interestRateStrategy.address}`); return true; }; func.id = 'GhoInterestRateStrategy'; func.tags = ['GhoInterestRateStrategy', 'full_gho_deploy']; export default func; ================================================ FILE: deploy/06_deploy_gho_discount_rate.ts ================================================ import { DeployFunction } from 'hardhat-deploy/types'; const func: DeployFunction = async function ({ getNamedAccounts, deployments, ...hre }) { const { deploy } = deployments; const { deployer } = await getNamedAccounts(); const discountRateStrategy = await deploy('GhoDiscountRateStrategy', { from: deployer, args: [], log: true, }); console.log(`Discount Rate Strategy: ${discountRateStrategy.address}`); return true; }; func.id = 'GhoDiscountRateStrategy'; func.tags = ['GhoDiscountRateStrategy', 'full_gho_deploy']; export default func; ================================================ FILE: deploy/07_deploy_stakedAave_upgrade.ts ================================================ import { DeployFunction } from 'hardhat-deploy/types'; import { StakedTokenV2Rev3__factory, STAKE_AAVE_PROXY, waitForTx } from '@aave/deploy-v3'; const func: DeployFunction = async function ({ getNamedAccounts, deployments, ...hre }) { const { deploy } = deployments; const { deployer } = await getNamedAccounts(); const [deployerSigner] = await hre.ethers.getSigners(); const stkAaveProxy = await deployments.get(STAKE_AAVE_PROXY); const instance = StakedTokenV2Rev3__factory.connect(stkAaveProxy.address, deployerSigner); const stakedAaveImpl = await deploy('StakedAaveV3Impl', { from: deployer, contract: 'StakedAaveV3', args: [ await instance.STAKED_TOKEN(), await instance.REWARD_TOKEN(), await instance.UNSTAKE_WINDOW(), await instance.REWARDS_VAULT(), await instance.EMISSION_MANAGER(), '3153600000', // 100 years from the time of deployment ], log: true, }); console.log(`stakedAaveImpl Logic: ${stakedAaveImpl.address}`); }; func.id = 'StkAaveUpgrade'; func.tags = ['StkAaveUpgrade', 'full_gho_deploy']; export default func; ================================================ FILE: deploy/08_deploy_gho_flashminter.ts ================================================ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { DeployFunction } from 'hardhat-deploy/types'; import { ghoEntityConfig } from '../helpers/config'; import { getGhoToken } from '../helpers/contract-getters'; import { TREASURY_PROXY_ID, getPoolAddressesProvider, getTreasuryAddress } from '@aave/deploy-v3'; const func: DeployFunction = async function ({ getNamedAccounts, deployments, }: HardhatRuntimeEnvironment) { const { deploy } = deployments; const { deployer } = await getNamedAccounts(); const ghoToken = await getGhoToken(); const addressesProvider = await getPoolAddressesProvider(); const treasury = (await deployments.get(TREASURY_PROXY_ID)).address; // flash fee 100 = 1.00% const flashFee = ghoEntityConfig.flashMinterFee; const ghoFlashMinterResult = await deploy('GhoFlashMinter', { from: deployer, args: [ghoToken.address, treasury, flashFee, addressesProvider.address], log: true, }); console.log(`GHO FlashMinter: ${ghoFlashMinterResult.address}`); return true; }; func.id = 'GhoFlashMinter'; func.tags = ['GhoFlashMinter', 'full_gho_deploy']; export default func; ================================================ FILE: deploy/09_deploy_uighodataprovider.ts ================================================ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { DeployFunction } from 'hardhat-deploy/types'; import { getGhoToken } from '../helpers/contract-getters'; import { getPoolAddressesProvider } from '@aave/deploy-v3'; const func: DeployFunction = async function ({ getNamedAccounts, deployments, ...hre }: HardhatRuntimeEnvironment) { const { deploy } = deployments; const { deployer } = await getNamedAccounts(); const ghoToken = await getGhoToken(); const addressesProvider = await getPoolAddressesProvider(); const pool = await addressesProvider.getPool(); const uiGhoDataProviderResult = await deploy('UiGhoDataProvider', { from: deployer, args: [pool, ghoToken.address], }); console.log(`UiGhoDataProvider: ${uiGhoDataProviderResult.address}`); return true; }; func.id = 'UiGhoDataProvider'; func.tags = ['UiGhoDataProvider', 'full_gho_deploy']; export default func; ================================================ FILE: docs/gho-stewards.md ================================================ ## Overview These contracts each control different parameters related to GHO and its facilitators. They allow the Aave DAO and an approved Risk Council to change these parameters, according to set rules and configurations. Each Steward is designed to have a specific set of segregated responsibilities in an effort to avoid having to redeploy the entire original Steward. Instead, only the specific steward whose responsibilities are affected will have to be redeployed. ### [GhoAaveSteward](/src/contracts/misc/GhoAaveSteward.sol) This Steward manages parameters related to the GHO token. Specifically, it allows the Risk Council to change the following parameters: - Borrow Rate - Borrow Cap - Supply Cap In addition, the Aave DAO is allowed to change the configuration for the GHO Borrow Rate. This puts restrictions on how much the Risk Council is allowed to change parameters related to the borrow rate. There are 4 parameters that comprise the borrow rate: - `optimalUsageRatio` - `baseVariableBorrowRate` - `variableRateSlope1` - `variableRateSlope2` For example, the Aave DAO can specify that the optimalUsageRatio variable may only be changed by 3% at a time. ### [GhoBucketSteward](/src/contracts/misc/GhoBucketSteward.sol) This Steward allows the Risk Council to set the bucket capacities of controlled facilitators. Additionally, it allows the Aave DAO to add or remove controlled facilitators. ### [GhoCcipSteward](/src/contracts/misc/GhoCcipSteward.sol) This Steward allows the management of parameters related to CCIP token pools. It allows the Risk Council to update the CCIP bridge limit, and to update the CCIP rate limit configuration. ### [GhoGsmSteward](/src/contracts/misc/GhoGsmSteward.sol) This Steward allows the Risk Council to update the exposure cap of the GSM, and to update the buy and sell fees of the GSM. ### [RiskCouncilControlled](/src/contracts/misc/RiskCouncilControlled.sol) This is a helper contract to define the approved Risk Council and enforce its authority to call permissioned functions. ================================================ FILE: foundry.toml ================================================ [profile.default] src = 'src' out = 'out' test = 'src/test' script = 'src/script' cache_path = 'cache_forge' libs = ['node_modules', 'lib'] solc_version = "0.8.10" extra_output_files = ["metadata"] optimizer = true optimizer_runs = 200 [rpc_endpoints] mainnet = "${RPC_MAINNET}" arbitrum = "${RPC_ARBITRUM}" # See more config options https://github.com/foundry-rs/foundry/tree/master/config ================================================ FILE: hardhat.config.ts ================================================ import { getCommonNetworkConfig, hardhatNetworkSettings } from './helpers/hardhat-config'; import { config } from 'dotenv'; import { HardhatUserConfig } from 'hardhat/types'; import { DEFAULT_NAMED_ACCOUNTS, eEthereumNetwork } from '@aave/deploy-v3'; import '@nomicfoundation/hardhat-toolbox'; import '@nomicfoundation/hardhat-foundry'; import 'hardhat-deploy'; import 'hardhat-contract-sizer'; import 'hardhat-tracer'; config(); import { loadHardhatTasks } from './helpers/misc-utils'; import '@aave/deploy-v3'; // Prevent to load tasks before compilation and typechain if (!process.env.SKIP_LOAD) { loadHardhatTasks(['misc', 'testnet-setup', 'roles', 'main']); } const hardhatConfig: HardhatUserConfig = { networks: { hardhat: hardhatNetworkSettings, goerli: getCommonNetworkConfig(eEthereumNetwork.goerli, 5), sepolia: getCommonNetworkConfig('sepolia', 11155111), localhost: { url: 'http://127.0.0.1:8545', ...hardhatNetworkSettings, }, }, solidity: { compilers: [ { version: '0.8.10', settings: { optimizer: { enabled: true, runs: 100000, }, evmVersion: 'london', }, }, { version: '0.8.0', }, { version: '0.7.0', settings: {}, }, { version: '0.7.5', settings: { optimizer: { enabled: true, runs: 200 }, evmVersion: 'istanbul', }, }, { version: '0.6.12', settings: { optimizer: { enabled: true, runs: 200 }, evmVersion: 'istanbul', }, }, ], }, paths: { sources: './src/', tests: './test/', cache: './cache', artifacts: './artifacts', }, namedAccounts: { ...DEFAULT_NAMED_ACCOUNTS, }, typechain: { outDir: 'types', target: 'ethers-v5', alwaysGenerateOverloads: false, // should overloads with full signatures like deposit(uint256) be generated always, even if there are no overloads? }, gasReporter: { enabled: process.env.REPORT_GAS ? true : false, }, mocha: { timeout: 0, bail: true, }, external: { contracts: [ { artifacts: 'node_modules/@aave/deploy-v3/artifacts', deploy: 'node_modules/@aave/deploy-v3/dist/deploy', }, ], }, tracer: { nameTags: {}, }, }; export default hardhatConfig; ================================================ FILE: helpers/config.ts ================================================ import { ethers } from 'ethers'; import { ZERO_ADDRESS } from './constants'; export const helperAddresses = { wethWhale: '0xe78388b4ce79068e89bf8aa7f218ef6b9ab0e9d0', usdcWhale: '0x55fe002aeff02f77364de339a1292923a15844b8', stkAaveWhale: '0x32b61bb22cbe4834bc3e73dce85280037d944a4d', aaveToken: '0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9', aaveWhale: '0x26a78d5b6d7a7aceedd1e6ee3229b372a624d8b7', }; export const ghoTokenConfig = { TOKEN_NAME: 'Gho Token', TOKEN_SYMBOL: 'GHO', TOKEN_DECIMALS: 18, }; export const ghoReserveConfig = { INTEREST_RATE: ethers.utils.parseUnits('2.0', 25), }; export const ghoEntityConfig = { label: 'Aave V3 Mainnet Market', entityAddress: ZERO_ADDRESS, mintLimit: ethers.utils.parseUnits('1.0', 27), // 100M flashMinterLabel: 'GHO FlashMinter', flashMinterCapacity: ethers.utils.parseUnits('1.0', 26), // 10M flashMinterMaxFee: ethers.utils.parseUnits('10000', 0), flashMinterFee: 100, }; ================================================ FILE: helpers/constants.ts ================================================ // ---------------- // MATH // ---------------- import { BigNumber } from 'ethers'; import { parseEther, parseUnits } from 'ethers/lib/utils'; export const PERCENTAGE_FACTOR = '10000'; export const HALF_PERCENTAGE = BigNumber.from(PERCENTAGE_FACTOR).div(2).toString(); export const WAD = BigNumber.from(10).pow(18).toString(); export const HALF_WAD = BigNumber.from(WAD).div(2).toString(); export const RAY = BigNumber.from(10).pow(27).toString(); export const HALF_RAY = BigNumber.from(RAY).div(2).toString(); export const WAD_RAY_RATIO = parseUnits('1', 9).toString(); export const oneEther = parseUnits('1', 18); export const oneRay = parseUnits('1', 27); export const MAX_UINT_AMOUNT = '115792089237316195423570985008687907853269984665640564039457584007913129639935'; export const MAX_BORROW_CAP = '68719476735'; export const MAX_SUPPLY_CAP = '68719476735'; export const MAX_UNBACKED_MINT_CAP = '68719476735'; export const ONE_YEAR = '31536000'; export const YEAR = 31536000; export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; export const ONE_ADDRESS = '0x0000000000000000000000000000000000000001'; // ---------------- // PROTOCOL GLOBAL PARAMS // ---------------- export const MOCK_USD_PRICE_IN_WEI = '5848466240000000'; export const USD_ADDRESS = '0x10F7Fc1F91Ba351f9C629c5947AD69bD03C05b96'; export const AAVE_REFERRAL = '0'; export const TEST_SNAPSHOT_ID = '0x1'; export const HARDHAT_CHAINID = 31337; export const COVERAGE_CHAINID = 1337; export const MAX_UINT = BigNumber.from(MAX_UINT_AMOUNT); ================================================ FILE: helpers/contract-getters.ts ================================================ import { Contract } from 'ethers'; import { tEthereumAddress } from './types'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { AaveOracle, AaveProtocolDataProvider, GhoInterestRateStrategy, GhoAToken, GhoDiscountRateStrategy, GhoOracle, GhoToken, GhoVariableDebtToken, GhoStableDebtToken, AToken, BaseImmutableAdminUpgradeabilityProxy, Pool, AggregatorInterface, MintableERC20, IERC20, PoolConfigurator, StableDebtToken, VariableDebtToken, StakedAaveV3, GhoFlashMinter, } from '../types'; // Prevent error HH9 when importing this file inside tasks or helpers at Hardhat config load declare var hre: HardhatRuntimeEnvironment; export const getAaveOracle = async (address: tEthereumAddress): Promise => getContract('AaveOracle', address); export const getAaveProtocolDataProvider = async ( address: tEthereumAddress ): Promise => getContract('AaveProtocolDataProvider', address); export const getGhoInterestRateStrategy = async ( address?: tEthereumAddress ): Promise => getContract( 'GhoInterestRateStrategy', address || (await hre.deployments.get('GhoInterestRateStrategy')).address ); export const getGhoOracle = async (address?: tEthereumAddress): Promise => getContract('GhoOracle', address || (await hre.deployments.get('GhoOracle')).address); export const getGhoToken = async (address?: tEthereumAddress): Promise => getContract('GhoToken', address || (await hre.deployments.get('GhoToken')).address); export const getGhoAToken = async (address?: tEthereumAddress): Promise => getContract('GhoAToken', address || (await hre.deployments.get('GhoAToken')).address); export const getGhoDiscountRateStrategy = async ( address?: tEthereumAddress ): Promise => getContract( 'GhoDiscountRateStrategy', address || (await hre.deployments.get('GhoDiscountRateStrategy')).address ); export const getGhoVariableDebtToken = async ( address?: tEthereumAddress ): Promise => getContract( 'GhoVariableDebtToken', address || (await hre.deployments.get('GhoVariableDebtToken')).address ); export const getGhoStableDebtToken = async ( address?: tEthereumAddress ): Promise => getContract( 'GhoStableDebtToken', address || (await hre.deployments.get('GhoStableDebtToken')).address ); export const getBaseImmutableAdminUpgradeabilityProxy = async ( address: tEthereumAddress ): Promise => getContract('BaseImmutableAdminUpgradeabilityProxy', address); export const getERC20 = async (address: tEthereumAddress): Promise => getContract( '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol:IERC20', address ); export const getAggregatorInterface = async ( address?: tEthereumAddress ): Promise => getContract( 'AggregatorInterface', address || (await hre.deployments.get('AggregatorInterface')).address ); export const getPool = async (address: tEthereumAddress): Promise => getContract('Pool', address); export const getPoolConfigurator = async (address: tEthereumAddress): Promise => getContract('PoolConfigurator', address); export const getAToken = async (address?: tEthereumAddress): Promise => getContract('AToken', address || (await hre.deployments.get('AToken')).address); export const getVariableDebtToken = async ( address?: tEthereumAddress ): Promise => getContract( 'VariableDebtToken', address || (await hre.deployments.get('VariableDebtToken')).address ); export const getStableDebtToken = async (address?: tEthereumAddress): Promise => getContract('StableDebtToken', address || (await hre.deployments.get('StableDebtToken')).address); export const getStakedAave = async (address?: tEthereumAddress): Promise => { return ( await getContract( 'StakedAaveV3', address || (await hre.deployments.get('StakedAaveV3')).address ) ).connect((await hre.ethers.getSigners())[2]) as StakedAaveV3; }; export const getMintableErc20 = async (address?: tEthereumAddress): Promise => getContract('MintableERC20', address); export const getGhoFlashMinter = async (address?: tEthereumAddress): Promise => getContract('GhoFlashMinter', address); export const getContract = async ( id: string, address?: tEthereumAddress ): Promise => { const artifact = await hre.deployments.getArtifact(id); return hre.ethers.getContractAt( artifact.abi, address || (await (await hre.deployments.get(id)).address) ); }; ================================================ FILE: helpers/hardhat-config.ts ================================================ import { DEFAULT_BLOCK_GAS_LIMIT, eEthereumNetwork, FORK, FORK_BLOCK_NUMBER, getAlchemyKey, } from '@aave/deploy-v3'; import { HardhatNetworkForkingUserConfig } from 'hardhat/types'; import fs from 'fs'; /** HARDHAT NETWORK CONFIGURATION */ const MNEMONIC = process.env.MNEMONIC || ''; const MNEMONIC_PATH = "m/44'/60'/0'/0"; export const NETWORKS_RPC_URL: Record = { [eEthereumNetwork.main]: `https://eth-mainnet.alchemyapi.io/v2/${getAlchemyKey( eEthereumNetwork.main )}`, [eEthereumNetwork.hardhat]: 'http://localhost:8545', [eEthereumNetwork.goerli]: `https://eth-goerli.alchemyapi.io/v2/${getAlchemyKey( eEthereumNetwork.goerli )}`, sepolia: 'https://rpc.sepolia.ethpandaops.io', }; const GAS_PRICE_PER_NET: Record = {}; export const LIVE_NETWORKS: Record = { [eEthereumNetwork.main]: true, }; /** HARDHAT HELPERS */ export const buildForkConfig = (): HardhatNetworkForkingUserConfig | undefined => { let forkMode: HardhatNetworkForkingUserConfig | undefined; if (FORK && NETWORKS_RPC_URL[FORK]) { forkMode = { url: NETWORKS_RPC_URL[FORK] as string, }; console.log('Fork mode activated:', NETWORKS_RPC_URL[FORK]); if (FORK_BLOCK_NUMBER) { forkMode.blockNumber = FORK_BLOCK_NUMBER; } } return forkMode; }; export const hardhatNetworkSettings = { blockGasLimit: DEFAULT_BLOCK_GAS_LIMIT, throwOnTransactionFailures: true, throwOnCallFailures: true, chainId: 31337, forking: buildForkConfig(), saveDeployments: true, allowUnlimitedContractSize: true, tags: ['local'], accounts: FORK && !!MNEMONIC ? { mnemonic: MNEMONIC, path: MNEMONIC_PATH, initialIndex: 0, count: 10, } : undefined, }; export const getCommonNetworkConfig = (networkName: string, chainId?: number) => ({ url: NETWORKS_RPC_URL[networkName] || '', blockGasLimit: DEFAULT_BLOCK_GAS_LIMIT, chainId, gasPrice: GAS_PRICE_PER_NET[networkName] || undefined, ...(!!MNEMONIC && { accounts: { mnemonic: MNEMONIC, path: MNEMONIC_PATH, initialIndex: 0, count: 10, }, }), live: !!LIVE_NETWORKS[networkName], }); export function getRemappings() { return fs .readFileSync('hardhat-remappings.txt', 'utf8') .split('\n') .filter(Boolean) // remove empty lines .map((line) => { return line.trim().split('='); }); } ================================================ FILE: helpers/misc-utils.ts ================================================ import { formatEther } from 'ethers/lib/utils'; import path from 'path'; import fs from 'fs'; import { BigNumber, Signer } from 'ethers'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { tEthereumAddress } from './types'; import { config } from 'dotenv'; import Bluebird from 'bluebird'; import { getWalletBalances } from '@aave/deploy-v3'; config(); declare var hre: HardhatRuntimeEnvironment; export const evmSnapshot = async () => await hre.ethers.provider.send('evm_snapshot', []); export const evmRevert = async (id: string) => hre.ethers.provider.send('evm_revert', [id]); export const timeLatest = async () => { const block = await hre.ethers.provider.getBlock('latest'); return BigNumber.from(block.timestamp); }; export const setBlocktime = async (time: number) => { await hre.ethers.provider.send('evm_setNextBlockTimestamp', [time]); }; export const advanceTimeAndBlock = async function (forwardTime: number) { const currentBlockNumber = await hre.ethers.provider.getBlockNumber(); const currentBlock = await hre.ethers.provider.getBlock(currentBlockNumber); const currentTime = currentBlock.timestamp; const futureTime = currentTime + forwardTime; await hre.ethers.provider.send('evm_setNextBlockTimestamp', [futureTime]); await hre.ethers.provider.send('evm_mine', []); }; export const mine = async () => { await hre.ethers.provider.send('evm_mine', []); }; export const impersonateAccountHardhat = async (account: string): Promise => { await hre.network.provider.send('hardhat_setBalance', [account, '0xFFFFFFFFFFFFFFFFFFFFFFFFF']); await hre.network.provider.request({ method: 'hardhat_impersonateAccount', params: [account], }); return await hre.ethers.getSigner(account); }; export const setCode = async (address: tEthereumAddress, bytecode: string): Promise => { await hre.network.provider.request({ method: 'hardhat_setCode', params: [address, bytecode], }); }; export const setStorageAt = async ( address: tEthereumAddress, storageSlot: string, storageValue: string ): Promise => { await hre.network.provider.request({ method: 'hardhat_setStorageAt', params: [address, storageSlot, storageValue], }); }; export const getProxyImplementationBySlot = async (proxyAddress: tEthereumAddress) => { const proxyImplementationSlot = await hre.ethers.provider.getStorageAt( proxyAddress, '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' ); return hre.ethers.utils.getAddress( hre.ethers.utils.defaultAbiCoder.decode(['address'], proxyImplementationSlot).toString() ); }; export const FULL_DEPLOY = process.env.FULL_DEPLOY === 'true'; export const loadHardhatTasks = (taskFolders: string[]): void => taskFolders.forEach((folder) => { const tasksPath = path.join(__dirname, '../tasks', folder); fs.readdirSync(tasksPath) .filter((pth) => pth.includes('.ts') || pth.includes('.js')) .forEach((task) => { require(`${tasksPath}/${task}`); }); }); export const setSignersBalance = async () => { const signers = await hre.ethers.getSigners(); await Bluebird.each(signers, async (signer) => { await setBalance(signer.address); }); const balances = await getWalletBalances(); console.log('Balances'); console.log('========'); console.table(balances); }; export const setBalance = async (address: string) => { await hre.ethers.provider.send('hardhat_setBalance', [address, '0x3635c9adc5dea00000']); console.log( 'Updated balance', address, formatEther(await hre.ethers.provider.getBalance(address)) ); }; ================================================ FILE: helpers/types.ts ================================================ export type tEthereumAddress = string; export type tStringTokenSmallUnits = string; // 1 wei, or 1 basic unit of USDC, or 1 basic unit of DAI ================================================ FILE: package.json ================================================ { "name": "@aave/gho", "description": "GHO core smart contracts", "keywords": [ "gho", "stablecoin", "aave", "protocol", "ethereum", "solidity" ], "files": [ "src", "artifacts", "types" ], "engines": { "node": ">=16.0.0" }, "scripts": { "hardhat": "hardhat", "clean": "npm run clean:hh & npm run clean:forge", "clean:hh": "hardhat clean", "clean:forge": "forge clean", "prettier:check": "prettier --check .", "prettier:write": "prettier --write .", "prepare": "husky install", "compile": "npm run compile:hh && npm run compile:forge", "compile:hh": "rm -rf ./artifacts ./cache ./types && SKIP_LOAD=true hardhat compile", "compile:forge": "forge build --force", "test": "npm run test:hh && npm run test:forge", "test:hh": ". ./setup-test-env.sh && hardhat test ./test/*.ts", "test:forge": "forge test -vvv --no-match-test 'skip'", "test-goerli:fork": ". ./setup-test-env.sh && FORK=goerli npm run test:hh --no-compile", "test-goerli:fork:skip-deploy": ". ./setup-test-env.sh && FORK=goerli SKIP_DEPLOY=true npm run test:hh", "test:stkAave": ". ./setup-test-env.sh && hardhat test ./test/__setup.test.ts ./test/stkAave-upgrade.test.ts", "coverage:hh": ". ./setup-test-env.sh && hardhat coverage", "coverage:forge": "forge coverage --report summary", "coverage:forge:report": "forge coverage --report lcov && lcov --remove lcov.info \"*test/*\" \"*script/*\" \"*node_modules/*\" --output-file lcov.info --rc lcov_branch_coverage=1 && genhtml lcov.info --branch-coverage --output-dir coverage", "deploy-testnet": ". ./setup-test-env.sh && hardhat deploy-and-setup", "deploy-testnet:goerli": "HARDHAT_NETWORK=goerli npm run deploy-testnet", "deploy-testnet:goerli:fork": "FORK=goerli npm run deploy-testnet", "deploy-testnet:sepolia": "HARDHAT_NETWORK=sepolia npm run deploy-testnet", "deploy-testnet:sepolia:fork": "FORK=sepolia npm run deploy-testnet", "ci:clean": "rm -rf ./artifacts ./cache ./types ./cache_forge", "ci:test": "npm run test" }, "devDependencies": { "@aave/deploy-v3": "^1.55.3", "@nomicfoundation/hardhat-foundry": "^1.1.1", "@nomicfoundation/hardhat-toolbox": "^2.0.2", "@typechain/ethers-v5": "^10.0.0", "@typechain/hardhat": "^6.0.0", "@types/bluebird": "^3.5.38", "@types/chai": "^4.3.1", "@types/mocha": "^9.1.0", "@types/node": "^17.0.25", "bluebird": "^3.7.2", "chai": "^4.3.6", "dotenv": "^16.0.3", "eth-sig-util": "^3.0.1", "ethereumjs-util": "^7.1.5", "ethers": "^5.6.4", "hardhat": "^2.20.1", "hardhat-contract-sizer": "^2.6.1", "hardhat-deploy": "^0.11.22", "hardhat-gas-reporter": "^1.0.9", "hardhat-tracer": "^1.2.1", "husky": "^8.0.3", "jsondiffpatch": "^0.4.1", "lint-staged": "^13.1.0", "prettier": "^2.8.3", "prettier-plugin-solidity": "^1.1.1", "ts-node": "^10.7.0", "typechain": "^8.0.0", "typescript": "^4.6.3" }, "overrides": { "@nomicfoundation/hardhat-toolbox": { "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@0.3.0-beta.13" } }, "lint-staged": { "*.{ts,js,md,sol}": "prettier --write" }, "author": "Aave", "contributors": [ "Emilio Frangella ", "Steven Valeri ", "Miguel Martinez ", "David Racero ", "Peter Michael ", "Mark Hinschberger " ], "license": "MIT", "repository": { "type": "git", "url": "git://github.com/aave/gho" } } ================================================ FILE: remappings.txt ================================================ @aave/core-v3/=lib/aave-v3-core/ @aave/periphery-v3/=lib/aave-v3-periphery/ @aave/=lib/aave-token/ @openzeppelin/=lib/openzeppelin-contracts/ aave-stk-v1-5/=lib/aave-stk-v1-5/ ds-test/=lib/forge-std/lib/ds-test/src/ eth-gas-reporter/=node_modules/eth-gas-reporter/ forge-std/=lib/forge-std/src/ hardhat-deploy/=node_modules/hardhat-deploy/ hardhat/=node_modules/hardhat/ aave-address-book/=lib/aave-address-book/src/ aave-helpers/=lib/aave-stk-v1-5/lib/aave-helpers/ aave-v3-core/=lib/aave-address-book/lib/aave-v3-core/ aave-v3-periphery/=lib/aave-address-book/lib/aave-v3-periphery/ erc4626-tests/=lib/aave-stk-v1-5/lib/openzeppelin-contracts/lib/erc4626-tests/ openzeppelin-contracts/=lib/aave-stk-v1-5/lib/openzeppelin-contracts/ solidity-utils/=lib/solidity-utils/src/ ================================================ FILE: setup-test-env.sh ================================================ #!/bin/bash # @dev # This bash script ensures a clean repository # and loads environment variables for testing and deploying GHO source code. export NODE_OPTIONS="--max_old_space_size=16384" set -e echo "[BASH] Setting up testnet environment" if [ ! "$COVERAGE" = true ]; then # remove hardhat and artifacts cache npm run ci:clean # compile contracts npm run compile else echo "[BASH] Skipping compilation to keep coverage artifacts" fi # Export MARKET_NAME variable to use Aave market as testnet deployment setup export MARKET_NAME="Test" # Deploy stkAave in local export ENABLE_REWARDS="true" echo "[BASH] Testnet environment ready" ================================================ FILE: slither.config.json ================================================ { "detectors_to_exclude": "naming-convention", "filter_paths": "(node_modules/|lib/|src/test/)" } ================================================ FILE: src/contracts/facilitators/aave/interestStrategy/FixedRateStrategyFactory.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {IDefaultInterestRateStrategy} from '@aave/core-v3/contracts/interfaces/IDefaultInterestRateStrategy.sol'; import {VersionedInitializable} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol'; import {IFixedRateStrategyFactory} from './interfaces/IFixedRateStrategyFactory.sol'; import {GhoInterestRateStrategy} from './GhoInterestRateStrategy.sol'; /** * @title FixedRateStrategyFactory * @author Aave Labs * @notice Factory contract to create and keep record of Aave v3 fixed rate strategy contracts * @dev `GhoInterestRateStrategy` is used to provide a fixed interest rate strategy. */ contract FixedRateStrategyFactory is VersionedInitializable, IFixedRateStrategyFactory { ///@inheritdoc IFixedRateStrategyFactory address public immutable POOL_ADDRESSES_PROVIDER; mapping(uint256 => address) internal _strategiesByRate; address[] internal _strategies; /** * @dev Constructor * @param addressesProvider The address of the PoolAddressesProvider of Aave V3 Pool */ constructor(address addressesProvider) { require(addressesProvider != address(0), 'INVALID_ADDRESSES_PROVIDER'); POOL_ADDRESSES_PROVIDER = addressesProvider; } /** * @notice FixedRateStrategyFactory initializer * @dev assumes that the addresses provided are fixed rate deployed strategies. * @param fixedRateStrategiesList List of fixed rate strategies */ function initialize(address[] memory fixedRateStrategiesList) external initializer { for (uint256 i = 0; i < fixedRateStrategiesList.length; i++) { address fixedRateStrategy = fixedRateStrategiesList[i]; uint256 rate = IDefaultInterestRateStrategy(fixedRateStrategy).getBaseVariableBorrowRate(); _strategiesByRate[rate] = fixedRateStrategy; _strategies.push(fixedRateStrategy); emit RateStrategyCreated(fixedRateStrategy, rate); } } ///@inheritdoc IFixedRateStrategyFactory function createStrategies(uint256[] memory fixedRateList) public returns (address[] memory) { address[] memory strategies = new address[](fixedRateList.length); for (uint256 i = 0; i < fixedRateList.length; i++) { uint256 rate = fixedRateList[i]; address cachedStrategy = _strategiesByRate[rate]; if (cachedStrategy == address(0)) { cachedStrategy = address(new GhoInterestRateStrategy(POOL_ADDRESSES_PROVIDER, rate)); _strategiesByRate[rate] = cachedStrategy; _strategies.push(cachedStrategy); emit RateStrategyCreated(cachedStrategy, rate); } strategies[i] = cachedStrategy; } return strategies; } ///@inheritdoc IFixedRateStrategyFactory function getAllStrategies() external view returns (address[] memory) { return _strategies; } ///@inheritdoc IFixedRateStrategyFactory function getStrategyByRate(uint256 borrowRate) external view returns (address) { return _strategiesByRate[borrowRate]; } /// @inheritdoc IFixedRateStrategyFactory function REVISION() public pure virtual override returns (uint256) { return 1; } /// @inheritdoc VersionedInitializable function getRevision() internal pure virtual override returns (uint256) { return REVISION(); } } ================================================ FILE: src/contracts/facilitators/aave/interestStrategy/GhoDiscountRateStrategy.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {WadRayMath} from '@aave/core-v3/contracts/protocol/libraries/math/WadRayMath.sol'; import {IGhoDiscountRateStrategy} from './interfaces/IGhoDiscountRateStrategy.sol'; /** * @title GhoDiscountRateStrategy contract * @author Aave * @notice Implements the calculation of the discount rate depending on the current strategy */ contract GhoDiscountRateStrategy is IGhoDiscountRateStrategy { using WadRayMath for uint256; /** * @dev Amount of debt that is entitled to get a discount per unit of discount token * Expressed with the number of decimals of the discounted token */ uint256 public constant GHO_DISCOUNTED_PER_DISCOUNT_TOKEN = 100e18; /** * @dev Percentage of discount to apply to the part of the debt that is entitled to get a discount * Expressed in bps, a value of 3000 results in 30.00% */ uint256 public constant DISCOUNT_RATE = 0.3e4; /** * @dev Minimum balance amount of discount token to be entitled to a discount * Expressed with the number of decimals of the discount token */ uint256 public constant MIN_DISCOUNT_TOKEN_BALANCE = 1e15; /** * @dev Minimum balance amount of debt token to be entitled to a discount * Expressed with the number of decimals of the debt token */ uint256 public constant MIN_DEBT_TOKEN_BALANCE = 1e18; /// @inheritdoc IGhoDiscountRateStrategy function calculateDiscountRate( uint256 debtBalance, uint256 discountTokenBalance ) external pure override returns (uint256) { if (discountTokenBalance < MIN_DISCOUNT_TOKEN_BALANCE || debtBalance < MIN_DEBT_TOKEN_BALANCE) { return 0; } else { uint256 discountedBalance = discountTokenBalance.wadMul(GHO_DISCOUNTED_PER_DISCOUNT_TOKEN); if (discountedBalance >= debtBalance) { return DISCOUNT_RATE; } else { return (discountedBalance * DISCOUNT_RATE) / debtBalance; } } } } ================================================ FILE: src/contracts/facilitators/aave/interestStrategy/GhoInterestRateStrategy.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {DataTypes} from '@aave/core-v3/contracts/protocol/libraries/types/DataTypes.sol'; import {IDefaultInterestRateStrategy} from '@aave/core-v3/contracts/interfaces/IDefaultInterestRateStrategy.sol'; import {IReserveInterestRateStrategy} from '@aave/core-v3/contracts/interfaces/IReserveInterestRateStrategy.sol'; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; /** * @title GhoInterestRateStrategy * @author Aave * @notice Implements the calculation of GHO interest rates, which defines a fixed variable borrow rate. * @dev The variable borrow interest rate is fixed at deployment time. The rest of parameters are zeroed. */ contract GhoInterestRateStrategy is IDefaultInterestRateStrategy { /// @inheritdoc IDefaultInterestRateStrategy uint256 public constant OPTIMAL_USAGE_RATIO = 0; /// @inheritdoc IDefaultInterestRateStrategy uint256 public constant OPTIMAL_STABLE_TO_TOTAL_DEBT_RATIO = 0; /// @inheritdoc IDefaultInterestRateStrategy uint256 public constant MAX_EXCESS_USAGE_RATIO = 0; /// @inheritdoc IDefaultInterestRateStrategy uint256 public constant MAX_EXCESS_STABLE_TO_TOTAL_DEBT_RATIO = 0; /// @inheritdoc IDefaultInterestRateStrategy IPoolAddressesProvider public immutable ADDRESSES_PROVIDER; // Base variable borrow rate when usage rate = 0. Expressed in ray uint256 internal immutable _baseVariableBorrowRate; /** * @dev Constructor * @param addressesProvider The address of the PoolAddressesProvider * @param borrowRate The variable borrow rate (expressed in ray) */ constructor(address addressesProvider, uint256 borrowRate) { ADDRESSES_PROVIDER = IPoolAddressesProvider(addressesProvider); _baseVariableBorrowRate = borrowRate; } /// @inheritdoc IDefaultInterestRateStrategy function getVariableRateSlope1() external pure returns (uint256) { return 0; } /// @inheritdoc IDefaultInterestRateStrategy function getVariableRateSlope2() external pure returns (uint256) { return 0; } /// @inheritdoc IDefaultInterestRateStrategy function getStableRateSlope1() external pure returns (uint256) { return 0; } /// @inheritdoc IDefaultInterestRateStrategy function getStableRateSlope2() external pure returns (uint256) { return 0; } /// @inheritdoc IDefaultInterestRateStrategy function getStableRateExcessOffset() external pure returns (uint256) { return 0; } /// @inheritdoc IDefaultInterestRateStrategy function getBaseStableBorrowRate() public pure returns (uint256) { return 0; } /// @inheritdoc IDefaultInterestRateStrategy function getBaseVariableBorrowRate() external view override returns (uint256) { return _baseVariableBorrowRate; } /// @inheritdoc IDefaultInterestRateStrategy function getMaxVariableBorrowRate() external view override returns (uint256) { return _baseVariableBorrowRate; } /// @inheritdoc IReserveInterestRateStrategy function calculateInterestRates( DataTypes.CalculateInterestRatesParams memory ) public view override returns (uint256, uint256, uint256) { return (0, 0, _baseVariableBorrowRate); } } ================================================ FILE: src/contracts/facilitators/aave/interestStrategy/ZeroDiscountRateStrategy.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {IGhoDiscountRateStrategy} from '../interestStrategy/interfaces/IGhoDiscountRateStrategy.sol'; /** * @title ZeroDiscountRateStrategy * @author Aave * @notice Discount Rate Strategy that always return zero discount rate. */ contract ZeroDiscountRateStrategy is IGhoDiscountRateStrategy { /// @inheritdoc IGhoDiscountRateStrategy function calculateDiscountRate( uint256 debtBalance, uint256 discountTokenBalance ) external view override returns (uint256) { return 0; } } ================================================ FILE: src/contracts/facilitators/aave/interestStrategy/interfaces/IFixedRateStrategyFactory.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /** * @title IFixedRateStrategyFactory * @author Aave Labs * @notice Defines the interface of the FixedRateStrategyFactory */ interface IFixedRateStrategyFactory { /** * @dev Emitted when a new strategy is created * @param strategy The address of the new fixed rate strategy * @param rate The rate of the new strategy, expressed in ray (e.g. 0.0150e27 results in 1.50%) */ event RateStrategyCreated(address indexed strategy, uint256 indexed rate); /** * @notice Creates new fixed rate strategy contracts from a list of rates. * @dev Returns the address of a cached contract if a strategy with same rate already exists * @param fixedRateList The list of rates for interest rates strategies, expressed in ray (e.g. 0.0150e27 results in 1.50%) * @return The list of fixed interest rate strategy contracts */ function createStrategies(uint256[] memory fixedRateList) external returns (address[] memory); /** * @notice Returns the address of the Pool Addresses Provider of Aave * @return The address of the PoolAddressesProvider of Aave */ function POOL_ADDRESSES_PROVIDER() external view returns (address); /** * @notice Returns all the fixed interest rate strategy contracts of the factory * @return The list of fixed interest rate strategy contracts */ function getAllStrategies() external view returns (address[] memory); /** * @notice Returns the fixed interest rate strategy contract which corresponds to the given rate. * @dev Returns `address(0)` if there is no interest rate strategy for the given rate * @param rate The rate of the fixed interest rate strategy contract * @return The address of the fixed interest rate strategy contract */ function getStrategyByRate(uint256 rate) external view returns (address); /** * @notice Returns the FixedRateStrategyFactory revision number * @return The revision number */ function REVISION() external pure returns (uint256); } ================================================ FILE: src/contracts/facilitators/aave/interestStrategy/interfaces/IGhoDiscountRateStrategy.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /** * @title IGhoDiscountRateStrategy * @author Aave * @notice Defines the basic interface of the GhoDiscountRateStrategy */ interface IGhoDiscountRateStrategy { /** * @notice Calculates the discount rate depending on the debt and discount token balances * @param debtBalance The debt balance of the user * @param discountTokenBalance The discount token balance of the user * @return The discount rate, as a percentage - the maximum can be 10000 = 100.00% */ function calculateDiscountRate( uint256 debtBalance, uint256 discountTokenBalance ) external view returns (uint256); } ================================================ FILE: src/contracts/facilitators/aave/misc/UiGhoDataProvider.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; import {DataTypes} from '@aave/core-v3/contracts/protocol/libraries/types/DataTypes.sol'; import {IGhoToken} from '../../../gho/interfaces/IGhoToken.sol'; import {GhoDiscountRateStrategy} from '../interestStrategy/GhoDiscountRateStrategy.sol'; import {IGhoVariableDebtToken} from '../tokens/interfaces/IGhoVariableDebtToken.sol'; import {IUiGhoDataProvider} from './interfaces/IUiGhoDataProvider.sol'; /** * @title UiGhoDataProvider * @author Aave * @notice Data provider of GHO token as a reserve within the Aave Protocol */ contract UiGhoDataProvider is IUiGhoDataProvider { IPool public immutable POOL; IGhoToken public immutable GHO; /** * @dev Constructor * @param pool The address of the Pool contract * @param ghoToken The address of the GhoToken contract */ constructor(IPool pool, IGhoToken ghoToken) { POOL = pool; GHO = ghoToken; } /// @inheritdoc IUiGhoDataProvider function getGhoReserveData() public view override returns (GhoReserveData memory) { DataTypes.ReserveData memory baseData = POOL.getReserveData(address(GHO)); IGhoVariableDebtToken debtToken = IGhoVariableDebtToken(baseData.variableDebtTokenAddress); GhoDiscountRateStrategy discountRateStrategy = GhoDiscountRateStrategy( debtToken.getDiscountRateStrategy() ); (uint256 bucketCapacity, uint256 bucketLevel) = GHO.getFacilitatorBucket( baseData.aTokenAddress ); return GhoReserveData({ ghoBaseVariableBorrowRate: baseData.currentVariableBorrowRate, ghoDiscountedPerToken: discountRateStrategy.GHO_DISCOUNTED_PER_DISCOUNT_TOKEN(), ghoDiscountRate: discountRateStrategy.DISCOUNT_RATE(), ghoMinDebtTokenBalanceForDiscount: discountRateStrategy.MIN_DEBT_TOKEN_BALANCE(), ghoMinDiscountTokenBalanceForDiscount: discountRateStrategy.MIN_DISCOUNT_TOKEN_BALANCE(), ghoReserveLastUpdateTimestamp: baseData.lastUpdateTimestamp, ghoCurrentBorrowIndex: baseData.variableBorrowIndex, aaveFacilitatorBucketLevel: bucketLevel, aaveFacilitatorBucketMaxCapacity: bucketCapacity }); } /// @inheritdoc IUiGhoDataProvider function getGhoUserData(address user) public view override returns (GhoUserData memory) { DataTypes.ReserveData memory baseData = POOL.getReserveData(address(GHO)); IGhoVariableDebtToken debtToken = IGhoVariableDebtToken(baseData.variableDebtTokenAddress); address discountToken = debtToken.getDiscountToken(); return GhoUserData({ userGhoDiscountPercent: debtToken.getDiscountPercent(user), userDiscountTokenBalance: IERC20(discountToken).balanceOf(user), userPreviousGhoBorrowIndex: debtToken.getPreviousIndex(user), userGhoScaledBorrowBalance: debtToken.scaledBalanceOf(user) }); } } ================================================ FILE: src/contracts/facilitators/aave/misc/interfaces/IUiGhoDataProvider.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; /** * @title IUiGhoDataProvider * @author Aave * @notice Defines the basic interface of the UiGhoDataProvider */ interface IUiGhoDataProvider { struct GhoReserveData { uint256 ghoBaseVariableBorrowRate; uint256 ghoDiscountedPerToken; uint256 ghoDiscountRate; uint256 ghoMinDebtTokenBalanceForDiscount; uint256 ghoMinDiscountTokenBalanceForDiscount; uint40 ghoReserveLastUpdateTimestamp; uint128 ghoCurrentBorrowIndex; uint256 aaveFacilitatorBucketLevel; uint256 aaveFacilitatorBucketMaxCapacity; } struct GhoUserData { uint256 userGhoDiscountPercent; uint256 userDiscountTokenBalance; uint256 userPreviousGhoBorrowIndex; uint256 userGhoScaledBorrowBalance; } /** * @notice Returns data of the GHO reserve and the Aave Facilitator * @return An object with information related to the GHO reserve and the Aave Facilitator */ function getGhoReserveData() external view returns (GhoReserveData memory); /** * @notice Returns data of the user's position on GHO * @return An object with information related to the user's position with regard to GHO */ function getGhoUserData(address user) external view returns (GhoUserData memory); } ================================================ FILE: src/contracts/facilitators/aave/oracle/GhoOracle.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; /** * @title GhoOracle * @notice Price feed for GHO (USD denominated) * @dev Price fixed at 1 USD, Chainlink format with 8 decimals * @author Aave */ contract GhoOracle { int256 public constant GHO_PRICE = 1e8; /** * @notice Returns the price of a unit of GHO (USD denominated) * @dev GHO price is fixed at 1 USD * @return The price of a unit of GHO (with 8 decimals) */ function latestAnswer() external pure returns (int256) { return GHO_PRICE; } /** * @notice Returns the number of decimals the price is formatted with * @return The number of decimals */ function decimals() external pure returns (uint8) { return 8; } } ================================================ FILE: src/contracts/facilitators/aave/tokens/GhoAToken.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; import {GPv2SafeERC20} from '@aave/core-v3/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol'; import {VersionedInitializable} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol'; import {Errors} from '@aave/core-v3/contracts/protocol/libraries/helpers/Errors.sol'; import {WadRayMath} from '@aave/core-v3/contracts/protocol/libraries/math/WadRayMath.sol'; import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; import {IAToken} from '@aave/core-v3/contracts/interfaces/IAToken.sol'; import {IAaveIncentivesController} from '@aave/core-v3/contracts/interfaces/IAaveIncentivesController.sol'; import {IInitializableAToken} from '@aave/core-v3/contracts/interfaces/IInitializableAToken.sol'; import {ScaledBalanceTokenBase} from '@aave/core-v3/contracts/protocol/tokenization/base/ScaledBalanceTokenBase.sol'; import {IncentivizedERC20} from '@aave/core-v3/contracts/protocol/tokenization/base/IncentivizedERC20.sol'; import {EIP712Base} from '@aave/core-v3/contracts/protocol/tokenization/base/EIP712Base.sol'; // Gho Imports import {IGhoToken} from '../../../gho/interfaces/IGhoToken.sol'; import {IGhoFacilitator} from '../../../gho/interfaces/IGhoFacilitator.sol'; import {IGhoAToken} from './interfaces/IGhoAToken.sol'; import {GhoVariableDebtToken} from './GhoVariableDebtToken.sol'; /** * @title GhoAToken * @author Aave * @notice Implementation of the interest bearing token for the Aave protocol */ contract GhoAToken is VersionedInitializable, ScaledBalanceTokenBase, EIP712Base, IGhoAToken { using WadRayMath for uint256; using GPv2SafeERC20 for IERC20; bytes32 public constant PERMIT_TYPEHASH = keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)'); uint256 public constant ATOKEN_REVISION = 0x1; address internal _treasury; address internal _underlyingAsset; // Gho Storage GhoVariableDebtToken internal _ghoVariableDebtToken; address internal _ghoTreasury; /// @inheritdoc VersionedInitializable function getRevision() internal pure virtual override returns (uint256) { return ATOKEN_REVISION; } /** * @dev Constructor. * @param pool The address of the Pool contract */ constructor( IPool pool ) ScaledBalanceTokenBase(pool, 'GHO_ATOKEN_IMPL', 'GHO_ATOKEN_IMPL', 0) EIP712Base() { // Intentionally left blank } /// @inheritdoc IInitializableAToken function initialize( IPool initializingPool, address treasury, address underlyingAsset, IAaveIncentivesController incentivesController, uint8 aTokenDecimals, string calldata aTokenName, string calldata aTokenSymbol, bytes calldata params ) external override initializer { require(initializingPool == POOL, Errors.POOL_ADDRESSES_DO_NOT_MATCH); _setName(aTokenName); _setSymbol(aTokenSymbol); _setDecimals(aTokenDecimals); _treasury = treasury; _underlyingAsset = underlyingAsset; _incentivesController = incentivesController; _domainSeparator = _calculateDomainSeparator(); emit Initialized( underlyingAsset, address(POOL), treasury, address(incentivesController), aTokenDecimals, aTokenName, aTokenSymbol, params ); } /// @inheritdoc IAToken function mint( address caller, address onBehalfOf, uint256 amount, uint256 index ) external virtual override onlyPool returns (bool) { revert(Errors.OPERATION_NOT_SUPPORTED); } /// @inheritdoc IAToken function burn( address from, address receiverOfUnderlying, uint256 amount, uint256 index ) external virtual override onlyPool { revert(Errors.OPERATION_NOT_SUPPORTED); } /// @inheritdoc IAToken function mintToTreasury(uint256 amount, uint256 index) external virtual override onlyPool { revert(Errors.OPERATION_NOT_SUPPORTED); } /// @inheritdoc IAToken function transferOnLiquidation( address from, address to, uint256 value ) external virtual override onlyPool { revert(Errors.OPERATION_NOT_SUPPORTED); } /// @inheritdoc IERC20 function balanceOf( address user ) public view virtual override(IncentivizedERC20, IERC20) returns (uint256) { return 0; } /// @inheritdoc IERC20 function totalSupply() public view virtual override(IncentivizedERC20, IERC20) returns (uint256) { return 0; } /// @inheritdoc IAToken function RESERVE_TREASURY_ADDRESS() external view override returns (address) { return _treasury; } /// @inheritdoc IAToken function UNDERLYING_ASSET_ADDRESS() external view override returns (address) { return _underlyingAsset; } /** * @notice Transfers the underlying asset to `target`. * @dev It performs a mint of GHO on behalf of the `target` * @dev Used by the Pool to transfer assets in borrow(), withdraw() and flashLoan() * @param target The recipient of the underlying * @param amount The amount getting transferred */ function transferUnderlyingTo(address target, uint256 amount) external virtual override onlyPool { IGhoToken(_underlyingAsset).mint(target, amount); } /// @inheritdoc IAToken function handleRepayment( address user, address onBehalfOf, uint256 amount ) external virtual override onlyPool { uint256 balanceFromInterest = _ghoVariableDebtToken.getBalanceFromInterest(onBehalfOf); if (amount <= balanceFromInterest) { _ghoVariableDebtToken.decreaseBalanceFromInterest(onBehalfOf, amount); } else { _ghoVariableDebtToken.decreaseBalanceFromInterest(onBehalfOf, balanceFromInterest); IGhoToken(_underlyingAsset).burn(amount - balanceFromInterest); } } /// @inheritdoc IGhoFacilitator function distributeFeesToTreasury() external virtual override { uint256 balance = IERC20(_underlyingAsset).balanceOf(address(this)); IERC20(_underlyingAsset).transfer(_ghoTreasury, balance); emit FeesDistributedToTreasury(_ghoTreasury, _underlyingAsset, balance); } /// @inheritdoc IAToken function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external override { revert(Errors.OPERATION_NOT_SUPPORTED); } /** * @notice Overrides the parent _transfer to force validated transfer() and transferFrom() * @param from The source address * @param to The destination address * @param amount The amount getting transferred */ function _transfer(address from, address to, uint128 amount) internal override { revert(Errors.OPERATION_NOT_SUPPORTED); } /** * @dev Overrides the base function to fully implement IAToken * @dev see `EIP712Base.DOMAIN_SEPARATOR()` for more detailed documentation */ function DOMAIN_SEPARATOR() public view override(IAToken, EIP712Base) returns (bytes32) { return super.DOMAIN_SEPARATOR(); } /** * @dev Overrides the base function to fully implement IAToken * @dev see `EIP712Base.nonces()` for more detailed documentation */ function nonces(address owner) public view override(IAToken, EIP712Base) returns (uint256) { return super.nonces(owner); } /// @inheritdoc EIP712Base function _EIP712BaseId() internal view override returns (string memory) { return name(); } /// @inheritdoc IAToken function rescueTokens(address token, address to, uint256 amount) external override onlyPoolAdmin { require(token != _underlyingAsset, Errors.UNDERLYING_CANNOT_BE_RESCUED); IERC20(token).safeTransfer(to, amount); } /// @inheritdoc IGhoAToken function setVariableDebtToken(address ghoVariableDebtToken) external override onlyPoolAdmin { require(address(_ghoVariableDebtToken) == address(0), 'VARIABLE_DEBT_TOKEN_ALREADY_SET'); require(ghoVariableDebtToken != address(0), 'ZERO_ADDRESS_NOT_VALID'); _ghoVariableDebtToken = GhoVariableDebtToken(ghoVariableDebtToken); emit VariableDebtTokenSet(ghoVariableDebtToken); } /// @inheritdoc IGhoAToken function getVariableDebtToken() external view override returns (address) { return address(_ghoVariableDebtToken); } /// @inheritdoc IGhoFacilitator function updateGhoTreasury(address newGhoTreasury) external override onlyPoolAdmin { require(newGhoTreasury != address(0), 'ZERO_ADDRESS_NOT_VALID'); address oldGhoTreasury = _ghoTreasury; _ghoTreasury = newGhoTreasury; emit GhoTreasuryUpdated(oldGhoTreasury, newGhoTreasury); } /// @inheritdoc IGhoFacilitator function getGhoTreasury() external view override returns (address) { return _ghoTreasury; } } ================================================ FILE: src/contracts/facilitators/aave/tokens/GhoStableDebtToken.sol ================================================ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.10; import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; import {VersionedInitializable} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol'; import {MathUtils} from '@aave/core-v3/contracts/protocol/libraries/math/MathUtils.sol'; import {WadRayMath} from '@aave/core-v3/contracts/protocol/libraries/math/WadRayMath.sol'; import {Errors} from '@aave/core-v3/contracts/protocol/libraries/helpers/Errors.sol'; import {IAaveIncentivesController} from '@aave/core-v3/contracts/interfaces/IAaveIncentivesController.sol'; import {IInitializableDebtToken} from '@aave/core-v3/contracts/interfaces/IInitializableDebtToken.sol'; import {IStableDebtToken} from '@aave/core-v3/contracts/interfaces/IStableDebtToken.sol'; import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; import {EIP712Base} from '@aave/core-v3/contracts/protocol/tokenization/base/EIP712Base.sol'; import {DebtTokenBase} from '@aave/core-v3/contracts/protocol/tokenization/base/DebtTokenBase.sol'; import {IncentivizedERC20} from '@aave/core-v3/contracts/protocol/tokenization/base/IncentivizedERC20.sol'; import {SafeCast} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/SafeCast.sol'; /** * @title GhoStableDebtToken * @author Aave * @notice Implements a non-usable and reverting stable debt token, only used for listing configuration purposes. * @dev All write operations revert and read functions return 0 */ contract GhoStableDebtToken is DebtTokenBase, IncentivizedERC20, IStableDebtToken { using WadRayMath for uint256; using SafeCast for uint256; uint256 public constant DEBT_TOKEN_REVISION = 0x1; /** * @dev Constructor. * @param pool The address of the Pool contract */ constructor( IPool pool ) DebtTokenBase() IncentivizedERC20(pool, 'GHO_STABLE_DEBT_TOKEN_IMPL', 'GHO_STABLE_DEBT_TOKEN_IMPL', 0) { // Intentionally left blank } /// @inheritdoc IInitializableDebtToken function initialize( IPool initializingPool, address underlyingAsset, IAaveIncentivesController incentivesController, uint8 debtTokenDecimals, string memory debtTokenName, string memory debtTokenSymbol, bytes calldata params ) external override initializer { require(initializingPool == POOL, Errors.POOL_ADDRESSES_DO_NOT_MATCH); _setName(debtTokenName); _setSymbol(debtTokenSymbol); _setDecimals(debtTokenDecimals); _underlyingAsset = underlyingAsset; _incentivesController = incentivesController; _domainSeparator = _calculateDomainSeparator(); emit Initialized( underlyingAsset, address(POOL), address(incentivesController), debtTokenDecimals, debtTokenName, debtTokenSymbol, params ); } /// @inheritdoc VersionedInitializable function getRevision() internal pure virtual override returns (uint256) { return DEBT_TOKEN_REVISION; } /// @inheritdoc IStableDebtToken function getAverageStableRate() external pure virtual override returns (uint256) { return 0; } /// @inheritdoc IStableDebtToken function getUserLastUpdated(address) external pure virtual override returns (uint40) { return 0; } /// @inheritdoc IStableDebtToken function getUserStableRate(address) external pure virtual override returns (uint256) { return 0; } /// @inheritdoc IERC20 function balanceOf(address) public pure virtual override returns (uint256) { return 0; } /// @inheritdoc IStableDebtToken function mint( address, address, uint256, uint256 ) external virtual override onlyPool returns (bool, uint256, uint256) { revert(Errors.OPERATION_NOT_SUPPORTED); } /// @inheritdoc IStableDebtToken function burn(address, uint256) external virtual override onlyPool returns (uint256, uint256) { revert(Errors.OPERATION_NOT_SUPPORTED); } /// @inheritdoc IStableDebtToken function getSupplyData() external pure override returns (uint256, uint256, uint256, uint40) { return (0, 0, 0, 0); } /// @inheritdoc IStableDebtToken function getTotalSupplyAndAvgRate() external pure override returns (uint256, uint256) { return (0, 0); } /// @inheritdoc IERC20 function totalSupply() public pure virtual override returns (uint256) { return 0; } /// @inheritdoc IStableDebtToken function getTotalSupplyLastUpdated() external pure override returns (uint40) { return 0; } /// @inheritdoc IStableDebtToken function principalBalanceOf(address) external pure virtual override returns (uint256) { return 0; } /// @inheritdoc IStableDebtToken function UNDERLYING_ASSET_ADDRESS() external view override returns (address) { return _underlyingAsset; } /// @inheritdoc EIP712Base function _EIP712BaseId() internal view override returns (string memory) { return name(); } /** * @dev Being non transferrable, the debt token does not implement any of the * standard ERC20 functions for transfer and allowance. */ function transfer(address, uint256) external virtual override returns (bool) { revert(Errors.OPERATION_NOT_SUPPORTED); } function allowance(address, address) external view virtual override returns (uint256) { revert(Errors.OPERATION_NOT_SUPPORTED); } function approve(address, uint256) external virtual override returns (bool) { revert(Errors.OPERATION_NOT_SUPPORTED); } function transferFrom(address, address, uint256) external virtual override returns (bool) { revert(Errors.OPERATION_NOT_SUPPORTED); } function increaseAllowance(address, uint256) external virtual override returns (bool) { revert(Errors.OPERATION_NOT_SUPPORTED); } function decreaseAllowance(address, uint256) external virtual override returns (bool) { revert(Errors.OPERATION_NOT_SUPPORTED); } } ================================================ FILE: src/contracts/facilitators/aave/tokens/GhoVariableDebtToken.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; import {SafeCast} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/SafeCast.sol'; import {VersionedInitializable} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol'; import {WadRayMath} from '@aave/core-v3/contracts/protocol/libraries/math/WadRayMath.sol'; import {PercentageMath} from '@aave/core-v3/contracts/protocol/libraries/math/PercentageMath.sol'; import {Errors} from '@aave/core-v3/contracts/protocol/libraries/helpers/Errors.sol'; import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; import {IAaveIncentivesController} from '@aave/core-v3/contracts/interfaces/IAaveIncentivesController.sol'; import {IInitializableDebtToken} from '@aave/core-v3/contracts/interfaces/IInitializableDebtToken.sol'; import {IVariableDebtToken} from '@aave/core-v3/contracts/interfaces/IVariableDebtToken.sol'; import {EIP712Base} from '@aave/core-v3/contracts/protocol/tokenization/base/EIP712Base.sol'; import {DebtTokenBase} from '@aave/core-v3/contracts/protocol/tokenization/base/DebtTokenBase.sol'; // Gho Imports import {IGhoDiscountRateStrategy} from '../interestStrategy/interfaces/IGhoDiscountRateStrategy.sol'; import {IGhoVariableDebtToken} from './interfaces/IGhoVariableDebtToken.sol'; import {ScaledBalanceTokenBase} from './base/ScaledBalanceTokenBase.sol'; /** * @title GhoVariableDebtToken * @author Aave * @notice Implements a variable debt token to track the borrowing positions of users * at variable rate mode for GHO * @dev Transfer and approve functionalities are disabled since its a non-transferable token */ contract GhoVariableDebtToken is DebtTokenBase, ScaledBalanceTokenBase, IGhoVariableDebtToken { using WadRayMath for uint256; using SafeCast for uint256; using PercentageMath for uint256; uint256 public constant DEBT_TOKEN_REVISION = 0x1; // Corresponding AToken to this DebtToken address internal _ghoAToken; // Token that grants discounts off the debt interest IERC20 internal _discountToken; // Strategy of the discount rate to apply on debt interests IGhoDiscountRateStrategy internal _discountRateStrategy; struct GhoUserState { // Accumulated debt interest of the user uint128 accumulatedDebtInterest; // Discount percent of the user (expressed in bps) uint16 discountPercent; } // Map of users' address and their gho state data (userAddress => ghoUserState) mapping(address => GhoUserState) internal _ghoUserState; /** * @dev Only discount token can call functions marked by this modifier. */ modifier onlyDiscountToken() { require(address(_discountToken) == msg.sender, 'CALLER_NOT_DISCOUNT_TOKEN'); _; } /** * @dev Only AToken can call functions marked by this modifier. */ modifier onlyAToken() { require(_ghoAToken == msg.sender, 'CALLER_NOT_A_TOKEN'); _; } /** * @dev Constructor. * @param pool The address of the Pool contract */ constructor( IPool pool ) DebtTokenBase() ScaledBalanceTokenBase(pool, 'GHO_VARIABLE_DEBT_TOKEN_IMPL', 'GHO_VARIABLE_DEBT_TOKEN_IMPL', 0) { // Intentionally left blank } /// @inheritdoc IInitializableDebtToken function initialize( IPool initializingPool, address underlyingAsset, IAaveIncentivesController incentivesController, uint8 debtTokenDecimals, string memory debtTokenName, string memory debtTokenSymbol, bytes calldata params ) external override initializer { require(initializingPool == POOL, Errors.POOL_ADDRESSES_DO_NOT_MATCH); _setName(debtTokenName); _setSymbol(debtTokenSymbol); _setDecimals(debtTokenDecimals); _underlyingAsset = underlyingAsset; _incentivesController = incentivesController; _domainSeparator = _calculateDomainSeparator(); emit Initialized( underlyingAsset, address(POOL), address(incentivesController), debtTokenDecimals, debtTokenName, debtTokenSymbol, params ); } /// @inheritdoc VersionedInitializable function getRevision() internal pure virtual override returns (uint256) { return DEBT_TOKEN_REVISION; } /// @inheritdoc IERC20 function balanceOf(address user) public view virtual override returns (uint256) { uint256 scaledBalance = super.balanceOf(user); if (scaledBalance == 0) { return 0; } uint256 index = POOL.getReserveNormalizedVariableDebt(_underlyingAsset); uint256 previousIndex = _userState[user].additionalData; uint256 balance = scaledBalance.rayMul(index); if (index == previousIndex) { return balance; } uint256 discountPercent = _ghoUserState[user].discountPercent; if (discountPercent != 0) { uint256 balanceIncrease = balance - scaledBalance.rayMul(previousIndex); balance -= balanceIncrease.percentMul(discountPercent); } return balance; } /// @inheritdoc IVariableDebtToken function mint( address user, address onBehalfOf, uint256 amount, uint256 index ) external virtual override onlyPool returns (bool, uint256) { if (user != onBehalfOf) { _decreaseBorrowAllowance(onBehalfOf, user, amount); } return (_mintScaled(user, onBehalfOf, amount, index), scaledTotalSupply()); } /// @inheritdoc IVariableDebtToken function burn( address from, uint256 amount, uint256 index ) external virtual override onlyPool returns (uint256) { _burnScaled(from, address(0), amount, index); return scaledTotalSupply(); } /** * @notice Returns the amount of tokens in existence. * @dev It does not account for active discounts of the users. The discount is deducted from the user's debt at * repayment / liquidation time, so this function does always return a greater or equal value than the actual total * supply. * @return The amount of tokens in existence (without accounting for active discounts on debt) */ function totalSupply() public view virtual override returns (uint256) { return super.totalSupply().rayMul(POOL.getReserveNormalizedVariableDebt(_underlyingAsset)); } /// @inheritdoc EIP712Base function _EIP712BaseId() internal view override returns (string memory) { return name(); } /** * @dev Being non transferrable, the debt token does not implement any of the * standard ERC20 functions for transfer and allowance. */ function transfer(address, uint256) external virtual override returns (bool) { revert(Errors.OPERATION_NOT_SUPPORTED); } function allowance(address, address) external view virtual override returns (uint256) { revert(Errors.OPERATION_NOT_SUPPORTED); } function approve(address, uint256) external virtual override returns (bool) { revert(Errors.OPERATION_NOT_SUPPORTED); } function transferFrom(address, address, uint256) external virtual override returns (bool) { revert(Errors.OPERATION_NOT_SUPPORTED); } function increaseAllowance(address, uint256) external virtual override returns (bool) { revert(Errors.OPERATION_NOT_SUPPORTED); } function decreaseAllowance(address, uint256) external virtual override returns (bool) { revert(Errors.OPERATION_NOT_SUPPORTED); } /// @inheritdoc IVariableDebtToken function UNDERLYING_ASSET_ADDRESS() external view override returns (address) { return _underlyingAsset; } /// @inheritdoc IGhoVariableDebtToken function setAToken(address ghoAToken) external override onlyPoolAdmin { require(_ghoAToken == address(0), 'ATOKEN_ALREADY_SET'); require(ghoAToken != address(0), 'ZERO_ADDRESS_NOT_VALID'); _ghoAToken = ghoAToken; emit ATokenSet(ghoAToken); } /// @inheritdoc IGhoVariableDebtToken function getAToken() external view override returns (address) { return _ghoAToken; } /// @inheritdoc IGhoVariableDebtToken function updateDiscountRateStrategy( address newDiscountRateStrategy ) external override onlyPoolAdmin { require(newDiscountRateStrategy != address(0), 'ZERO_ADDRESS_NOT_VALID'); address oldDiscountRateStrategy = address(_discountRateStrategy); _discountRateStrategy = IGhoDiscountRateStrategy(newDiscountRateStrategy); emit DiscountRateStrategyUpdated(oldDiscountRateStrategy, newDiscountRateStrategy); } /// @inheritdoc IGhoVariableDebtToken function getDiscountRateStrategy() external view override returns (address) { return address(_discountRateStrategy); } /// @inheritdoc IGhoVariableDebtToken function updateDiscountToken(address newDiscountToken) external override onlyPoolAdmin { require(newDiscountToken != address(0), 'ZERO_ADDRESS_NOT_VALID'); address oldDiscountToken = address(_discountToken); _discountToken = IERC20(newDiscountToken); emit DiscountTokenUpdated(oldDiscountToken, newDiscountToken); } /// @inheritdoc IGhoVariableDebtToken function getDiscountToken() external view override returns (address) { return address(_discountToken); } /// @inheritdoc IGhoVariableDebtToken function updateDiscountDistribution( address sender, address recipient, uint256 senderDiscountTokenBalance, uint256 recipientDiscountTokenBalance, uint256 amount ) external override onlyDiscountToken { // Skipping computation in case of discount token self-transfer if (sender == recipient) { return; } uint256 senderPreviousScaledBalance = super.balanceOf(sender); uint256 recipientPreviousScaledBalance = super.balanceOf(recipient); // Skipping computation in case users do not have a position if (senderPreviousScaledBalance == 0 && recipientPreviousScaledBalance == 0) { return; } uint256 index = POOL.getReserveNormalizedVariableDebt(_underlyingAsset); uint256 balanceIncrease; uint256 discountScaled; if (senderPreviousScaledBalance > 0) { (balanceIncrease, discountScaled) = _accrueDebtOnAction( sender, senderPreviousScaledBalance, _ghoUserState[sender].discountPercent, index ); _burn(sender, discountScaled.toUint128()); _refreshDiscountPercent( sender, super.balanceOf(sender).rayMul(index), senderDiscountTokenBalance - amount, _ghoUserState[sender].discountPercent ); emit Transfer(address(0), sender, balanceIncrease); emit Mint(address(0), sender, balanceIncrease, balanceIncrease, index); } if (recipientPreviousScaledBalance > 0) { (balanceIncrease, discountScaled) = _accrueDebtOnAction( recipient, recipientPreviousScaledBalance, _ghoUserState[recipient].discountPercent, index ); _burn(recipient, discountScaled.toUint128()); _refreshDiscountPercent( recipient, super.balanceOf(recipient).rayMul(index), recipientDiscountTokenBalance + amount, _ghoUserState[recipient].discountPercent ); emit Transfer(address(0), recipient, balanceIncrease); emit Mint(address(0), recipient, balanceIncrease, balanceIncrease, index); } } /// @inheritdoc IGhoVariableDebtToken function getDiscountPercent(address user) external view override returns (uint256) { return _ghoUserState[user].discountPercent; } /// @inheritdoc IGhoVariableDebtToken function getBalanceFromInterest(address user) external view override returns (uint256) { return _ghoUserState[user].accumulatedDebtInterest; } /// @inheritdoc IGhoVariableDebtToken function decreaseBalanceFromInterest(address user, uint256 amount) external override onlyAToken { _ghoUserState[user].accumulatedDebtInterest = (_ghoUserState[user].accumulatedDebtInterest - amount).toUint128(); } /// @inheritdoc IGhoVariableDebtToken function rebalanceUserDiscountPercent(address user) external override { uint256 index = POOL.getReserveNormalizedVariableDebt(_underlyingAsset); uint256 previousScaledBalance = super.balanceOf(user); uint256 discountPercent = _ghoUserState[user].discountPercent; (uint256 balanceIncrease, uint256 discountScaled) = _accrueDebtOnAction( user, previousScaledBalance, discountPercent, index ); _burn(user, discountScaled.toUint128()); _refreshDiscountPercent( user, super.balanceOf(user).rayMul(index), _discountToken.balanceOf(user), discountPercent ); emit Transfer(address(0), user, balanceIncrease); emit Mint(address(0), user, balanceIncrease, balanceIncrease, index); } /** * @notice Implements the basic logic to mint a scaled balance token. * @param caller The address performing the mint * @param onBehalfOf The address of the user that will receive the scaled tokens * @param amount The amount of tokens getting minted * @param index The next liquidity index of the reserve * @return `true` if the the previous balance of the user was 0 */ function _mintScaled( address caller, address onBehalfOf, uint256 amount, uint256 index ) internal override returns (bool) { uint256 amountScaled = amount.rayDiv(index); require(amountScaled != 0, Errors.INVALID_MINT_AMOUNT); uint256 previousScaledBalance = super.balanceOf(onBehalfOf); uint256 discountPercent = _ghoUserState[onBehalfOf].discountPercent; (uint256 balanceIncrease, uint256 discountScaled) = _accrueDebtOnAction( onBehalfOf, previousScaledBalance, discountPercent, index ); // confirm the amount being borrowed is greater than the discount if (amountScaled > discountScaled) { _mint(onBehalfOf, (amountScaled - discountScaled).toUint128()); } else { _burn(onBehalfOf, (discountScaled - amountScaled).toUint128()); } _refreshDiscountPercent( onBehalfOf, super.balanceOf(onBehalfOf).rayMul(index), _discountToken.balanceOf(onBehalfOf), discountPercent ); uint256 amountToMint = amount + balanceIncrease; emit Transfer(address(0), onBehalfOf, amountToMint); emit Mint(caller, onBehalfOf, amountToMint, balanceIncrease, index); return true; } /** * @notice Implements the basic logic to burn a scaled balance token. * @dev In some instances, a burn transaction will emit a mint event * if the amount to burn is less than the interest that the user accrued * @param user The user which debt is burnt * @param target The address that will receive the underlying, if any * @param amount The amount getting burned * @param index The variable debt index of the reserve */ function _burnScaled( address user, address target, uint256 amount, uint256 index ) internal override { uint256 amountScaled = amount.rayDiv(index); require(amountScaled != 0, Errors.INVALID_BURN_AMOUNT); uint256 balanceBeforeBurn = balanceOf(user); uint256 previousScaledBalance = super.balanceOf(user); uint256 discountPercent = _ghoUserState[user].discountPercent; (uint256 balanceIncrease, uint256 discountScaled) = _accrueDebtOnAction( user, previousScaledBalance, discountPercent, index ); if (amount == balanceBeforeBurn) { _burn(user, previousScaledBalance.toUint128()); } else { _burn(user, (amountScaled + discountScaled).toUint128()); } _refreshDiscountPercent( user, super.balanceOf(user).rayMul(index), _discountToken.balanceOf(user), discountPercent ); if (balanceIncrease > amount) { uint256 amountToMint = balanceIncrease - amount; emit Transfer(address(0), user, amountToMint); emit Mint(user, user, amountToMint, balanceIncrease, index); } else { uint256 amountToBurn = amount - balanceIncrease; emit Transfer(user, address(0), amountToBurn); emit Burn(user, target, amountToBurn, balanceIncrease, index); } } /** * @dev Accumulates debt of the user since last action. * @dev It skips applying discount in case there is no balance increase or discount percent is zero. * @param user The address of the user * @param previousScaledBalance The previous scaled balance of the user * @param discountPercent The discount percent * @param index The variable debt index of the reserve * @return The increase in scaled balance since the last action of `user` * @return The discounted amount in scaled balance off the balance increase */ function _accrueDebtOnAction( address user, uint256 previousScaledBalance, uint256 discountPercent, uint256 index ) internal returns (uint256, uint256) { uint256 balanceIncrease = previousScaledBalance.rayMul(index) - previousScaledBalance.rayMul(_userState[user].additionalData); uint256 discountScaled = 0; if (balanceIncrease != 0 && discountPercent != 0) { uint256 discount = balanceIncrease.percentMul(discountPercent); discountScaled = discount.rayDiv(index); balanceIncrease = balanceIncrease - discount; } _userState[user].additionalData = index.toUint128(); _ghoUserState[user].accumulatedDebtInterest = (balanceIncrease + _ghoUserState[user].accumulatedDebtInterest).toUint128(); return (balanceIncrease, discountScaled); } /** * @dev Updates the discount percent of the user according to current discount rate strategy * @param user The address of the user * @param balance The debt balance of the user * @param discountTokenBalance The discount token balance of the user * @param previousDiscountPercent The previous discount percent of the user */ function _refreshDiscountPercent( address user, uint256 balance, uint256 discountTokenBalance, uint256 previousDiscountPercent ) internal { uint256 newDiscountPercent = _discountRateStrategy.calculateDiscountRate( balance, discountTokenBalance ); if (previousDiscountPercent != newDiscountPercent) { _ghoUserState[user].discountPercent = newDiscountPercent.toUint16(); emit DiscountPercentUpdated(user, previousDiscountPercent, newDiscountPercent); } } } ================================================ FILE: src/contracts/facilitators/aave/tokens/base/ScaledBalanceTokenBase.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {SafeCast} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/SafeCast.sol'; import {Errors} from '@aave/core-v3/contracts/protocol/libraries/helpers/Errors.sol'; import {WadRayMath} from '@aave/core-v3/contracts/protocol/libraries/math/WadRayMath.sol'; import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; import {IScaledBalanceToken} from '@aave/core-v3/contracts/interfaces/IScaledBalanceToken.sol'; import {MintableIncentivizedERC20} from '@aave/core-v3/contracts/protocol/tokenization/base/MintableIncentivizedERC20.sol'; /** * @title ScaledBalanceTokenBase * @author Aave * @notice Basic ERC20 implementation of scaled balance token */ abstract contract ScaledBalanceTokenBase is MintableIncentivizedERC20, IScaledBalanceToken { using WadRayMath for uint256; using SafeCast for uint256; /** * @dev Constructor. * @param pool The reference to the main Pool contract * @param name The name of the token * @param symbol The symbol of the token * @param decimals The number of decimals of the token */ constructor( IPool pool, string memory name, string memory symbol, uint8 decimals ) MintableIncentivizedERC20(pool, name, symbol, decimals) { // Intentionally left blank } /// @inheritdoc IScaledBalanceToken function scaledBalanceOf(address user) external view override returns (uint256) { return super.balanceOf(user); } /// @inheritdoc IScaledBalanceToken function getScaledUserBalanceAndSupply( address user ) external view override returns (uint256, uint256) { return (super.balanceOf(user), super.totalSupply()); } /// @inheritdoc IScaledBalanceToken function scaledTotalSupply() public view virtual override returns (uint256) { return super.totalSupply(); } /// @inheritdoc IScaledBalanceToken function getPreviousIndex(address user) external view virtual override returns (uint256) { return _userState[user].additionalData; } /** * @notice Implements the basic logic to mint a scaled balance token. * @param caller The address performing the mint * @param onBehalfOf The address of the user that will receive the scaled tokens * @param amount The amount of tokens getting minted * @param index The next liquidity index of the reserve * @return `true` if the the previous balance of the user was 0 */ function _mintScaled( address caller, address onBehalfOf, uint256 amount, uint256 index ) internal virtual returns (bool) { uint256 amountScaled = amount.rayDiv(index); require(amountScaled != 0, Errors.INVALID_MINT_AMOUNT); uint256 scaledBalance = super.balanceOf(onBehalfOf); uint256 balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].additionalData); _userState[onBehalfOf].additionalData = index.toUint128(); _mint(onBehalfOf, amountScaled.toUint128()); uint256 amountToMint = amount + balanceIncrease; emit Transfer(address(0), onBehalfOf, amountToMint); emit Mint(caller, onBehalfOf, amountToMint, balanceIncrease, index); return (scaledBalance == 0); } /** * @notice Implements the basic logic to burn a scaled balance token. * @dev In some instances, a burn transaction will emit a mint event * if the amount to burn is less than the interest that the user accrued * @param user The user which debt is burnt * @param target The address that will receive the underlying, if any * @param amount The amount getting burned * @param index The variable debt index of the reserve */ function _burnScaled( address user, address target, uint256 amount, uint256 index ) internal virtual { uint256 amountScaled = amount.rayDiv(index); require(amountScaled != 0, Errors.INVALID_BURN_AMOUNT); uint256 scaledBalance = super.balanceOf(user); uint256 balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[user].additionalData); _userState[user].additionalData = index.toUint128(); _burn(user, amountScaled.toUint128()); if (balanceIncrease > amount) { uint256 amountToMint = balanceIncrease - amount; emit Transfer(address(0), user, amountToMint); emit Mint(user, user, amountToMint, balanceIncrease, index); } else { uint256 amountToBurn = amount - balanceIncrease; emit Transfer(user, address(0), amountToBurn); emit Burn(user, target, amountToBurn, balanceIncrease, index); } } } ================================================ FILE: src/contracts/facilitators/aave/tokens/interfaces/IGhoAToken.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IAToken} from '@aave/core-v3/contracts/interfaces/IAToken.sol'; import {IGhoFacilitator} from '../../../../gho/interfaces/IGhoFacilitator.sol'; /** * @title IGhoAToken * @author Aave * @notice Defines the basic interface of the GhoAToken */ interface IGhoAToken is IAToken, IGhoFacilitator { /** * @dev Emitted when variable debt contract is set * @param variableDebtToken The address of the GhoVariableDebtToken contract */ event VariableDebtTokenSet(address indexed variableDebtToken); /** * @notice Sets a reference to the GHO variable debt token * @param ghoVariableDebtToken The address of the GhoVariableDebtToken contract */ function setVariableDebtToken(address ghoVariableDebtToken) external; /** * @notice Returns the address of the GHO variable debt token * @return The address of the GhoVariableDebtToken contract */ function getVariableDebtToken() external view returns (address); } ================================================ FILE: src/contracts/facilitators/aave/tokens/interfaces/IGhoVariableDebtToken.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IVariableDebtToken} from '@aave/core-v3/contracts/interfaces/IVariableDebtToken.sol'; /** * @title IGhoVariableDebtToken * @author Aave * @notice Defines the basic interface of the VariableDebtToken */ interface IGhoVariableDebtToken is IVariableDebtToken { /** * @dev Emitted when the address of the GHO AToken is set * @param aToken The address of the GhoAToken contract */ event ATokenSet(address indexed aToken); /** * @dev Emitted when the GhoDiscountRateStrategy is updated * @param oldDiscountRateStrategy The address of the old GhoDiscountRateStrategy * @param newDiscountRateStrategy The address of the new GhoDiscountRateStrategy */ event DiscountRateStrategyUpdated( address indexed oldDiscountRateStrategy, address indexed newDiscountRateStrategy ); /** * @dev Emitted when the Discount Token is updated * @param oldDiscountToken The address of the old discount token * @param newDiscountToken The address of the new discount token */ event DiscountTokenUpdated(address indexed oldDiscountToken, address indexed newDiscountToken); /** * @dev Emitted when a user's discount is updated * @param user The address of the user * @param oldDiscountPercent The old discount percent of the user * @param newDiscountPercent The new discount percent of the user */ event DiscountPercentUpdated( address indexed user, uint256 oldDiscountPercent, uint256 indexed newDiscountPercent ); /** * @notice Sets a reference to the GHO AToken * @param ghoAToken The address of the GhoAToken contract */ function setAToken(address ghoAToken) external; /** * @notice Returns the address of the GHO AToken * @return The address of the GhoAToken contract */ function getAToken() external view returns (address); /** * @notice Updates the Discount Rate Strategy * @param newDiscountRateStrategy The address of DiscountRateStrategy contract */ function updateDiscountRateStrategy(address newDiscountRateStrategy) external; /** * @notice Returns the address of the Discount Rate Strategy * @return The address of DiscountRateStrategy contract */ function getDiscountRateStrategy() external view returns (address); /** * @notice Updates the Discount Token * @param newDiscountToken The address of the DiscountToken contract */ function updateDiscountToken(address newDiscountToken) external; /** * @notice Returns the address of the Discount Token * @return address The address of DiscountToken */ function getDiscountToken() external view returns (address); /** * @notice Updates the discount percents of the users when a discount token transfer occurs * @dev To be executed before the token transfer happens * @param sender The address of sender * @param recipient The address of recipient * @param senderDiscountTokenBalance The sender discount token balance * @param recipientDiscountTokenBalance The recipient discount token balance * @param amount The amount of discount token being transferred */ function updateDiscountDistribution( address sender, address recipient, uint256 senderDiscountTokenBalance, uint256 recipientDiscountTokenBalance, uint256 amount ) external; /** * @notice Returns the discount percent being applied to the debt interest of the user * @param user The address of the user * @return The discount percent (expressed in bps) */ function getDiscountPercent(address user) external view returns (uint256); /* * @dev Returns the amount of interests accumulated by the user * @param user The address of the user * @return The amount of interests accumulated by the user */ function getBalanceFromInterest(address user) external view returns (uint256); /** * @dev Decrease the amount of interests accumulated by the user * @param user The address of the user * @param amount The value to be decrease */ function decreaseBalanceFromInterest(address user, uint256 amount) external; /** * @notice Rebalances the discount percent of a user * @param user The address of the user */ function rebalanceUserDiscountPercent(address user) external; } ================================================ FILE: src/contracts/facilitators/flashMinter/GhoFlashMinter.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IACLManager} from '@aave/core-v3/contracts/interfaces/IACLManager.sol'; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; import {PercentageMath} from '@aave/core-v3/contracts/protocol/libraries/math/PercentageMath.sol'; import {IERC3156FlashBorrower} from '@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol'; import {IERC3156FlashLender} from '@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol'; import {IGhoToken} from '../../gho/interfaces/IGhoToken.sol'; import {IGhoFacilitator} from '../../gho/interfaces/IGhoFacilitator.sol'; import {IGhoFlashMinter} from './interfaces/IGhoFlashMinter.sol'; /** * @title GhoFlashMinter * @author Aave * @notice Contract that enables FlashMinting of GHO. * @dev Based heavily on the EIP3156 reference implementation */ contract GhoFlashMinter is IGhoFlashMinter { using PercentageMath for uint256; // @inheritdoc IGhoFlashMinter bytes32 public constant CALLBACK_SUCCESS = keccak256('ERC3156FlashBorrower.onFlashLoan'); // @inheritdoc IGhoFlashMinter uint256 public constant MAX_FEE = 1e4; // @inheritdoc IGhoFlashMinter IPoolAddressesProvider public immutable override ADDRESSES_PROVIDER; // @inheritdoc IGhoFlashMinter IGhoToken public immutable GHO_TOKEN; // The Access Control List manager contract IACLManager private immutable ACL_MANAGER; // The flashmint fee, expressed in bps (a value of 10000 results in 100.00%) uint256 private _fee; // The GHO treasury, the recipient of fee distributions address private _ghoTreasury; /** * @dev Only pool admin can call functions marked by this modifier. */ modifier onlyPoolAdmin() { require(ACL_MANAGER.isPoolAdmin(msg.sender), 'CALLER_NOT_POOL_ADMIN'); _; } /** * @dev Constructor * @param ghoToken The address of the GHO token contract * @param ghoTreasury The address of the GHO treasury * @param fee The percentage of the flash-mint amount that needs to be repaid, on top of the principal (in bps) * @param addressesProvider The address of the Aave PoolAddressesProvider */ constructor(address ghoToken, address ghoTreasury, uint256 fee, address addressesProvider) { require(fee <= MAX_FEE, 'FlashMinter: Fee out of range'); GHO_TOKEN = IGhoToken(ghoToken); _updateGhoTreasury(ghoTreasury); _updateFee(fee); ADDRESSES_PROVIDER = IPoolAddressesProvider(addressesProvider); ACL_MANAGER = IACLManager(IPoolAddressesProvider(addressesProvider).getACLManager()); } /// @inheritdoc IERC3156FlashLender function flashLoan( IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data ) external override returns (bool) { require(token == address(GHO_TOKEN), 'FlashMinter: Unsupported currency'); uint256 fee = ACL_MANAGER.isFlashBorrower(msg.sender) ? 0 : _flashFee(amount); GHO_TOKEN.mint(address(receiver), amount); require( receiver.onFlashLoan(msg.sender, address(GHO_TOKEN), amount, fee, data) == CALLBACK_SUCCESS, 'FlashMinter: Callback failed' ); GHO_TOKEN.transferFrom(address(receiver), address(this), amount + fee); GHO_TOKEN.burn(amount); emit FlashMint(address(receiver), msg.sender, address(GHO_TOKEN), amount, fee); return true; } /// @inheritdoc IGhoFacilitator function distributeFeesToTreasury() external override { uint256 balance = GHO_TOKEN.balanceOf(address(this)); GHO_TOKEN.transfer(_ghoTreasury, balance); emit FeesDistributedToTreasury(_ghoTreasury, address(GHO_TOKEN), balance); } // @inheritdoc IGhoFlashMinter function updateFee(uint256 newFee) external override onlyPoolAdmin { _updateFee(newFee); } /// @inheritdoc IGhoFacilitator function updateGhoTreasury(address newGhoTreasury) external override onlyPoolAdmin { _updateGhoTreasury(newGhoTreasury); } /// @inheritdoc IERC3156FlashLender function maxFlashLoan(address token) external view override returns (uint256) { if (token != address(GHO_TOKEN)) { return 0; } else { (uint256 capacity, uint256 level) = GHO_TOKEN.getFacilitatorBucket(address(this)); return capacity > level ? capacity - level : 0; } } /// @inheritdoc IERC3156FlashLender function flashFee(address token, uint256 amount) external view override returns (uint256) { require(token == address(GHO_TOKEN), 'FlashMinter: Unsupported currency'); return ACL_MANAGER.isFlashBorrower(msg.sender) ? 0 : _flashFee(amount); } /// @inheritdoc IGhoFlashMinter function getFee() external view override returns (uint256) { return _fee; } /// @inheritdoc IGhoFacilitator function getGhoTreasury() external view override returns (address) { return _ghoTreasury; } /** * @notice Returns the fee to charge for a given flashloan. * @dev Internal function with no checks. * @param amount The amount of tokens to be borrowed. * @return The amount of `token` to be charged for the flashloan, on top of the returned principal. */ function _flashFee(uint256 amount) internal view returns (uint256) { return amount.percentMul(_fee); } function _updateFee(uint256 newFee) internal { require(newFee <= MAX_FEE, 'FlashMinter: Fee out of range'); uint256 oldFee = _fee; _fee = newFee; emit FeeUpdated(oldFee, newFee); } function _updateGhoTreasury(address newGhoTreasury) internal { address oldGhoTreasury = _ghoTreasury; _ghoTreasury = newGhoTreasury; emit GhoTreasuryUpdated(oldGhoTreasury, newGhoTreasury); } } ================================================ FILE: src/contracts/facilitators/flashMinter/interfaces/IGhoFlashMinter.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IERC3156FlashLender} from '@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol'; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; import {IGhoFacilitator} from '../../../gho/interfaces/IGhoFacilitator.sol'; import {IGhoToken} from '../../../gho/interfaces/IGhoToken.sol'; /** * @title IGhoFlashMinter * @author Aave * @notice Defines the behavior of the GHO Flash Minter */ interface IGhoFlashMinter is IERC3156FlashLender, IGhoFacilitator { /** * @dev Emitted when the percentage fee is updated * @param oldFee The old fee (in bps) * @param newFee The new fee (in bps) */ event FeeUpdated(uint256 oldFee, uint256 newFee); /** * @dev Emitted when a FlashMint occurs * @param receiver The receiver of the FlashMinted tokens (it is also the receiver of the callback) * @param initiator The address initiating the FlashMint * @param asset The asset being FlashMinted. Always GHO. * @param amount The principal being FlashMinted * @param fee The fee returned on top of the principal */ event FlashMint( address indexed receiver, address indexed initiator, address asset, uint256 indexed amount, uint256 fee ); /** * @notice Returns the required return value for a successful flashmint * @return The required callback, the keccak256 hash of 'ERC3156FlashBorrower.onFlashLoan' */ function CALLBACK_SUCCESS() external view returns (bytes32); /** * @notice Returns the maximum value the fee can be set to * @return The maximum percentage fee of the flash-minted amount that the flashFee can be set to (in bps). */ function MAX_FEE() external view returns (uint256); /** * @notice Returns the address of the Aave Pool Addresses Provider contract * @return The address of the PoolAddressesProvider */ function ADDRESSES_PROVIDER() external view returns (IPoolAddressesProvider); /** * @notice Returns the address of the GHO token contract * @return The address of the GhoToken */ function GHO_TOKEN() external view returns (IGhoToken); /** * @notice Updates the percentage fee. It is the percentage of the flash-minted amount that needs to be repaid. * @dev The fee is expressed in bps. A value of 100, results in 1.00% * @param newFee The new percentage fee (in bps) */ function updateFee(uint256 newFee) external; /** * @notice Returns the percentage of each flash mint taken as a fee * @return The percentage fee of the flash-minted amount that needs to be repaid, on top of the principal (in bps). */ function getFee() external view returns (uint256); } ================================================ FILE: src/contracts/facilitators/gsm/Gsm.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {VersionedInitializable} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol'; import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; import {GPv2SafeERC20} from '@aave/core-v3/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol'; import {EIP712} from '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; import {SignatureChecker} from '@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol'; import {SafeCast} from '@openzeppelin/contracts/utils/math/SafeCast.sol'; import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol'; import {IGhoFacilitator} from '../../gho/interfaces/IGhoFacilitator.sol'; import {IGhoToken} from '../../gho/interfaces/IGhoToken.sol'; import {IGsmPriceStrategy} from './priceStrategy/interfaces/IGsmPriceStrategy.sol'; import {IGsmFeeStrategy} from './feeStrategy/interfaces/IGsmFeeStrategy.sol'; import {IGsm} from './interfaces/IGsm.sol'; /** * @title Gsm * @author Aave * @notice GHO Stability Module. It provides buy/sell facilities to go to/from an underlying asset to/from GHO. * @dev To be covered by a proxy contract. */ contract Gsm is AccessControl, VersionedInitializable, EIP712, IGsm { using GPv2SafeERC20 for IERC20; using SafeCast for uint256; /// @inheritdoc IGsm bytes32 public constant CONFIGURATOR_ROLE = keccak256('CONFIGURATOR_ROLE'); /// @inheritdoc IGsm bytes32 public constant TOKEN_RESCUER_ROLE = keccak256('TOKEN_RESCUER_ROLE'); /// @inheritdoc IGsm bytes32 public constant SWAP_FREEZER_ROLE = keccak256('SWAP_FREEZER_ROLE'); /// @inheritdoc IGsm bytes32 public constant LIQUIDATOR_ROLE = keccak256('LIQUIDATOR_ROLE'); /// @inheritdoc IGsm bytes32 public constant BUY_ASSET_WITH_SIG_TYPEHASH = keccak256( 'BuyAssetWithSig(address originator,uint256 minAmount,address receiver,uint256 nonce,uint256 deadline)' ); /// @inheritdoc IGsm bytes32 public constant SELL_ASSET_WITH_SIG_TYPEHASH = keccak256( 'SellAssetWithSig(address originator,uint256 maxAmount,address receiver,uint256 nonce,uint256 deadline)' ); /// @inheritdoc IGsm address public immutable GHO_TOKEN; /// @inheritdoc IGsm address public immutable UNDERLYING_ASSET; /// @inheritdoc IGsm address public immutable PRICE_STRATEGY; /// @inheritdoc IGsm mapping(address => uint256) public nonces; address internal _ghoTreasury; address internal _feeStrategy; bool internal _isFrozen; bool internal _isSeized; uint128 internal _exposureCap; uint128 internal _currentExposure; uint128 internal _accruedFees; /** * @dev Require GSM to not be frozen for functions marked by this modifier */ modifier notFrozen() { require(!_isFrozen, 'GSM_FROZEN'); _; } /** * @dev Require GSM to not be seized for functions marked by this modifier */ modifier notSeized() { require(!_isSeized, 'GSM_SEIZED'); _; } /** * @dev Constructor * @param ghoToken The address of the GHO token contract * @param underlyingAsset The address of the collateral asset * @param priceStrategy The address of the price strategy */ constructor(address ghoToken, address underlyingAsset, address priceStrategy) EIP712('GSM', '1') { require(ghoToken != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(underlyingAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); require( IGsmPriceStrategy(priceStrategy).UNDERLYING_ASSET() == underlyingAsset, 'INVALID_PRICE_STRATEGY' ); GHO_TOKEN = ghoToken; UNDERLYING_ASSET = underlyingAsset; PRICE_STRATEGY = priceStrategy; } /** * @notice GSM initializer * @param admin The address of the default admin role * @param ghoTreasury The address of the GHO treasury * @param exposureCap Maximum amount of user-supplied underlying asset in GSM */ function initialize( address admin, address ghoTreasury, uint128 exposureCap ) external initializer { require(admin != address(0), 'ZERO_ADDRESS_NOT_VALID'); _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(CONFIGURATOR_ROLE, admin); _updateGhoTreasury(ghoTreasury); _updateExposureCap(exposureCap); } /// @inheritdoc IGsm function buyAsset( uint256 minAmount, address receiver ) external notFrozen notSeized returns (uint256, uint256) { return _buyAsset(msg.sender, minAmount, receiver); } /// @inheritdoc IGsm function buyAssetWithSig( address originator, uint256 minAmount, address receiver, uint256 deadline, bytes calldata signature ) external notFrozen notSeized returns (uint256, uint256) { require(deadline >= block.timestamp, 'SIGNATURE_DEADLINE_EXPIRED'); bytes32 digest = keccak256( abi.encode( '\x19\x01', _domainSeparatorV4(), BUY_ASSET_WITH_SIG_TYPEHASH, abi.encode(originator, minAmount, receiver, nonces[originator]++, deadline) ) ); require( SignatureChecker.isValidSignatureNow(originator, digest, signature), 'SIGNATURE_INVALID' ); return _buyAsset(originator, minAmount, receiver); } /// @inheritdoc IGsm function sellAsset( uint256 maxAmount, address receiver ) external notFrozen notSeized returns (uint256, uint256) { return _sellAsset(msg.sender, maxAmount, receiver); } /// @inheritdoc IGsm function sellAssetWithSig( address originator, uint256 maxAmount, address receiver, uint256 deadline, bytes calldata signature ) external notFrozen notSeized returns (uint256, uint256) { require(deadline >= block.timestamp, 'SIGNATURE_DEADLINE_EXPIRED'); bytes32 digest = keccak256( abi.encode( '\x19\x01', _domainSeparatorV4(), SELL_ASSET_WITH_SIG_TYPEHASH, abi.encode(originator, maxAmount, receiver, nonces[originator]++, deadline) ) ); require( SignatureChecker.isValidSignatureNow(originator, digest, signature), 'SIGNATURE_INVALID' ); return _sellAsset(originator, maxAmount, receiver); } /// @inheritdoc IGsm function rescueTokens( address token, address to, uint256 amount ) external onlyRole(TOKEN_RESCUER_ROLE) { require(amount > 0, 'INVALID_AMOUNT'); if (token == GHO_TOKEN) { uint256 rescuableBalance = IERC20(token).balanceOf(address(this)) - _accruedFees; require(rescuableBalance >= amount, 'INSUFFICIENT_GHO_TO_RESCUE'); } if (token == UNDERLYING_ASSET) { uint256 rescuableBalance = IERC20(token).balanceOf(address(this)) - _currentExposure; require(rescuableBalance >= amount, 'INSUFFICIENT_EXOGENOUS_ASSET_TO_RESCUE'); } IERC20(token).safeTransfer(to, amount); emit TokensRescued(token, to, amount); } /// @inheritdoc IGsm function setSwapFreeze(bool enable) external onlyRole(SWAP_FREEZER_ROLE) { if (enable) { require(!_isFrozen, 'GSM_ALREADY_FROZEN'); } else { require(_isFrozen, 'GSM_ALREADY_UNFROZEN'); } _isFrozen = enable; emit SwapFreeze(msg.sender, enable); } /// @inheritdoc IGsm function seize() external notSeized onlyRole(LIQUIDATOR_ROLE) returns (uint256) { _isSeized = true; _currentExposure = 0; _updateExposureCap(0); (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); uint256 underlyingBalance = IERC20(UNDERLYING_ASSET).balanceOf(address(this)); if (underlyingBalance > 0) { IERC20(UNDERLYING_ASSET).safeTransfer(_ghoTreasury, underlyingBalance); } emit Seized(msg.sender, _ghoTreasury, underlyingBalance, ghoMinted); return underlyingBalance; } /// @inheritdoc IGsm function burnAfterSeize(uint256 amount) external onlyRole(LIQUIDATOR_ROLE) returns (uint256) { require(_isSeized, 'GSM_NOT_SEIZED'); require(amount > 0, 'INVALID_AMOUNT'); (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); if (amount > ghoMinted) { amount = ghoMinted; } IGhoToken(GHO_TOKEN).transferFrom(msg.sender, address(this), amount); IGhoToken(GHO_TOKEN).burn(amount); emit BurnAfterSeize(msg.sender, amount, (ghoMinted - amount)); return amount; } /// @inheritdoc IGsm function updateFeeStrategy(address feeStrategy) external onlyRole(CONFIGURATOR_ROLE) { _updateFeeStrategy(feeStrategy); } /// @inheritdoc IGsm function updateExposureCap(uint128 exposureCap) external onlyRole(CONFIGURATOR_ROLE) { _updateExposureCap(exposureCap); } /// @inheritdoc IGhoFacilitator function distributeFeesToTreasury() public virtual override { uint256 accruedFees = _accruedFees; if (accruedFees > 0) { _accruedFees = 0; IERC20(GHO_TOKEN).transfer(_ghoTreasury, accruedFees); emit FeesDistributedToTreasury(_ghoTreasury, GHO_TOKEN, accruedFees); } } /// @inheritdoc IGhoFacilitator function updateGhoTreasury(address newGhoTreasury) external override onlyRole(CONFIGURATOR_ROLE) { _updateGhoTreasury(newGhoTreasury); } /// @inheritdoc IGsm function DOMAIN_SEPARATOR() external view returns (bytes32) { return _domainSeparatorV4(); } /// @inheritdoc IGsm function getGhoAmountForBuyAsset( uint256 minAssetAmount ) external view returns (uint256, uint256, uint256, uint256) { return _calculateGhoAmountForBuyAsset(minAssetAmount); } /// @inheritdoc IGsm function getGhoAmountForSellAsset( uint256 maxAssetAmount ) external view returns (uint256, uint256, uint256, uint256) { return _calculateGhoAmountForSellAsset(maxAssetAmount); } /// @inheritdoc IGsm function getAssetAmountForBuyAsset( uint256 maxGhoAmount ) external view returns (uint256, uint256, uint256, uint256) { bool withFee = _feeStrategy != address(0); uint256 grossAmount = withFee ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalBought(maxGhoAmount) : maxGhoAmount; // round down so maxGhoAmount is guaranteed uint256 assetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset(grossAmount, false); uint256 finalGrossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho( assetAmount, true ); uint256 finalFee = withFee ? IGsmFeeStrategy(_feeStrategy).getBuyFee(finalGrossAmount) : 0; return (assetAmount, finalGrossAmount + finalFee, finalGrossAmount, finalFee); } /// @inheritdoc IGsm function getAssetAmountForSellAsset( uint256 minGhoAmount ) external view returns (uint256, uint256, uint256, uint256) { bool withFee = _feeStrategy != address(0); uint256 grossAmount = withFee ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalSold(minGhoAmount) : minGhoAmount; // round up so minGhoAmount is guaranteed uint256 assetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset(grossAmount, true); uint256 finalGrossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho( assetAmount, false ); uint256 finalFee = withFee ? IGsmFeeStrategy(_feeStrategy).getSellFee(finalGrossAmount) : 0; return (assetAmount, finalGrossAmount - finalFee, finalGrossAmount, finalFee); } /// @inheritdoc IGsm function getAvailableUnderlyingExposure() external view returns (uint256) { return _exposureCap > _currentExposure ? _exposureCap - _currentExposure : 0; } /// @inheritdoc IGsm function getExposureCap() external view returns (uint128) { return _exposureCap; } /// @inheritdoc IGsm function getAvailableLiquidity() external view returns (uint256) { return _currentExposure; } /// @inheritdoc IGsm function getFeeStrategy() external view returns (address) { return _feeStrategy; } /// @inheritdoc IGsm function getAccruedFees() external view returns (uint256) { return _accruedFees; } /// @inheritdoc IGsm function getIsFrozen() external view returns (bool) { return _isFrozen; } /// @inheritdoc IGsm function getIsSeized() external view returns (bool) { return _isSeized; } /// @inheritdoc IGsm function canSwap() external view returns (bool) { return !_isFrozen && !_isSeized; } /// @inheritdoc IGhoFacilitator function getGhoTreasury() external view override returns (address) { return _ghoTreasury; } /// @inheritdoc IGsm function GSM_REVISION() public pure virtual override returns (uint256) { return 1; } /** * @dev Buys an underlying asset with GHO * @param originator The originator of the request * @param minAmount The minimum amount of the underlying asset desired for purchase * @param receiver The recipient address of the underlying asset being purchased * @return The amount of underlying asset bought * @return The amount of GHO sold by the user */ function _buyAsset( address originator, uint256 minAmount, address receiver ) internal returns (uint256, uint256) { ( uint256 assetAmount, uint256 ghoSold, uint256 grossAmount, uint256 fee ) = _calculateGhoAmountForBuyAsset(minAmount); _beforeBuyAsset(originator, assetAmount, receiver); require(assetAmount > 0, 'INVALID_AMOUNT'); require(_currentExposure >= assetAmount, 'INSUFFICIENT_AVAILABLE_EXOGENOUS_ASSET_LIQUIDITY'); _currentExposure -= uint128(assetAmount); _accruedFees += fee.toUint128(); IGhoToken(GHO_TOKEN).transferFrom(originator, address(this), ghoSold); IGhoToken(GHO_TOKEN).burn(grossAmount); IERC20(UNDERLYING_ASSET).safeTransfer(receiver, assetAmount); emit BuyAsset(originator, receiver, assetAmount, ghoSold, fee); return (assetAmount, ghoSold); } /** * @dev Hook that is called before `buyAsset`. * @dev This can be used to add custom logic * @param originator Originator of the request * @param amount The amount of the underlying asset desired for purchase * @param receiver Recipient address of the underlying asset being purchased */ function _beforeBuyAsset(address originator, uint256 amount, address receiver) internal virtual {} /** * @dev Sells an underlying asset for GHO * @param originator The originator of the request * @param maxAmount The maximum amount of the underlying asset desired to sell * @param receiver The recipient address of the GHO being purchased * @return The amount of underlying asset sold * @return The amount of GHO bought by the user */ function _sellAsset( address originator, uint256 maxAmount, address receiver ) internal returns (uint256, uint256) { ( uint256 assetAmount, uint256 ghoBought, uint256 grossAmount, uint256 fee ) = _calculateGhoAmountForSellAsset(maxAmount); _beforeSellAsset(originator, assetAmount, receiver); require(assetAmount > 0, 'INVALID_AMOUNT'); require(_currentExposure + assetAmount <= _exposureCap, 'EXOGENOUS_ASSET_EXPOSURE_TOO_HIGH'); _currentExposure += uint128(assetAmount); _accruedFees += fee.toUint128(); IERC20(UNDERLYING_ASSET).safeTransferFrom(originator, address(this), assetAmount); IGhoToken(GHO_TOKEN).mint(address(this), grossAmount); IGhoToken(GHO_TOKEN).transfer(receiver, ghoBought); emit SellAsset(originator, receiver, assetAmount, grossAmount, fee); return (assetAmount, ghoBought); } /** * @dev Hook that is called before `sellAsset`. * @dev This can be used to add custom logic * @param originator Originator of the request * @param amount The amount of the underlying asset desired to sell * @param receiver Recipient address of the GHO being purchased */ function _beforeSellAsset( address originator, uint256 amount, address receiver ) internal virtual {} /** * @dev Returns the amount of GHO sold in exchange of buying underlying asset * @param assetAmount The amount of underlying asset to buy * @return The exact amount of asset the user purchases * @return The total amount of GHO the user sells (gross amount in GHO plus fee) * @return The gross amount of GHO * @return The fee amount in GHO, applied on top of gross amount of GHO */ function _calculateGhoAmountForBuyAsset( uint256 assetAmount ) internal view returns (uint256, uint256, uint256, uint256) { bool withFee = _feeStrategy != address(0); // pick the highest GHO amount possible for given asset amount uint256 grossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho(assetAmount, true); uint256 fee = withFee ? IGsmFeeStrategy(_feeStrategy).getBuyFee(grossAmount) : 0; uint256 ghoSold = grossAmount + fee; uint256 finalGrossAmount = withFee ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalBought(ghoSold) : ghoSold; // pick the lowest asset amount possible for given GHO amount uint256 finalAssetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset( finalGrossAmount, false ); uint256 finalFee = ghoSold - finalGrossAmount; return (finalAssetAmount, finalGrossAmount + finalFee, finalGrossAmount, finalFee); } /** * @dev Returns the amount of GHO bought in exchange of a given amount of underlying asset * @param assetAmount The amount of underlying asset to sell * @return The exact amount of asset the user sells * @return The total amount of GHO the user buys (gross amount in GHO minus fee) * @return The gross amount of GHO * @return The fee amount in GHO, applied to the gross amount of GHO */ function _calculateGhoAmountForSellAsset( uint256 assetAmount ) internal view returns (uint256, uint256, uint256, uint256) { bool withFee = _feeStrategy != address(0); // pick the lowest GHO amount possible for given asset amount uint256 grossAmount = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho(assetAmount, false); uint256 fee = withFee ? IGsmFeeStrategy(_feeStrategy).getSellFee(grossAmount) : 0; uint256 ghoBought = grossAmount - fee; uint256 finalGrossAmount = withFee ? IGsmFeeStrategy(_feeStrategy).getGrossAmountFromTotalSold(ghoBought) : ghoBought; // pick the highest asset amount possible for given GHO amount uint256 finalAssetAmount = IGsmPriceStrategy(PRICE_STRATEGY).getGhoPriceInAsset( finalGrossAmount, true ); uint256 finalFee = finalGrossAmount - ghoBought; return (finalAssetAmount, finalGrossAmount - finalFee, finalGrossAmount, finalFee); } /** * @dev Updates Fee Strategy * @param feeStrategy The address of the new Fee Strategy */ function _updateFeeStrategy(address feeStrategy) internal { address oldFeeStrategy = _feeStrategy; _feeStrategy = feeStrategy; emit FeeStrategyUpdated(oldFeeStrategy, feeStrategy); } /** * @dev Updates Exposure Cap * @param exposureCap The value of the new Exposure Cap */ function _updateExposureCap(uint128 exposureCap) internal { uint128 oldExposureCap = _exposureCap; _exposureCap = exposureCap; emit ExposureCapUpdated(oldExposureCap, exposureCap); } /** * @dev Updates GHO Treasury Address * @param newGhoTreasury The address of the new GHO Treasury */ function _updateGhoTreasury(address newGhoTreasury) internal { require(newGhoTreasury != address(0), 'ZERO_ADDRESS_NOT_VALID'); address oldGhoTreasury = _ghoTreasury; _ghoTreasury = newGhoTreasury; emit GhoTreasuryUpdated(oldGhoTreasury, newGhoTreasury); } /// @inheritdoc VersionedInitializable function getRevision() internal pure virtual override returns (uint256) { return GSM_REVISION(); } } ================================================ FILE: src/contracts/facilitators/gsm/Gsm4626.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; import {GPv2SafeERC20} from '@aave/core-v3/contracts/dependencies/gnosis/contracts/GPv2SafeERC20.sol'; import {SafeCast} from '@openzeppelin/contracts/utils/math/SafeCast.sol'; import {IGhoFacilitator} from '../../gho/interfaces/IGhoFacilitator.sol'; import {IGhoToken} from '../../gho/interfaces/IGhoToken.sol'; import {IGsmPriceStrategy} from './priceStrategy/interfaces/IGsmPriceStrategy.sol'; import {IGsm4626} from './interfaces/IGsm4626.sol'; import {Gsm} from './Gsm.sol'; /** * @title Gsm4626 * @author Aave * @notice GHO Stability Module for ERC4626 vault shares. It provides buy/sell facilities to go to/from an ERC4626 * vault share to/from GHO. * @dev Aimed to be used with ERC4626 vault shares as underlying asset. Users can use the ERC4626 vault share to * buy/sell GHO and the generated yield is redirected to the GHO Treasury in form of GHO. * @dev To be covered by a proxy contract. */ contract Gsm4626 is Gsm, IGsm4626 { using GPv2SafeERC20 for IERC20; using SafeCast for uint256; /** * @dev Constructor * @param ghoToken The address of the GHO token contract * @param underlyingAsset The address of the ERC4626 vault * @param priceStrategy The address of the price strategy */ constructor( address ghoToken, address underlyingAsset, address priceStrategy ) Gsm(ghoToken, underlyingAsset, priceStrategy) { // Intentionally left blank } /// @inheritdoc IGsm4626 function backWithGho( uint256 amount ) external notSeized onlyRole(CONFIGURATOR_ROLE) returns (uint256) { require(amount > 0, 'INVALID_AMOUNT'); (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); (, uint256 deficit) = _getCurrentBacking(ghoMinted); require(deficit > 0, 'NO_CURRENT_DEFICIT_BACKING'); uint256 ghoToBack = amount > deficit ? deficit : amount; IGhoToken(GHO_TOKEN).transferFrom(msg.sender, address(this), ghoToBack); IGhoToken(GHO_TOKEN).burn(ghoToBack); emit BackingProvided(msg.sender, GHO_TOKEN, ghoToBack, ghoToBack, deficit - ghoToBack); return ghoToBack; } /// @inheritdoc IGsm4626 function backWithUnderlying( uint256 amount ) external notSeized onlyRole(CONFIGURATOR_ROLE) returns (uint256) { require(amount > 0, 'INVALID_AMOUNT'); (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); (, uint256 deficit) = _getCurrentBacking(ghoMinted); require(deficit > 0, 'NO_CURRENT_DEFICIT_BACKING'); uint128 deficitInUnderlying = IGsmPriceStrategy(PRICE_STRATEGY) .getGhoPriceInAsset(deficit, false) .toUint128(); if (amount >= deficitInUnderlying) { _currentExposure += deficitInUnderlying; IERC20(UNDERLYING_ASSET).safeTransferFrom(msg.sender, address(this), deficitInUnderlying); emit BackingProvided(msg.sender, UNDERLYING_ASSET, deficitInUnderlying, deficit, 0); return deficitInUnderlying; } else { uint256 amountInGho = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho(amount, false); _currentExposure += uint128(amount); IERC20(UNDERLYING_ASSET).safeTransferFrom(msg.sender, address(this), amount); emit BackingProvided( msg.sender, UNDERLYING_ASSET, amount, amountInGho, deficit - amountInGho ); return amount; } } /// @inheritdoc IGsm4626 function getCurrentBacking() external view returns (uint256, uint256) { (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); return _getCurrentBacking(ghoMinted); } /// @inheritdoc IGhoFacilitator function distributeFeesToTreasury() public override(Gsm, IGhoFacilitator) { _cumulateYieldInGho(); super.distributeFeesToTreasury(); } /// @inheritdoc Gsm function _beforeBuyAsset(address, uint256, address) internal override { _cumulateYieldInGho(); } /// @inheritdoc Gsm function _beforeSellAsset(address, uint256, address) internal override {} /** * @dev Cumulates yield in form of GHO, aimed to be redirected to the treasury * @dev It mints GHO backed by the excess of underlying produced by the ERC4626 yield * @dev If the GHO amount exceeds the amount available, it will mint up to the remaining capacity */ function _cumulateYieldInGho() internal { (uint256 ghoCapacity, uint256 ghoLevel) = IGhoToken(GHO_TOKEN).getFacilitatorBucket( address(this) ); uint256 ghoAvailableToMint = ghoCapacity > ghoLevel ? ghoCapacity - ghoLevel : 0; (uint256 ghoExcess, ) = _getCurrentBacking(ghoLevel); if (ghoExcess > 0 && ghoAvailableToMint > 0) { ghoExcess = ghoExcess > ghoAvailableToMint ? ghoAvailableToMint : ghoExcess; _accruedFees += uint128(ghoExcess); IGhoToken(GHO_TOKEN).mint(address(this), ghoExcess); } } /** * @dev Calculates the excess or deficit of GHO minted, reflective of GSM backing * @param ghoMinted The amount of GHO currently minted by the GSM * @return The excess amount of GHO minted, relative to the value of the underlying * @return The deficit of GHO minted, relative to the value of the underlying */ function _getCurrentBacking(uint256 ghoMinted) internal view returns (uint256, uint256) { uint256 ghoToBack = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho( _currentExposure, false ); if (ghoToBack >= ghoMinted) { return (ghoToBack - ghoMinted, 0); } else { return (0, ghoMinted - ghoToBack); } } } ================================================ FILE: src/contracts/facilitators/gsm/dependencies/chainlink/AutomationCompatibleInterface.sol ================================================ // SPDX-License-Identifier: MIT // Chainlink Contracts v0.8 pragma solidity ^0.8.0; interface AutomationCompatibleInterface { /** * @notice method that is simulated by the keepers to see if any work actually * needs to be performed. This method does does not actually need to be * executable, and since it is only ever simulated it can consume lots of gas. * @dev To ensure that it is never called, you may want to add the * cannotExecute modifier from KeeperBase to your implementation of this * method. * @param checkData specified in the upkeep registration so it is always the * same for a registered upkeep. This can easily be broken down into specific * arguments using `abi.decode`, so multiple upkeeps can be registered on the * same contract and easily differentiated by the contract. * @return upkeepNeeded boolean to indicate whether the keeper should call * performUpkeep or not. * @return performData bytes that the keeper should call performUpkeep with, if * upkeep is needed. If you would like to encode data to decode later, try * `abi.encode`. */ function checkUpkeep( bytes calldata checkData ) external returns (bool upkeepNeeded, bytes memory performData); /** * @notice method that is actually executed by the keepers, via the registry. * The data returned by the checkUpkeep simulation will be passed into * this method to actually be executed. * @dev The input to this method should not be trusted, and the caller of the * method should not even be restricted to any single registry. Anyone should * be able call it, and the input should be validated, there is no guarantee * that the data passed in is the performData returned from checkUpkeep. This * could happen due to malicious keepers, racing keepers, or simply a state * change while the performUpkeep transaction is waiting for confirmation. * Always validate the data passed in. * @param performData is the data which was passed back from the checkData * simulation. If it is encoded, it can easily be decoded into other types by * calling `abi.decode`. This data should not be trusted, and should be * validated against the contract's current state. */ function performUpkeep(bytes calldata performData) external; } ================================================ FILE: src/contracts/facilitators/gsm/feeStrategy/FixedFeeStrategy.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; import {PercentageMath} from '@aave/core-v3/contracts/protocol/libraries/math/PercentageMath.sol'; import {IGsmFeeStrategy} from './interfaces/IGsmFeeStrategy.sol'; /** * @title FixedFeeStrategy * @author Aave * @notice Fee strategy using a fixed rate to calculate buy/sell fees */ contract FixedFeeStrategy is IGsmFeeStrategy { using Math for uint256; uint256 internal constant MAXIMUM_FEE_PERCENT = 5000; uint256 internal immutable _buyFee; uint256 internal immutable _sellFee; /** * @dev Constructor * @dev Fees must be lower than 5000 bps (e.g. 50.00%) * @param buyFee The fee paid when buying the underlying asset in exchange for GHO, expressed in bps * @param sellFee The fee paid when selling the underlying asset in exchange for GHO, expressed in bps */ constructor(uint256 buyFee, uint256 sellFee) { require(buyFee < MAXIMUM_FEE_PERCENT, 'INVALID_BUY_FEE'); require(sellFee < MAXIMUM_FEE_PERCENT, 'INVALID_SELL_FEE'); require(buyFee > 0 || sellFee > 0, 'MUST_HAVE_ONE_NONZERO_FEE'); _buyFee = buyFee; _sellFee = sellFee; } /// @inheritdoc IGsmFeeStrategy function getBuyFee(uint256 grossAmount) external view returns (uint256) { return grossAmount.mulDiv(_buyFee, PercentageMath.PERCENTAGE_FACTOR, Math.Rounding.Up); } /// @inheritdoc IGsmFeeStrategy function getSellFee(uint256 grossAmount) external view returns (uint256) { return grossAmount.mulDiv(_sellFee, PercentageMath.PERCENTAGE_FACTOR, Math.Rounding.Up); } /// @inheritdoc IGsmFeeStrategy function getGrossAmountFromTotalBought(uint256 totalAmount) external view returns (uint256) { if (totalAmount == 0) { return 0; } else if (_buyFee == 0) { return totalAmount; } else { return totalAmount.mulDiv( PercentageMath.PERCENTAGE_FACTOR, PercentageMath.PERCENTAGE_FACTOR + _buyFee, Math.Rounding.Down ); } } /// @inheritdoc IGsmFeeStrategy function getGrossAmountFromTotalSold(uint256 totalAmount) external view returns (uint256) { if (totalAmount == 0) { return 0; } else if (_sellFee == 0) { return totalAmount; } else { return totalAmount.mulDiv( PercentageMath.PERCENTAGE_FACTOR, PercentageMath.PERCENTAGE_FACTOR - _sellFee, Math.Rounding.Up ); } } } ================================================ FILE: src/contracts/facilitators/gsm/feeStrategy/FixedFeeStrategyFactory.sol ================================================ /// SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; import {VersionedInitializable} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol'; import {IFixedFeeStrategyFactory} from './interfaces/IFixedFeeStrategyFactory.sol'; import {IGsmFeeStrategy} from './interfaces/IGsmFeeStrategy.sol'; import {FixedFeeStrategy} from './FixedFeeStrategy.sol'; /** * @title FixedFeeStrategyFactory * @author Aave Labs * @notice Factory contract to create and keep record of Gsm FixedFeeStrategy contracts */ contract FixedFeeStrategyFactory is VersionedInitializable, IFixedFeeStrategyFactory { using EnumerableSet for EnumerableSet.AddressSet; // Mapping of fee strategy contracts by buy and sell fees (buyFee => sellFee => feeStrategy) mapping(uint256 => mapping(uint256 => address)) internal _gsmFeeStrategiesByFees; EnumerableSet.AddressSet internal _gsmFeeStrategies; /** * @dev Initializer * @param feeStrategiesList List of fee strategies * @dev Assumes that the addresses provided are deployed FixedFeeStrategy contracts */ function initialize(address[] memory feeStrategiesList) external initializer { for (uint256 i = 0; i < feeStrategiesList.length; i++) { address feeStrategy = feeStrategiesList[i]; uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); _gsmFeeStrategiesByFees[buyFee][sellFee] = feeStrategy; _gsmFeeStrategies.add(feeStrategy); emit FeeStrategyCreated(feeStrategy, buyFee, sellFee); } } ///@inheritdoc IFixedFeeStrategyFactory function createStrategies( uint256[] memory buyFeeList, uint256[] memory sellFeeList ) external returns (address[] memory) { require(buyFeeList.length == sellFeeList.length, 'INVALID_FEE_LIST'); address[] memory strategies = new address[](buyFeeList.length); for (uint256 i = 0; i < buyFeeList.length; i++) { uint256 buyFee = buyFeeList[i]; uint256 sellFee = sellFeeList[i]; address cachedStrategy = _gsmFeeStrategiesByFees[buyFee][sellFee]; if (cachedStrategy == address(0)) { cachedStrategy = address(new FixedFeeStrategy(buyFee, sellFee)); _gsmFeeStrategiesByFees[buyFee][sellFee] = cachedStrategy; _gsmFeeStrategies.add(cachedStrategy); emit FeeStrategyCreated(cachedStrategy, buyFee, sellFee); } strategies[i] = cachedStrategy; } return strategies; } ///@inheritdoc IFixedFeeStrategyFactory function getFixedFeeStrategies() external view returns (address[] memory) { return _gsmFeeStrategies.values(); } ///@inheritdoc IFixedFeeStrategyFactory function getFixedFeeStrategy(uint256 buyFee, uint256 sellFee) external view returns (address) { return _gsmFeeStrategiesByFees[buyFee][sellFee]; } ///@inheritdoc IFixedFeeStrategyFactory function REVISION() public pure virtual override returns (uint256) { return 1; } /// @inheritdoc VersionedInitializable function getRevision() internal pure virtual override returns (uint256) { return REVISION(); } } ================================================ FILE: src/contracts/facilitators/gsm/feeStrategy/interfaces/IFixedFeeStrategyFactory.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /** * @title IFixedFeeStrategyFactory * @author Aave Labs * @notice Defines the interface of the FixedFeeStrategyFactory */ interface IFixedFeeStrategyFactory { /** * @dev Emitted when a new strategy is created * @param strategy The address of the new Gsm fee strategy * @param buyFee The buy fee of the new strategy * @param sellFee The sell fee of the new strategy */ event FeeStrategyCreated( address indexed strategy, uint256 indexed buyFee, uint256 indexed sellFee ); /** * @notice Creates new Gsm Fee strategy contracts from lists of buy and sell fees * @dev Returns the address of a cached contract if a strategy with same fees already exists * @param buyFeeList The list of buy fees for Gsm fee strategies * @param sellFeeList The list of sell fees for Gsm fee strategies * @return The list of Gsm fee strategy contracts */ function createStrategies( uint256[] memory buyFeeList, uint256[] memory sellFeeList ) external returns (address[] memory); /** * @notice Returns all the fee strategy contracts of the factory * @return The list of fee strategy contracts */ function getFixedFeeStrategies() external view returns (address[] memory); /** * @notice Returns the fee strategy contract which corresponds to the given fees. * @dev Returns `address(0)` if there is no fee strategy for the given fees * @param buyFee The buy fee of the fee strategy contract * @param sellFee The sell fee of the fee strategy contract * @return The address of the fee strategy contract */ function getFixedFeeStrategy(uint256 buyFee, uint256 sellFee) external view returns (address); /** * @notice Returns the GsmFeeStrategyFactory revision number * @return The revision number */ function REVISION() external pure returns (uint256); } ================================================ FILE: src/contracts/facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /** * @title IGsmFeeStrategy * @author Aave * @notice Defines the behaviour of Fee Strategies * @dev Functions' logic must be invertible, being possible to calculate the fee amount based on the gross amount, and * the other way round. * @dev All math operations must round up, favoring the protocol. */ interface IGsmFeeStrategy { /** * @notice Returns the fee to be applied when buying an underlying asset in exchange for GHO * @param grossAmount The amount of GHO being sold for the underlying asset * @return The fee amount of GHO */ function getBuyFee(uint256 grossAmount) external view returns (uint256); /** * @notice Returns the fee to be applied when buying GHO in exchange for an underlying asset * @param grossAmount The amount of underlying, converted to GHO, being sold * @return The fee amount of GHO */ function getSellFee(uint256 grossAmount) external view returns (uint256); /** * @notice Returns the gross amount of GHO being bought based on the total bought amount * @param totalAmount The total amount of GHO being bought (gross amount, GHO bought plus fee) * @return The gross amount of GHO being bought (total amount minus fee) */ function getGrossAmountFromTotalBought(uint256 totalAmount) external view returns (uint256); /** * @notice Returns the amount of GHO being sold based on the total sold amount * @param totalAmount The total amount of GHO being sold (gross amount, GHO sold minus fee) * @return The gross amount of GHO being sold (total amount plus fee) */ function getGrossAmountFromTotalSold(uint256 totalAmount) external view returns (uint256); } ================================================ FILE: src/contracts/facilitators/gsm/interfaces/IGsm.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IAccessControl} from '@openzeppelin/contracts/access/IAccessControl.sol'; import {IGhoFacilitator} from '../../../gho/interfaces/IGhoFacilitator.sol'; /** * @title IGsm * @author Aave * @notice Defines the behaviour of a GHO Stability Module */ interface IGsm is IAccessControl, IGhoFacilitator { /** * @dev Emitted when a user buys an asset (selling GHO) in the GSM * @param originator The address of the buyer originating the request * @param receiver The address of the receiver of the underlying asset * @param underlyingAmount The amount of the underlying asset bought * @param ghoAmount The amount of GHO sold, inclusive of fee * @param fee The fee paid by the buyer, in GHO */ event BuyAsset( address indexed originator, address indexed receiver, uint256 underlyingAmount, uint256 ghoAmount, uint256 fee ); /** * @dev Emitted when a user sells an asset (buying GHO) in the GSM * @param originator The address of the seller originating the request * @param receiver The address of the receiver of GHO * @param underlyingAmount The amount of the underlying asset sold * @param ghoAmount The amount of GHO bought, inclusive of fee * @param fee The fee paid by the buyer, in GHO */ event SellAsset( address indexed originator, address indexed receiver, uint256 underlyingAmount, uint256 ghoAmount, uint256 fee ); /** * @dev Emitted when the Swap Freezer freezes buys/sells * @param freezer The address of the Swap Freezer * @param enabled True if swap functions are frozen, False otherwise */ event SwapFreeze(address indexed freezer, bool enabled); /** * @dev Emitted when a Liquidator seizes GSM funds * @param seizer The address originating the seizure request * @param recipient The address of the recipient of seized funds * @param underlyingAmount The amount of the underlying asset seized * @param ghoOutstanding The amount of remaining GHO that the GSM had minted */ event Seized( address indexed seizer, address indexed recipient, uint256 underlyingAmount, uint256 ghoOutstanding ); /** * @dev Emitted when burning GHO after a seizure of GSM funds * @param burner The address of the burner * @param amount The amount of GHO burned * @param ghoOutstanding The amount of remaining GHO that the GSM had minted */ event BurnAfterSeize(address indexed burner, uint256 amount, uint256 ghoOutstanding); /** * @dev Emitted when the Fee Strategy is updated * @param oldFeeStrategy The address of the old Fee Strategy * @param newFeeStrategy The address of the new Fee Strategy */ event FeeStrategyUpdated(address indexed oldFeeStrategy, address indexed newFeeStrategy); /** * @dev Emitted when the GSM underlying asset Exposure Cap is updated * @param oldExposureCap The amount of the old Exposure Cap * @param newExposureCap The amount of the new Exposure Cap */ event ExposureCapUpdated(uint256 oldExposureCap, uint256 newExposureCap); /** * @dev Emitted when tokens are rescued from the GSM * @param tokenRescued The address of the rescued token * @param recipient The address that received the rescued tokens * @param amountRescued The amount of token rescued */ event TokensRescued( address indexed tokenRescued, address indexed recipient, uint256 amountRescued ); /** * @notice Buys the GSM underlying asset in exchange for selling GHO * @dev Use `getAssetAmountForBuyAsset` function to calculate the amount based on the GHO amount to sell * @param minAmount The minimum amount of the underlying asset to buy * @param receiver Recipient address of the underlying asset being purchased * @return The amount of underlying asset bought * @return The amount of GHO sold by the user */ function buyAsset(uint256 minAmount, address receiver) external returns (uint256, uint256); /** * @notice Buys the GSM underlying asset in exchange for selling GHO, using an EIP-712 signature * @dev Use `getAssetAmountForBuyAsset` function to calculate the amount based on the GHO amount to sell * @param originator The signer of the request * @param minAmount The minimum amount of the underlying asset to buy * @param receiver Recipient address of the underlying asset being purchased * @param deadline Signature expiration deadline * @param signature Signature data * @return The amount of underlying asset bought * @return The amount of GHO sold by the user */ function buyAssetWithSig( address originator, uint256 minAmount, address receiver, uint256 deadline, bytes calldata signature ) external returns (uint256, uint256); /** * @notice Sells the GSM underlying asset in exchange for buying GHO * @dev Use `getAssetAmountForSellAsset` function to calculate the amount based on the GHO amount to buy * @param maxAmount The maximum amount of the underlying asset to sell * @param receiver Recipient address of the GHO being purchased * @return The amount of underlying asset sold * @return The amount of GHO bought by the user */ function sellAsset(uint256 maxAmount, address receiver) external returns (uint256, uint256); /** * @notice Sells the GSM underlying asset in exchange for buying GHO, using an EIP-712 signature * @dev Use `getAssetAmountForSellAsset` function to calculate the amount based on the GHO amount to buy * @param originator The signer of the request * @param maxAmount The maximum amount of the underlying asset to sell * @param receiver Recipient address of the GHO being purchased * @param deadline Signature expiration deadline * @param signature Signature data * @return The amount of underlying asset sold * @return The amount of GHO bought by the user */ function sellAssetWithSig( address originator, uint256 maxAmount, address receiver, uint256 deadline, bytes calldata signature ) external returns (uint256, uint256); /** * @notice Rescue and transfer tokens locked in this contract * @param token The address of the token * @param to The address of the recipient * @param amount The amount of token to transfer */ function rescueTokens(address token, address to, uint256 amount) external; /** * @notice Enable or disable the swap freeze * @param enable True to freeze swap functions, false otherwise */ function setSwapFreeze(bool enable) external; /** * @notice Seizes all of the underlying asset from the GSM, sending to the Treasury * @dev Seizing is a last resort mechanism to provide the Treasury with the entire amount of underlying asset * so it can be used to backstop any potential event impacting the functionality of the Gsm. * @dev Seizing disables the swap feature * @return The amount of underlying asset seized and transferred to Treasury */ function seize() external returns (uint256); /** * @notice Burns an amount of GHO after seizure reducing the facilitator bucket level effectively * @dev Passing an amount higher than the facilitator bucket level will result in burning all minted GHO * @dev Only callable if the GSM has assets seized, helpful to wind down the facilitator * @param amount The amount of GHO to burn * @return The amount of GHO burned */ function burnAfterSeize(uint256 amount) external returns (uint256); /** * @notice Updates the address of the Fee Strategy * @param feeStrategy The address of the new FeeStrategy */ function updateFeeStrategy(address feeStrategy) external; /** * @notice Updates the exposure cap of the underlying asset * @param exposureCap The new value for the exposure cap (in underlying asset terms) */ function updateExposureCap(uint128 exposureCap) external; /** * @notice Returns the EIP712 domain separator * @return The EIP712 domain separator */ function DOMAIN_SEPARATOR() external view returns (bytes32); /** * @notice Returns the total amount of GHO, gross amount and fee result of buying assets * @param minAssetAmount The minimum amount of underlying asset to buy * @return The exact amount of underlying asset to be bought * @return The total amount of GHO the user sells (gross amount in GHO plus fee) * @return The gross amount of GHO * @return The fee amount in GHO, applied on top of gross amount of GHO */ function getGhoAmountForBuyAsset( uint256 minAssetAmount ) external view returns (uint256, uint256, uint256, uint256); /** * @notice Returns the total amount of GHO, gross amount and fee result of selling assets * @param maxAssetAmount The maximum amount of underlying asset to sell * @return The exact amount of underlying asset to sell * @return The total amount of GHO the user buys (gross amount in GHO minus fee) * @return The gross amount of GHO * @return The fee amount in GHO, applied to the gross amount of GHO */ function getGhoAmountForSellAsset( uint256 maxAssetAmount ) external view returns (uint256, uint256, uint256, uint256); /** * @notice Returns the amount of underlying asset, gross amount of GHO and fee result of buying assets * @param maxGhoAmount The maximum amount of GHO the user provides for buying underlying asset * @return The amount of underlying asset the user buys * @return The exact amount of GHO the user provides * @return The gross amount of GHO corresponding to the given total amount of GHO * @return The fee amount in GHO, charged for buying assets */ function getAssetAmountForBuyAsset( uint256 maxGhoAmount ) external view returns (uint256, uint256, uint256, uint256); /** * @notice Returns the amount of underlying asset, gross amount of GHO and fee result of selling assets * @param minGhoAmount The minimum amount of GHO the user must receive for selling underlying asset * @return The amount of underlying asset the user sells * @return The exact amount of GHO the user receives in exchange * @return The gross amount of GHO corresponding to the given total amount of GHO * @return The fee amount in GHO, charged for selling assets */ function getAssetAmountForSellAsset( uint256 minGhoAmount ) external view returns (uint256, uint256, uint256, uint256); /** * @notice Returns the remaining GSM exposure capacity * @return The amount of underlying asset that can be sold to the GSM */ function getAvailableUnderlyingExposure() external view returns (uint256); /** * @notice Returns the exposure limit to the underlying asset * @return The maximum amount of underlying asset that can be sold to the GSM */ function getExposureCap() external view returns (uint128); /** * @notice Returns the actual underlying asset balance immediately available in the GSM * @return The amount of underlying asset that can be bought from the GSM */ function getAvailableLiquidity() external view returns (uint256); /** * @notice Returns the Fee Strategy for the GSM * @dev It returns 0x0 in case of no fee strategy * @return The address of the FeeStrategy */ function getFeeStrategy() external view returns (address); /** * @notice Returns the amount of current accrued fees * @dev It does not factor in potential fees that can be accrued upon distribution of fees * @return The amount of accrued fees */ function getAccruedFees() external view returns (uint256); /** * @notice Returns the freeze status of the GSM * @return True if frozen, false if not */ function getIsFrozen() external view returns (bool); /** * @notice Returns the current seizure status of the GSM * @return True if the GSM has been seized, false if not */ function getIsSeized() external view returns (bool); /** * @notice Returns whether or not swaps via buyAsset/sellAsset are currently possible * @return True if the GSM has swapping enabled, false otherwise */ function canSwap() external view returns (bool); /** * @notice Returns the GSM revision number * @return The revision number */ function GSM_REVISION() external pure returns (uint256); /** * @notice Returns the address of the GHO token * @return The address of GHO token contract */ function GHO_TOKEN() external view returns (address); /** * @notice Returns the underlying asset of the GSM * @return The address of the underlying asset */ function UNDERLYING_ASSET() external view returns (address); /** * @notice Returns the price strategy of the GSM * @return The address of the price strategy */ function PRICE_STRATEGY() external view returns (address); /** * @notice Returns the current nonce (for EIP-712 signature methods) of an address * @param user The address of the user * @return The current nonce of the user */ function nonces(address user) external view returns (uint256); /** * @notice Returns the identifier of the Configurator Role * @return The bytes32 id hash of the Configurator role */ function CONFIGURATOR_ROLE() external pure returns (bytes32); /** * @notice Returns the identifier of the Token Rescuer Role * @return The bytes32 id hash of the TokenRescuer role */ function TOKEN_RESCUER_ROLE() external pure returns (bytes32); /** * @notice Returns the identifier of the Swap Freezer Role * @return The bytes32 id hash of the SwapFreezer role */ function SWAP_FREEZER_ROLE() external pure returns (bytes32); /** * @notice Returns the identifier of the Liquidator Role * @return The bytes32 id hash of the Liquidator role */ function LIQUIDATOR_ROLE() external pure returns (bytes32); /** * @notice Returns the EIP-712 signature typehash for buyAssetWithSig * @return The bytes32 signature typehash for buyAssetWithSig */ function BUY_ASSET_WITH_SIG_TYPEHASH() external pure returns (bytes32); /** * @notice Returns the EIP-712 signature typehash for sellAssetWithSig * @return The bytes32 signature typehash for sellAssetWithSig */ function SELL_ASSET_WITH_SIG_TYPEHASH() external pure returns (bytes32); } ================================================ FILE: src/contracts/facilitators/gsm/interfaces/IGsm4626.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IGsm} from './IGsm.sol'; /** * @title IGsm4626 * @author Aave * @notice Defines the behaviour of a GHO Stability Module with an ERC-4626 underlying asset */ interface IGsm4626 is IGsm { /** * @dev Emitted when an asset is provided to the GSM to backstop a loss * @param backer The address of the backer * @param asset The address of the provided asset * @param amount The amount of the asset * @param ghoAmount The amount of the asset, in GHO terms * @param remainingLoss The loss balance that remains after the operation */ event BackingProvided( address indexed backer, address indexed asset, uint256 amount, uint256 ghoAmount, uint256 remainingLoss ); /** * @notice Restores backing of GHO by burning GHO * @dev Useful in the event the underlying value declines relative to GHO minted * @dev Passing an amount higher than the current deficit will result in backing the entire deficit * @param amount The amount of GHO to be burned * @return The amount of GHO used for backing */ function backWithGho(uint256 amount) external returns (uint256); /** * @notice Restores backing of GHO by providing underlying asset * @dev Useful in the event the underlying value declines relative to GHO minted * @dev Passing an amount higher than the current deficit will result in backing the entire deficit * @param amount The amount of underlying to be used for backing * @return The amount of underlying used for backing */ function backWithUnderlying(uint256 amount) external returns (uint256); /** * @notice Returns the excess or deficit of GHO, reflecting current GSM backing * @return The excess amount of GHO minted, relative to the value of the underlying * @return The deficit of GHO minted, relative to the value of the underlying */ function getCurrentBacking() external view returns (uint256, uint256); } ================================================ FILE: src/contracts/facilitators/gsm/misc/GsmRegistry.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; import {IGsmRegistry} from './IGsmRegistry.sol'; /** * @title GsmRegistry * @author Aave * @notice Main registry of GSM contracts. */ contract GsmRegistry is Ownable, IGsmRegistry { using EnumerableSet for EnumerableSet.AddressSet; EnumerableSet.AddressSet internal _gsmList; /** * @dev Constructor * @param newOwner The address of the contract owner */ constructor(address newOwner) { require(newOwner != address(0), 'ZERO_ADDRESS_NOT_VALID'); _transferOwnership(newOwner); } /// @inheritdoc IGsmRegistry function addGsm(address gsmAddress) external onlyOwner { require(gsmAddress != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(_gsmList.add(gsmAddress), 'GSM_ALREADY_ADDED'); emit GsmAdded(gsmAddress); } /// @inheritdoc IGsmRegistry function removeGsm(address gsmAddress) external onlyOwner { require(gsmAddress != address(0), 'ZERO_ADDRESS_NOT_VALID'); require(_gsmList.remove(gsmAddress), 'NONEXISTENT_GSM'); emit GsmRemoved(gsmAddress); } /// @inheritdoc IGsmRegistry function getGsmList() external view returns (address[] memory) { return _gsmList.values(); } /// @inheritdoc IGsmRegistry function getGsmListLength() external view returns (uint256) { return _gsmList.length(); } /// @inheritdoc IGsmRegistry function getGsmAtIndex(uint256 index) external view returns (address) { require(index < _gsmList.length(), 'INVALID_INDEX'); return _gsmList.at(index); } } ================================================ FILE: src/contracts/facilitators/gsm/misc/IGsmRegistry.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /** * @title IGsmRegistry * @author Aave * @notice Defines the behaviour of the GsmRegistry */ interface IGsmRegistry { /** * @dev Emitted when a new GSM is added to the registry * @param gsmAddress The address of the GSM contract */ event GsmAdded(address indexed gsmAddress); /** * @dev Emitted when a new GSM is removed from the registry * @param gsmAddress The address of the GSM contract */ event GsmRemoved(address indexed gsmAddress); /** * @notice Adds a new GSM to the registry * @param gsmAddress The address of the GSM contract */ function addGsm(address gsmAddress) external; /** * @notice Removes a GSM from the registry * @param gsmAddress The address of the GSM contract */ function removeGsm(address gsmAddress) external; /** * @notice Returns a list of GSM addresses to the registry * @return A list of GSM contract addresses */ function getGsmList() external view returns (address[] memory); /** * @notice Returns the length of the list of GSM addresses * @return The size of the GSM list */ function getGsmListLength() external view returns (uint256); /** * @notice Returns the address of the GSM placed in the list at the given index * @param index The index of the GSM within the list * @return The GSM address */ function getGsmAtIndex(uint256 index) external view returns (address); } ================================================ FILE: src/contracts/facilitators/gsm/misc/SampleLiquidator.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; import {IGhoToken} from '../../../gho/interfaces/IGhoToken.sol'; import {IGsm} from '../interfaces/IGsm.sol'; /** * @title SampleLiquidator * @author Aave * @notice Minimal Liquidator that can serve as sample contract */ contract SampleLiquidator is Ownable { /** * @notice Triggers seizure of a GSM, sending seized funds to the Treasury * @param gsm Address of the GSM * @return The amount of underlying asset seized and transferred to Treasury */ function triggerSeize(address gsm) external onlyOwner returns (uint256) { return IGsm(gsm).seize(); } /** * @notice Pulls GHO from the sender and burns it via the GSM * @param gsm Address of the GSM * @param amount The maximum amount of GHO to be burned * @return The amount of GHO burned */ function triggerBurnAfterSeize(address gsm, uint256 amount) external onlyOwner returns (uint256) { IERC20 ghoToken = IERC20(IGsm(gsm).GHO_TOKEN()); (, uint256 ghoMinted) = IGhoToken(address(ghoToken)).getFacilitatorBucket(gsm); if (amount > ghoMinted) { amount = ghoMinted; } ghoToken.transferFrom(msg.sender, address(this), amount); ghoToken.approve(gsm, amount); return IGsm(gsm).burnAfterSeize(amount); } } ================================================ FILE: src/contracts/facilitators/gsm/misc/SampleSwapFreezer.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; import {IGsm} from '../interfaces/IGsm.sol'; /** * @title SampleSwapFreezer * @author Aave * @notice Minimal Swap Freezer that can serve as sample contract */ contract SampleSwapFreezer is Ownable { /** * @notice Triggers freezing of a GSM * @param gsm Address of the GSM */ function triggerFreeze(address gsm) external onlyOwner { IGsm(gsm).setSwapFreeze(true); } /** * @notice Triggers unfreezing of a GSM * @param gsm Address of the GSM */ function triggerUnfreeze(address gsm) external onlyOwner { IGsm(gsm).setSwapFreeze(false); } } ================================================ FILE: src/contracts/facilitators/gsm/priceStrategy/FixedPriceStrategy.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; import {IGsmPriceStrategy} from './interfaces/IGsmPriceStrategy.sol'; /** * @title FixedPriceStrategy * @author Aave * @notice Price strategy involving a fixed-rate conversion from an underlying asset to GHO */ contract FixedPriceStrategy is IGsmPriceStrategy { using Math for uint256; /// @inheritdoc IGsmPriceStrategy uint256 public constant GHO_DECIMALS = 18; /// @inheritdoc IGsmPriceStrategy address public immutable UNDERLYING_ASSET; /// @inheritdoc IGsmPriceStrategy uint256 public immutable UNDERLYING_ASSET_DECIMALS; /// @dev The price ratio from underlying asset to GHO (expressed in WAD), e.g. a ratio of 2e18 means 2 GHO per 1 underlying asset uint256 public immutable PRICE_RATIO; /// @dev Underlying asset units represent units for the underlying asset uint256 internal immutable _underlyingAssetUnits; /** * @dev Constructor * @param priceRatio The price ratio from underlying asset to GHO (expressed in WAD) * @param underlyingAsset The address of the underlying asset * @param underlyingAssetDecimals The number of decimals of the underlying asset */ constructor(uint256 priceRatio, address underlyingAsset, uint8 underlyingAssetDecimals) { require(priceRatio > 0, 'INVALID_PRICE_RATIO'); PRICE_RATIO = priceRatio; UNDERLYING_ASSET = underlyingAsset; UNDERLYING_ASSET_DECIMALS = underlyingAssetDecimals; _underlyingAssetUnits = 10 ** underlyingAssetDecimals; } /// @inheritdoc IGsmPriceStrategy function getAssetPriceInGho(uint256 assetAmount, bool roundUp) external view returns (uint256) { return assetAmount.mulDiv( PRICE_RATIO, _underlyingAssetUnits, roundUp ? Math.Rounding.Up : Math.Rounding.Down ); } /// @inheritdoc IGsmPriceStrategy function getGhoPriceInAsset(uint256 ghoAmount, bool roundUp) external view returns (uint256) { return ghoAmount.mulDiv( _underlyingAssetUnits, PRICE_RATIO, roundUp ? Math.Rounding.Up : Math.Rounding.Down ); } } ================================================ FILE: src/contracts/facilitators/gsm/priceStrategy/FixedPriceStrategy4626.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; import {IERC4626} from '@openzeppelin/contracts/interfaces/IERC4626.sol'; import {IGsmPriceStrategy} from './interfaces/IGsmPriceStrategy.sol'; /** * @title FixedPriceStrategy4626 * @author Aave * @notice Price strategy involving a fixed-rate conversion from an ERC4626 vault to GHO * @dev 4626 vault assets represent the underlying asset held by a vault, vault shares are the vault token */ contract FixedPriceStrategy4626 is IGsmPriceStrategy { using Math for uint256; /// @inheritdoc IGsmPriceStrategy uint256 public constant GHO_DECIMALS = 18; /// @inheritdoc IGsmPriceStrategy address public immutable UNDERLYING_ASSET; /// @dev Underlying asset decimals represent decimals for the 4626 vault asset, not for the vault share uint256 public immutable UNDERLYING_ASSET_DECIMALS; /// @dev The price ratio from 4626 vault asset to GHO (expressed in WAD), e.g. a ratio of 2e18 means 2 GHO per 1 vault asset uint256 public immutable PRICE_RATIO; /// @dev Underlying asset units represent units for the 4626 vault asset, not for the vault share uint256 internal immutable _underlyingAssetUnits; /** * @dev Constructor * @param priceRatio The price ratio from 4626 vault asset to GHO (expressed in WAD) * @param underlyingAsset The address of the 4626 vault (i.e., corresponding to vault shares) * @param underlyingAssetDecimals The number of decimals of the 4626 vault asset */ constructor(uint256 priceRatio, address underlyingAsset, uint8 underlyingAssetDecimals) { require(priceRatio > 0, 'INVALID_PRICE_RATIO'); PRICE_RATIO = priceRatio; UNDERLYING_ASSET = underlyingAsset; UNDERLYING_ASSET_DECIMALS = underlyingAssetDecimals; _underlyingAssetUnits = 10 ** underlyingAssetDecimals; } /// @inheritdoc IGsmPriceStrategy function getAssetPriceInGho(uint256 assetAmount, bool roundUp) external view returns (uint256) { // conversion from 4626 shares to 4626 assets uint256 vaultAssets = roundUp ? IERC4626(UNDERLYING_ASSET).previewMint(assetAmount) // round up : IERC4626(UNDERLYING_ASSET).convertToAssets(assetAmount); // round down return vaultAssets.mulDiv( PRICE_RATIO, _underlyingAssetUnits, roundUp ? Math.Rounding.Up : Math.Rounding.Down ); } /// @inheritdoc IGsmPriceStrategy function getGhoPriceInAsset(uint256 ghoAmount, bool roundUp) external view returns (uint256) { uint256 vaultAssets = ghoAmount.mulDiv( _underlyingAssetUnits, PRICE_RATIO, roundUp ? Math.Rounding.Up : Math.Rounding.Down ); // conversion of 4626 assets to 4626 shares return roundUp ? IERC4626(UNDERLYING_ASSET).previewWithdraw(vaultAssets) // round up : IERC4626(UNDERLYING_ASSET).convertToShares(vaultAssets); // round down } } ================================================ FILE: src/contracts/facilitators/gsm/priceStrategy/interfaces/IGsmPriceStrategy.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /** * @title IGsmPriceStrategy * @author Aave * @notice Defines the behaviour of Price Strategies */ interface IGsmPriceStrategy { /** * @notice Returns the number of decimals of GHO * @return The number of decimals of GHO */ function GHO_DECIMALS() external view returns (uint256); /** * @notice Returns the address of the underlying asset being priced * @return The address of the underlying asset */ function UNDERLYING_ASSET() external view returns (address); /** * @notice Returns the decimals of the underlying asset being priced * @return The number of decimals of the underlying asset */ function UNDERLYING_ASSET_DECIMALS() external view returns (uint256); /** * @notice Returns the price of the underlying asset (GHO denominated) * @param assetAmount The amount of the underlying asset to calculate the price of * @param roundUp True if the price should be rounded up, false if rounded down * @return The price of the underlying asset (expressed in GHO units) */ function getAssetPriceInGho(uint256 assetAmount, bool roundUp) external view returns (uint256); /** * @notice Returns the price of GHO (denominated in the underlying asset) * @param ghoAmount The amount of GHO to calculate the price of * @param roundUp True if the price should be rounded up, false if rounded down * @return The price of the GHO amount (expressed in underlying asset units) */ function getGhoPriceInAsset(uint256 ghoAmount, bool roundUp) external view returns (uint256); } ================================================ FILE: src/contracts/facilitators/gsm/swapFreezer/OracleSwapFreezer.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; import {IPriceOracle} from '@aave/core-v3/contracts/interfaces/IPriceOracle.sol'; import {AutomationCompatibleInterface} from '../dependencies/chainlink/AutomationCompatibleInterface.sol'; import {IGsm} from '../interfaces/IGsm.sol'; /** * @title OracleSwapFreezer * @author Aave * @notice Swap freezer that enacts the freeze action based on underlying oracle price, GSM's state and predefined price boundaries * @dev Chainlink Automation-compatible contract using Aave V3 Price Oracle, where prices are USD denominated with 8-decimal precision * @dev Freeze action is executable if GSM is not seized, not frozen and price is outside of the freeze bounds * @dev Unfreeze action is executable if GSM is not seized, frozen, unfreezing is allowed and price is inside the unfreeze bounds */ contract OracleSwapFreezer is AutomationCompatibleInterface { enum Action { NONE, FREEZE, UNFREEZE } IGsm public immutable GSM; address public immutable UNDERLYING_ASSET; IPoolAddressesProvider public immutable ADDRESS_PROVIDER; uint128 internal immutable _freezeLowerBound; uint128 internal immutable _freezeUpperBound; uint128 internal immutable _unfreezeLowerBound; uint128 internal immutable _unfreezeUpperBound; bool internal immutable _allowUnfreeze; /** * @dev Constructor * @dev Freeze/unfreeze bounds are specified in USD with 8-decimal precision, like Aave v3 Price Oracles * @dev Unfreeze boundaries are "contained" in freeze boundaries, where freezeLowerBound < unfreezeLowerBound and unfreezeUpperBound < freezeUpperBound * @dev All bound ranges are inclusive * @param gsm The GSM that this contract will trigger freezes/unfreezes on * @param underlyingAsset The address of the collateral asset * @param addressProvider The Aave Addresses Provider for looking up the Price Oracle * @param freezeLowerBound The lower price bound for freeze operations * @param freezeUpperBound The upper price bound for freeze operations * @param unfreezeLowerBound The lower price bound for unfreeze operations, must be 0 if unfreezing not allowed * @param unfreezeUpperBound The upper price bound for unfreeze operations, must be 0 if unfreezing not allowed * @param allowUnfreeze True if bounds verification should factor in the unfreeze boundary, false otherwise */ constructor( IGsm gsm, address underlyingAsset, IPoolAddressesProvider addressProvider, uint128 freezeLowerBound, uint128 freezeUpperBound, uint128 unfreezeLowerBound, uint128 unfreezeUpperBound, bool allowUnfreeze ) { require(underlyingAsset != address(0), 'ZERO_ADDRESS_NOT_VALID'); require( _validateBounds( freezeLowerBound, freezeUpperBound, unfreezeLowerBound, unfreezeUpperBound, allowUnfreeze ), 'BOUNDS_NOT_VALID' ); GSM = gsm; UNDERLYING_ASSET = underlyingAsset; ADDRESS_PROVIDER = addressProvider; _freezeLowerBound = freezeLowerBound; _freezeUpperBound = freezeUpperBound; _unfreezeLowerBound = unfreezeLowerBound; _unfreezeUpperBound = unfreezeUpperBound; _allowUnfreeze = allowUnfreeze; } /// @inheritdoc AutomationCompatibleInterface function performUpkeep(bytes calldata) external { Action action = _getAction(); if (action == Action.FREEZE) { GSM.setSwapFreeze(true); } else if (action == Action.UNFREEZE) { GSM.setSwapFreeze(false); } } /// @inheritdoc AutomationCompatibleInterface function checkUpkeep(bytes calldata) external view returns (bool, bytes memory) { return (_getAction() == Action.NONE ? false : true, ''); } /** * @notice Returns whether or not the swap freezer can unfreeze a GSM * @return True if the freezer can unfreeze, false otherwise */ function getCanUnfreeze() external view returns (bool) { return _allowUnfreeze; } /** * @notice Returns the bound used for freeze operations * @return The freeze lower bound (inclusive) * @return The freeze upper bound (inclusive) */ function getFreezeBound() external view returns (uint128, uint128) { return (_freezeLowerBound, _freezeUpperBound); } /** * @notice Returns the bound used for unfreeze operations, or (0, 0) if unfreezing not allowed * @return The unfreeze lower bound (inclusive), or 0 if unfreezing not allowed * @return The unfreeze upper bound (inclusive), or 0 if unfreezing not allowed */ function getUnfreezeBound() external view returns (uint128, uint128) { return (_unfreezeLowerBound, _unfreezeUpperBound); } /** * @notice Fetches price oracle data and checks whether a swap freeze or unfreeze action is required * @return The action to take (none, freeze, or unfreeze) */ function _getAction() internal view returns (Action) { if (GSM.hasRole(GSM.SWAP_FREEZER_ROLE(), address(this))) { if (GSM.getIsSeized()) { return Action.NONE; } else if (!GSM.getIsFrozen()) { if (_isActionAllowed(Action.FREEZE)) { return Action.FREEZE; } } else if (_allowUnfreeze) { if (_isActionAllowed(Action.UNFREEZE)) { return Action.UNFREEZE; } } } return Action.NONE; } /** * @notice Checks whether the action is allowed, based on the action, oracle price and freeze/unfreeze bounds * @dev Freeze action is allowed if price is outside of the freeze bounds * @dev Unfreeze action is allowed if price is inside the unfreeze bounds * @param actionToExecute The requested action type to validate * @return True if conditions to execute the action passed are met, false otherwise */ function _isActionAllowed(Action actionToExecute) internal view returns (bool) { uint256 oraclePrice = IPriceOracle(ADDRESS_PROVIDER.getPriceOracle()).getAssetPrice( UNDERLYING_ASSET ); // Assume a 0 oracle price is invalid and no action should be taken based on that data if (oraclePrice == 0) { return false; } else if (actionToExecute == Action.FREEZE) { if (oraclePrice <= _freezeLowerBound || oraclePrice >= _freezeUpperBound) { return true; } } else if (actionToExecute == Action.UNFREEZE) { if (oraclePrice >= _unfreezeLowerBound && oraclePrice <= _unfreezeUpperBound) { return true; } } return false; } /** * @notice Verifies that the unfreeze bound and freeze bounds do not conflict, causing unexpected behaviour * @param freezeLowerBound The lower bound for freeze operations * @param freezeUpperBound The upper bound for freeze operations * @param unfreezeLowerBound The lower bound for unfreeze operations, must be 0 if unfreezing not allowed * @param unfreezeUpperBound The upper bound for unfreeze operations, must be 0 if unfreezing not allowed * @param allowUnfreeze True if bounds verification should factor in the unfreeze boundary, false otherwise * @return True if the bounds are valid and conflict-free, false otherwise */ function _validateBounds( uint128 freezeLowerBound, uint128 freezeUpperBound, uint128 unfreezeLowerBound, uint128 unfreezeUpperBound, bool allowUnfreeze ) internal pure returns (bool) { if (freezeLowerBound >= freezeUpperBound) { return false; } else if (allowUnfreeze) { if ( unfreezeLowerBound >= unfreezeUpperBound || freezeLowerBound >= unfreezeLowerBound || freezeUpperBound <= unfreezeUpperBound ) { return false; } } else { if (unfreezeLowerBound != 0 || unfreezeUpperBound != 0) { return false; } } return true; } } ================================================ FILE: src/contracts/gho/ERC20.sol ================================================ // SPDX-License-Identifier: MIT-only pragma solidity ^0.8.0; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; /** * @title ERC20 * @notice Gas Efficient ERC20 + EIP-2612 implementation * @dev Modified version of Solmate ERC20 (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol), * implementing the basic IERC20 */ abstract contract ERC20 is IERC20 { /*/////////////////////////////////////////////////////////////// METADATA STORAGE //////////////////////////////////////////////////////////////*/ string public name; string public symbol; uint8 public immutable decimals; /*/////////////////////////////////////////////////////////////// ERC20 STORAGE //////////////////////////////////////////////////////////////*/ uint256 public totalSupply; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; /*/////////////////////////////////////////////////////////////// EIP-2612 STORAGE //////////////////////////////////////////////////////////////*/ bytes32 public constant PERMIT_TYPEHASH = keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)'); uint256 internal immutable INITIAL_CHAIN_ID; bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR; mapping(address => uint256) public nonces; /*/////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ constructor(string memory _name, string memory _symbol, uint8 _decimals) { name = _name; symbol = _symbol; decimals = _decimals; INITIAL_CHAIN_ID = block.chainid; INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); } /*/////////////////////////////////////////////////////////////// ERC20 LOGIC //////////////////////////////////////////////////////////////*/ function approve(address spender, uint256 amount) public virtual returns (bool) { allowance[msg.sender][spender] = amount; emit Approval(msg.sender, spender, amount); return true; } function transfer(address to, uint256 amount) public virtual returns (bool) { balanceOf[msg.sender] -= amount; // Cannot overflow because the sum of all user // balances can't exceed the max uint256 value. unchecked { balanceOf[to] += amount; } emit Transfer(msg.sender, to, amount); return true; } function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) { uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; balanceOf[from] -= amount; // Cannot overflow because the sum of all user // balances can't exceed the max uint256 value. unchecked { balanceOf[to] += amount; } emit Transfer(from, to, amount); return true; } /*/////////////////////////////////////////////////////////////// EIP-2612 LOGIC //////////////////////////////////////////////////////////////*/ function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) public virtual { require(deadline >= block.timestamp, 'PERMIT_DEADLINE_EXPIRED'); // Unchecked because the only math done is incrementing // the owner's nonce which cannot realistically overflow. unchecked { bytes32 digest = keccak256( abi.encodePacked( '\x19\x01', DOMAIN_SEPARATOR(), keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) ) ); address recoveredAddress = ecrecover(digest, v, r, s); require(recoveredAddress != address(0) && recoveredAddress == owner, 'INVALID_SIGNER'); allowance[recoveredAddress][spender] = value; } emit Approval(owner, spender, value); } function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); } function computeDomainSeparator() internal view virtual returns (bytes32) { return keccak256( abi.encode( keccak256( 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' ), keccak256(bytes(name)), keccak256('1'), block.chainid, address(this) ) ); } /*/////////////////////////////////////////////////////////////// INTERNAL MINT/BURN LOGIC //////////////////////////////////////////////////////////////*/ function _mint(address to, uint256 amount) internal virtual { totalSupply += amount; // Cannot overflow because the sum of all user // balances can't exceed the max uint256 value. unchecked { balanceOf[to] += amount; } emit Transfer(address(0), to, amount); } function _burn(address from, uint256 amount) internal virtual { balanceOf[from] -= amount; // Cannot underflow because a user's balance // will never be larger than the total supply. unchecked { totalSupply -= amount; } emit Transfer(from, address(0), amount); } } ================================================ FILE: src/contracts/gho/GhoToken.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol'; import {ERC20} from './ERC20.sol'; import {IGhoToken} from './interfaces/IGhoToken.sol'; /** * @title GHO Token * @author Aave */ contract GhoToken is ERC20, AccessControl, IGhoToken { using EnumerableSet for EnumerableSet.AddressSet; mapping(address => Facilitator) internal _facilitators; EnumerableSet.AddressSet internal _facilitatorsList; /// @inheritdoc IGhoToken bytes32 public constant FACILITATOR_MANAGER_ROLE = keccak256('FACILITATOR_MANAGER_ROLE'); /// @inheritdoc IGhoToken bytes32 public constant BUCKET_MANAGER_ROLE = keccak256('BUCKET_MANAGER_ROLE'); /** * @dev Constructor * @param admin This is the initial holder of the default admin role */ constructor(address admin) ERC20('Gho Token', 'GHO', 18) { _setupRole(DEFAULT_ADMIN_ROLE, admin); } /// @inheritdoc IGhoToken function mint(address account, uint256 amount) external { require(amount > 0, 'INVALID_MINT_AMOUNT'); Facilitator storage f = _facilitators[msg.sender]; uint256 currentBucketLevel = f.bucketLevel; uint256 newBucketLevel = currentBucketLevel + amount; require(f.bucketCapacity >= newBucketLevel, 'FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); f.bucketLevel = uint128(newBucketLevel); _mint(account, amount); emit FacilitatorBucketLevelUpdated(msg.sender, currentBucketLevel, newBucketLevel); } /// @inheritdoc IGhoToken function burn(uint256 amount) external { require(amount > 0, 'INVALID_BURN_AMOUNT'); Facilitator storage f = _facilitators[msg.sender]; uint256 currentBucketLevel = f.bucketLevel; uint256 newBucketLevel = currentBucketLevel - amount; f.bucketLevel = uint128(newBucketLevel); _burn(msg.sender, amount); emit FacilitatorBucketLevelUpdated(msg.sender, currentBucketLevel, newBucketLevel); } /// @inheritdoc IGhoToken function addFacilitator( address facilitatorAddress, string calldata facilitatorLabel, uint128 bucketCapacity ) external onlyRole(FACILITATOR_MANAGER_ROLE) { Facilitator storage facilitator = _facilitators[facilitatorAddress]; require(bytes(facilitator.label).length == 0, 'FACILITATOR_ALREADY_EXISTS'); require(bytes(facilitatorLabel).length > 0, 'INVALID_LABEL'); facilitator.label = facilitatorLabel; facilitator.bucketCapacity = bucketCapacity; _facilitatorsList.add(facilitatorAddress); emit FacilitatorAdded( facilitatorAddress, keccak256(abi.encodePacked(facilitatorLabel)), bucketCapacity ); } /// @inheritdoc IGhoToken function removeFacilitator( address facilitatorAddress ) external onlyRole(FACILITATOR_MANAGER_ROLE) { require( bytes(_facilitators[facilitatorAddress].label).length > 0, 'FACILITATOR_DOES_NOT_EXIST' ); require( _facilitators[facilitatorAddress].bucketLevel == 0, 'FACILITATOR_BUCKET_LEVEL_NOT_ZERO' ); delete _facilitators[facilitatorAddress]; _facilitatorsList.remove(facilitatorAddress); emit FacilitatorRemoved(facilitatorAddress); } /// @inheritdoc IGhoToken function setFacilitatorBucketCapacity( address facilitator, uint128 newCapacity ) external onlyRole(BUCKET_MANAGER_ROLE) { require(bytes(_facilitators[facilitator].label).length > 0, 'FACILITATOR_DOES_NOT_EXIST'); uint256 oldCapacity = _facilitators[facilitator].bucketCapacity; _facilitators[facilitator].bucketCapacity = newCapacity; emit FacilitatorBucketCapacityUpdated(facilitator, oldCapacity, newCapacity); } /// @inheritdoc IGhoToken function getFacilitator(address facilitator) external view returns (Facilitator memory) { return _facilitators[facilitator]; } /// @inheritdoc IGhoToken function getFacilitatorBucket(address facilitator) external view returns (uint256, uint256) { return (_facilitators[facilitator].bucketCapacity, _facilitators[facilitator].bucketLevel); } /// @inheritdoc IGhoToken function getFacilitatorsList() external view returns (address[] memory) { return _facilitatorsList.values(); } } ================================================ FILE: src/contracts/gho/UpgradeableERC20.sol ================================================ // SPDX-License-Identifier: MIT-only pragma solidity ^0.8.0; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; /** * @title UpgradeableERC20 * @author Aave Labs * @notice Upgradeable version of Solmate ERC20 * @dev Contract adaptations: * - Removal of domain separator optimization * - Move of name and symbol definition to initialization stage */ abstract contract UpgradeableERC20 is IERC20 { /*/////////////////////////////////////////////////////////////// METADATA STORAGE //////////////////////////////////////////////////////////////*/ string public name; string public symbol; uint8 public immutable decimals; /*/////////////////////////////////////////////////////////////// ERC20 STORAGE //////////////////////////////////////////////////////////////*/ uint256 public totalSupply; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; /*/////////////////////////////////////////////////////////////// EIP-2612 STORAGE //////////////////////////////////////////////////////////////*/ bytes32 public constant PERMIT_TYPEHASH = keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)'); mapping(address => uint256) public nonces; /*/////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ constructor(uint8 _decimals) { decimals = _decimals; } /*/////////////////////////////////////////////////////////////// INITIALIZER //////////////////////////////////////////////////////////////*/ function _ERC20_init(string memory _name, string memory _symbol) internal { name = _name; symbol = _symbol; } /*/////////////////////////////////////////////////////////////// ERC20 LOGIC //////////////////////////////////////////////////////////////*/ function approve(address spender, uint256 amount) public virtual returns (bool) { allowance[msg.sender][spender] = amount; emit Approval(msg.sender, spender, amount); return true; } function transfer(address to, uint256 amount) public virtual returns (bool) { balanceOf[msg.sender] -= amount; // Cannot overflow because the sum of all user // balances can't exceed the max uint256 value. unchecked { balanceOf[to] += amount; } emit Transfer(msg.sender, to, amount); return true; } function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) { uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; balanceOf[from] -= amount; // Cannot overflow because the sum of all user // balances can't exceed the max uint256 value. unchecked { balanceOf[to] += amount; } emit Transfer(from, to, amount); return true; } /*/////////////////////////////////////////////////////////////// EIP-2612 LOGIC //////////////////////////////////////////////////////////////*/ function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) public virtual { require(deadline >= block.timestamp, 'PERMIT_DEADLINE_EXPIRED'); // Unchecked because the only math done is incrementing // the owner's nonce which cannot realistically overflow. unchecked { bytes32 digest = keccak256( abi.encodePacked( '\x19\x01', DOMAIN_SEPARATOR(), keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) ) ); address recoveredAddress = ecrecover(digest, v, r, s); require(recoveredAddress != address(0) && recoveredAddress == owner, 'INVALID_SIGNER'); allowance[recoveredAddress][spender] = value; } emit Approval(owner, spender, value); } function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { return computeDomainSeparator(); } function computeDomainSeparator() internal view virtual returns (bytes32) { return keccak256( abi.encode( keccak256( 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' ), keccak256(bytes(name)), keccak256('1'), block.chainid, address(this) ) ); } /*/////////////////////////////////////////////////////////////// INTERNAL MINT/BURN LOGIC //////////////////////////////////////////////////////////////*/ function _mint(address to, uint256 amount) internal virtual { totalSupply += amount; // Cannot overflow because the sum of all user // balances can't exceed the max uint256 value. unchecked { balanceOf[to] += amount; } emit Transfer(address(0), to, amount); } function _burn(address from, uint256 amount) internal virtual { balanceOf[from] -= amount; // Cannot underflow because a user's balance // will never be larger than the total supply. unchecked { totalSupply -= amount; } emit Transfer(from, address(0), amount); } } ================================================ FILE: src/contracts/gho/UpgradeableGhoToken.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; import {AccessControl} from '@openzeppelin/contracts/access/AccessControl.sol'; import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol'; import {UpgradeableERC20} from './UpgradeableERC20.sol'; import {IGhoToken} from './interfaces/IGhoToken.sol'; /** * @title Upgradeable GHO Token * @author Aave Labs */ contract UpgradeableGhoToken is Initializable, UpgradeableERC20, AccessControl, IGhoToken { using EnumerableSet for EnumerableSet.AddressSet; mapping(address => Facilitator) internal _facilitators; EnumerableSet.AddressSet internal _facilitatorsList; /// @inheritdoc IGhoToken bytes32 public constant FACILITATOR_MANAGER_ROLE = keccak256('FACILITATOR_MANAGER_ROLE'); /// @inheritdoc IGhoToken bytes32 public constant BUCKET_MANAGER_ROLE = keccak256('BUCKET_MANAGER_ROLE'); /** * @dev Constructor */ constructor() UpgradeableERC20(18) { // Intentionally left bank } /** * @dev Initializer * @param admin This is the initial holder of the default admin role */ function initialize(address admin) public virtual initializer { _ERC20_init('Gho Token', 'GHO'); _grantRole(DEFAULT_ADMIN_ROLE, admin); } /// @inheritdoc IGhoToken function mint(address account, uint256 amount) external { require(amount > 0, 'INVALID_MINT_AMOUNT'); Facilitator storage f = _facilitators[msg.sender]; uint256 currentBucketLevel = f.bucketLevel; uint256 newBucketLevel = currentBucketLevel + amount; require(f.bucketCapacity >= newBucketLevel, 'FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); f.bucketLevel = uint128(newBucketLevel); _mint(account, amount); emit FacilitatorBucketLevelUpdated(msg.sender, currentBucketLevel, newBucketLevel); } /// @inheritdoc IGhoToken function burn(uint256 amount) external { require(amount > 0, 'INVALID_BURN_AMOUNT'); Facilitator storage f = _facilitators[msg.sender]; uint256 currentBucketLevel = f.bucketLevel; uint256 newBucketLevel = currentBucketLevel - amount; f.bucketLevel = uint128(newBucketLevel); _burn(msg.sender, amount); emit FacilitatorBucketLevelUpdated(msg.sender, currentBucketLevel, newBucketLevel); } /// @inheritdoc IGhoToken function addFacilitator( address facilitatorAddress, string calldata facilitatorLabel, uint128 bucketCapacity ) external onlyRole(FACILITATOR_MANAGER_ROLE) { Facilitator storage facilitator = _facilitators[facilitatorAddress]; require(bytes(facilitator.label).length == 0, 'FACILITATOR_ALREADY_EXISTS'); require(bytes(facilitatorLabel).length > 0, 'INVALID_LABEL'); facilitator.label = facilitatorLabel; facilitator.bucketCapacity = bucketCapacity; _facilitatorsList.add(facilitatorAddress); emit FacilitatorAdded( facilitatorAddress, keccak256(abi.encodePacked(facilitatorLabel)), bucketCapacity ); } /// @inheritdoc IGhoToken function removeFacilitator( address facilitatorAddress ) external onlyRole(FACILITATOR_MANAGER_ROLE) { require( bytes(_facilitators[facilitatorAddress].label).length > 0, 'FACILITATOR_DOES_NOT_EXIST' ); require( _facilitators[facilitatorAddress].bucketLevel == 0, 'FACILITATOR_BUCKET_LEVEL_NOT_ZERO' ); delete _facilitators[facilitatorAddress]; _facilitatorsList.remove(facilitatorAddress); emit FacilitatorRemoved(facilitatorAddress); } /// @inheritdoc IGhoToken function setFacilitatorBucketCapacity( address facilitator, uint128 newCapacity ) external onlyRole(BUCKET_MANAGER_ROLE) { require(bytes(_facilitators[facilitator].label).length > 0, 'FACILITATOR_DOES_NOT_EXIST'); uint256 oldCapacity = _facilitators[facilitator].bucketCapacity; _facilitators[facilitator].bucketCapacity = newCapacity; emit FacilitatorBucketCapacityUpdated(facilitator, oldCapacity, newCapacity); } /// @inheritdoc IGhoToken function getFacilitator(address facilitator) external view returns (Facilitator memory) { return _facilitators[facilitator]; } /// @inheritdoc IGhoToken function getFacilitatorBucket(address facilitator) external view returns (uint256, uint256) { return (_facilitators[facilitator].bucketCapacity, _facilitators[facilitator].bucketLevel); } /// @inheritdoc IGhoToken function getFacilitatorsList() external view returns (address[] memory) { return _facilitatorsList.values(); } } ================================================ FILE: src/contracts/gho/interfaces/IGhoFacilitator.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /** * @title IGhoFacilitator * @author Aave * @notice Defines the behavior of a Gho Facilitator */ interface IGhoFacilitator { /** * @dev Emitted when fees are distributed to the GhoTreasury * @param ghoTreasury The address of the ghoTreasury * @param asset The address of the asset transferred to the ghoTreasury * @param amount The amount of the asset transferred to the ghoTreasury */ event FeesDistributedToTreasury( address indexed ghoTreasury, address indexed asset, uint256 amount ); /** * @dev Emitted when Gho Treasury address is updated * @param oldGhoTreasury The address of the old GhoTreasury contract * @param newGhoTreasury The address of the new GhoTreasury contract */ event GhoTreasuryUpdated(address indexed oldGhoTreasury, address indexed newGhoTreasury); /** * @notice Distribute fees to the GhoTreasury */ function distributeFeesToTreasury() external; /** * @notice Updates the address of the Gho Treasury * @dev WARNING: The GhoTreasury is where revenue fees are sent to. Update carefully * @param newGhoTreasury The address of the GhoTreasury */ function updateGhoTreasury(address newGhoTreasury) external; /** * @notice Returns the address of the Gho Treasury * @return The address of the GhoTreasury contract */ function getGhoTreasury() external view returns (address); } ================================================ FILE: src/contracts/gho/interfaces/IGhoToken.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {IAccessControl} from '@openzeppelin/contracts/access/IAccessControl.sol'; /** * @title IGhoToken * @author Aave */ interface IGhoToken is IERC20, IAccessControl { struct Facilitator { uint128 bucketCapacity; uint128 bucketLevel; string label; } /** * @dev Emitted when a new facilitator is added * @param facilitatorAddress The address of the new facilitator * @param label A hashed human readable identifier for the facilitator * @param bucketCapacity The initial capacity of the facilitator's bucket */ event FacilitatorAdded( address indexed facilitatorAddress, bytes32 indexed label, uint256 bucketCapacity ); /** * @dev Emitted when a facilitator is removed * @param facilitatorAddress The address of the removed facilitator */ event FacilitatorRemoved(address indexed facilitatorAddress); /** * @dev Emitted when the bucket capacity of a facilitator is updated * @param facilitatorAddress The address of the facilitator whose bucket capacity is being changed * @param oldCapacity The old capacity of the bucket * @param newCapacity The new capacity of the bucket */ event FacilitatorBucketCapacityUpdated( address indexed facilitatorAddress, uint256 oldCapacity, uint256 newCapacity ); /** * @dev Emitted when the bucket level changed * @param facilitatorAddress The address of the facilitator whose bucket level is being changed * @param oldLevel The old level of the bucket * @param newLevel The new level of the bucket */ event FacilitatorBucketLevelUpdated( address indexed facilitatorAddress, uint256 oldLevel, uint256 newLevel ); /** * @notice Returns the identifier of the Facilitator Manager Role * @return The bytes32 id hash of the FacilitatorManager role */ function FACILITATOR_MANAGER_ROLE() external pure returns (bytes32); /** * @notice Returns the identifier of the Bucket Manager Role * @return The bytes32 id hash of the BucketManager role */ function BUCKET_MANAGER_ROLE() external pure returns (bytes32); /** * @notice Mints the requested amount of tokens to the account address. * @dev Only facilitators with enough bucket capacity available can mint. * @dev The bucket level is increased upon minting. * @param account The address receiving the GHO tokens * @param amount The amount to mint */ function mint(address account, uint256 amount) external; /** * @notice Burns the requested amount of tokens from the account address. * @dev Only active facilitators (bucket level > 0) can burn. * @dev The bucket level is decreased upon burning. * @param amount The amount to burn */ function burn(uint256 amount) external; /** * @notice Add the facilitator passed with the parameters to the facilitators list. * @dev Only accounts with `FACILITATOR_MANAGER_ROLE` role can call this function * @param facilitatorAddress The address of the facilitator to add * @param facilitatorLabel A human readable identifier for the facilitator * @param bucketCapacity The upward limit of GHO can be minted by the facilitator */ function addFacilitator( address facilitatorAddress, string calldata facilitatorLabel, uint128 bucketCapacity ) external; /** * @notice Remove the facilitator from the facilitators list. * @dev Only accounts with `FACILITATOR_MANAGER_ROLE` role can call this function * @param facilitatorAddress The address of the facilitator to remove */ function removeFacilitator(address facilitatorAddress) external; /** * @notice Set the bucket capacity of the facilitator. * @dev Only accounts with `BUCKET_MANAGER_ROLE` role can call this function * @param facilitator The address of the facilitator * @param newCapacity The new capacity of the bucket */ function setFacilitatorBucketCapacity(address facilitator, uint128 newCapacity) external; /** * @notice Returns the facilitator data * @param facilitator The address of the facilitator * @return The facilitator configuration */ function getFacilitator(address facilitator) external view returns (Facilitator memory); /** * @notice Returns the bucket configuration of the facilitator * @param facilitator The address of the facilitator * @return The capacity of the facilitator's bucket * @return The level of the facilitator's bucket */ function getFacilitatorBucket(address facilitator) external view returns (uint256, uint256); /** * @notice Returns the list of the addresses of the active facilitator * @return The list of the facilitators addresses */ function getFacilitatorsList() external view returns (address[] memory); } ================================================ FILE: src/contracts/misc/GhoAaveSteward.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; import {IPoolDataProvider} from '@aave/core-v3/contracts/interfaces/IPoolDataProvider.sol'; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; import {DataTypes} from '@aave/core-v3/contracts/protocol/libraries/types/DataTypes.sol'; import {ReserveConfiguration} from '@aave/core-v3/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; import {IPoolConfigurator, IDefaultInterestRateStrategyV2} from './dependencies/AaveV3-1.sol'; import {IGhoAaveSteward} from './interfaces/IGhoAaveSteward.sol'; import {RiskCouncilControlled} from './RiskCouncilControlled.sol'; /** * @title GhoAaveSteward * @author Aave Labs * @notice Helper contract for managing parameters of the GHO reserve * @dev Only the Risk Council is able to action contract's functions, based on specific conditions that have been agreed upon with the community. * @dev Requires role RiskAdmin on the Aave V3 Ethereum Pool */ contract GhoAaveSteward is Ownable, RiskCouncilControlled, IGhoAaveSteward { using ReserveConfiguration for DataTypes.ReserveConfigurationMap; uint256 internal constant BPS_MAX = 100_00; /// @inheritdoc IGhoAaveSteward address public immutable POOL_DATA_PROVIDER; /// @inheritdoc IGhoAaveSteward uint256 public constant MINIMUM_DELAY = 1 days; /// @inheritdoc IGhoAaveSteward address public immutable POOL_ADDRESSES_PROVIDER; /// @inheritdoc IGhoAaveSteward address public immutable GHO_TOKEN; BorrowRateConfig internal _borrowRateConfig; GhoDebounce internal _ghoTimelocks; /** * @dev Only methods that are not timelocked can be called if marked by this modifier. */ modifier notTimelocked(uint40 timelock) { require(block.timestamp - timelock > MINIMUM_DELAY, 'DEBOUNCE_NOT_RESPECTED'); _; } /** * @dev Constructor * @param owner The address of the contract's owner * @param addressesProvider The address of the PoolAddressesProvider of Aave V3 Ethereum Pool * @param poolDataProvider The pool data provider of the pool to be controlled by the steward * @param ghoToken The address of the GhoToken * @param riskCouncil The address of the risk council * @param borrowRateConfig The configuration conditions for GHO borrow rate changes */ constructor( address owner, address addressesProvider, address poolDataProvider, address ghoToken, address riskCouncil, BorrowRateConfig memory borrowRateConfig ) RiskCouncilControlled(riskCouncil) { require(owner != address(0), 'INVALID_OWNER'); require(addressesProvider != address(0), 'INVALID_ADDRESSES_PROVIDER'); require(poolDataProvider != address(0), 'INVALID_DATA_PROVIDER'); require(ghoToken != address(0), 'INVALID_GHO_TOKEN'); POOL_ADDRESSES_PROVIDER = addressesProvider; POOL_DATA_PROVIDER = poolDataProvider; GHO_TOKEN = ghoToken; _borrowRateConfig = borrowRateConfig; _transferOwnership(owner); } /// @inheritdoc IGhoAaveSteward function updateGhoBorrowRate( uint16 optimalUsageRatio, uint32 baseVariableBorrowRate, uint32 variableRateSlope1, uint32 variableRateSlope2 ) external onlyRiskCouncil notTimelocked(_ghoTimelocks.ghoBorrowRateLastUpdate) { IDefaultInterestRateStrategyV2.InterestRateData memory rateParams = IDefaultInterestRateStrategyV2.InterestRateData({ optimalUsageRatio: optimalUsageRatio, baseVariableBorrowRate: baseVariableBorrowRate, variableRateSlope1: variableRateSlope1, variableRateSlope2: variableRateSlope2 }); _validateRatesUpdate(rateParams); _ghoTimelocks.ghoBorrowRateLastUpdate = uint40(block.timestamp); IPoolConfigurator(IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPoolConfigurator()) .setReserveInterestRateData(GHO_TOKEN, abi.encode(rateParams)); } /// @inheritdoc IGhoAaveSteward function updateGhoBorrowCap( uint256 newBorrowCap ) external onlyRiskCouncil notTimelocked(_ghoTimelocks.ghoBorrowCapLastUpdate) { DataTypes.ReserveConfigurationMap memory configuration = IPool( IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPool() ).getConfiguration(GHO_TOKEN); uint256 currentBorrowCap = configuration.getBorrowCap(); require(newBorrowCap != currentBorrowCap, 'NO_CHANGE_IN_BORROW_CAP'); require( _isDifferenceLowerThanMax(currentBorrowCap, newBorrowCap, currentBorrowCap), 'INVALID_BORROW_CAP_UPDATE' ); _ghoTimelocks.ghoBorrowCapLastUpdate = uint40(block.timestamp); IPoolConfigurator(IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPoolConfigurator()) .setBorrowCap(GHO_TOKEN, newBorrowCap); } /// @inheritdoc IGhoAaveSteward function updateGhoSupplyCap( uint256 newSupplyCap ) external onlyRiskCouncil notTimelocked(_ghoTimelocks.ghoSupplyCapLastUpdate) { DataTypes.ReserveConfigurationMap memory configuration = IPool( IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPool() ).getConfiguration(GHO_TOKEN); uint256 currentSupplyCap = configuration.getSupplyCap(); require(newSupplyCap != currentSupplyCap, 'NO_CHANGE_IN_SUPPLY_CAP'); require( _isDifferenceLowerThanMax(currentSupplyCap, newSupplyCap, currentSupplyCap), 'INVALID_SUPPLY_CAP_UPDATE' ); _ghoTimelocks.ghoSupplyCapLastUpdate = uint40(block.timestamp); IPoolConfigurator(IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPoolConfigurator()) .setSupplyCap(GHO_TOKEN, newSupplyCap); } /// @inheritdoc IGhoAaveSteward function setBorrowRateConfig( uint16 optimalUsageRatioMaxChange, uint32 baseVariableBorrowRateMaxChange, uint32 variableRateSlope1MaxChange, uint32 variableRateSlope2MaxChange ) external onlyOwner { _borrowRateConfig.optimalUsageRatioMaxChange = optimalUsageRatioMaxChange; _borrowRateConfig.baseVariableBorrowRateMaxChange = baseVariableBorrowRateMaxChange; _borrowRateConfig.variableRateSlope1MaxChange = variableRateSlope1MaxChange; _borrowRateConfig.variableRateSlope2MaxChange = variableRateSlope2MaxChange; } /// @inheritdoc IGhoAaveSteward function getBorrowRateConfig() external view returns (BorrowRateConfig memory) { return _borrowRateConfig; } /// @inheritdoc IGhoAaveSteward function getGhoTimelocks() external view returns (GhoDebounce memory) { return _ghoTimelocks; } /// @inheritdoc IGhoAaveSteward function RISK_COUNCIL() public view override returns (address) { return _riskCouncil; } /** * @dev Validates the interest rates update * @param newRates The new interest rate data */ function _validateRatesUpdate( IDefaultInterestRateStrategyV2.InterestRateData memory newRates ) internal view { address rateStrategyAddress = IPoolDataProvider(POOL_DATA_PROVIDER) .getInterestRateStrategyAddress(GHO_TOKEN); IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = IDefaultInterestRateStrategyV2(rateStrategyAddress) .getInterestRateDataBps(GHO_TOKEN); require( newRates.optimalUsageRatio != currentRates.optimalUsageRatio || newRates.baseVariableBorrowRate != currentRates.baseVariableBorrowRate || newRates.variableRateSlope1 != currentRates.variableRateSlope1 || newRates.variableRateSlope2 != currentRates.variableRateSlope2, 'NO_CHANGE_IN_RATES' ); require( _updateWithinAllowedRange( currentRates.optimalUsageRatio, newRates.optimalUsageRatio, _borrowRateConfig.optimalUsageRatioMaxChange, false ), 'INVALID_OPTIMAL_USAGE_RATIO' ); require( _updateWithinAllowedRange( currentRates.baseVariableBorrowRate, newRates.baseVariableBorrowRate, _borrowRateConfig.baseVariableBorrowRateMaxChange, false ), 'INVALID_BORROW_RATE_UPDATE' ); require( _updateWithinAllowedRange( currentRates.variableRateSlope1, newRates.variableRateSlope1, _borrowRateConfig.variableRateSlope1MaxChange, false ), 'INVALID_VARIABLE_RATE_SLOPE1' ); require( _updateWithinAllowedRange( currentRates.variableRateSlope2, newRates.variableRateSlope2, _borrowRateConfig.variableRateSlope2MaxChange, false ), 'INVALID_VARIABLE_RATE_SLOPE2' ); } /** * @dev Ensures that the change difference is lower than max. * @param from current value * @param to new value * @param max maximum difference between from and to * @return bool true if difference between values lower than max, false otherwise */ function _isDifferenceLowerThanMax( uint256 from, uint256 to, uint256 max ) internal pure returns (bool) { return from < to ? to - from <= max : from - to <= max; } /** * @notice Ensures the risk param update is within the allowed range * @param from current risk param value * @param to new updated risk param value * @param maxPercentChange the max percent change allowed * @param isChangeRelative true, if maxPercentChange is relative in value, false if maxPercentChange * is absolute in value. * @return bool true, if difference is within the maxPercentChange */ function _updateWithinAllowedRange( uint256 from, uint256 to, uint256 maxPercentChange, bool isChangeRelative ) internal pure returns (bool) { // diff denotes the difference between the from and to values, ensuring it is a positive value always uint256 diff = from > to ? from - to : to - from; // maxDiff denotes the max permitted difference on both the upper and lower bounds, if the maxPercentChange is relative in value // we calculate the max permitted difference using the maxPercentChange and the from value, otherwise if the maxPercentChange is absolute in value // the max permitted difference is the maxPercentChange itself uint256 maxDiff = isChangeRelative ? (maxPercentChange * from) / BPS_MAX : maxPercentChange; if (diff > maxDiff) return false; return true; } } ================================================ FILE: src/contracts/misc/GhoBucketSteward.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; import {IGhoToken} from '../gho/interfaces/IGhoToken.sol'; import {RiskCouncilControlled} from './RiskCouncilControlled.sol'; import {IGhoBucketSteward} from './interfaces/IGhoBucketSteward.sol'; /** * @title GhoBucketSteward * @author Aave Labs * @notice Helper contract for managing bucket capacities of controlled facilitators * @dev Only the Risk Council is able to action contract's functions, based on specific conditions that have been agreed upon with the community. * @dev Requires role GHO_TOKEN_BUCKET_MANAGER_ROLE on GhoToken */ contract GhoBucketSteward is Ownable, RiskCouncilControlled, IGhoBucketSteward { using EnumerableSet for EnumerableSet.AddressSet; /// @inheritdoc IGhoBucketSteward uint256 public constant MINIMUM_DELAY = 1 days; /// @inheritdoc IGhoBucketSteward address public immutable GHO_TOKEN; mapping(address => uint40) internal _facilitatorsBucketCapacityTimelocks; mapping(address => bool) internal _controlledFacilitatorsByAddress; EnumerableSet.AddressSet internal _controlledFacilitators; /** * @dev Only methods that are not timelocked can be called if marked by this modifier. */ modifier notTimelocked(uint40 timelock) { require(block.timestamp - timelock > MINIMUM_DELAY, 'DEBOUNCE_NOT_RESPECTED'); _; } /** * @dev Constructor * @param owner The address of the contract's owner * @param ghoToken The address of the GhoToken * @param riskCouncil The address of the risk council */ constructor( address owner, address ghoToken, address riskCouncil ) RiskCouncilControlled(riskCouncil) { require(owner != address(0), 'INVALID_OWNER'); require(ghoToken != address(0), 'INVALID_GHO_TOKEN'); GHO_TOKEN = ghoToken; _transferOwnership(owner); } /// @inheritdoc IGhoBucketSteward function updateFacilitatorBucketCapacity( address facilitator, uint128 newBucketCapacity ) external onlyRiskCouncil notTimelocked(_facilitatorsBucketCapacityTimelocks[facilitator]) { require(_controlledFacilitatorsByAddress[facilitator], 'FACILITATOR_NOT_CONTROLLED'); (uint256 currentBucketCapacity, ) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(facilitator); require(newBucketCapacity != currentBucketCapacity, 'NO_CHANGE_IN_BUCKET_CAPACITY'); require( _isIncreaseLowerThanMax(currentBucketCapacity, newBucketCapacity, currentBucketCapacity), 'INVALID_BUCKET_CAPACITY_UPDATE' ); _facilitatorsBucketCapacityTimelocks[facilitator] = uint40(block.timestamp); IGhoToken(GHO_TOKEN).setFacilitatorBucketCapacity(facilitator, newBucketCapacity); } /// @inheritdoc IGhoBucketSteward function setControlledFacilitator( address[] memory facilitatorList, bool approve ) external onlyOwner { for (uint256 i = 0; i < facilitatorList.length; i++) { _controlledFacilitatorsByAddress[facilitatorList[i]] = approve; if (approve) { _controlledFacilitators.add(facilitatorList[i]); } else { _controlledFacilitators.remove(facilitatorList[i]); } } } /// @inheritdoc IGhoBucketSteward function getControlledFacilitators() external view returns (address[] memory) { return _controlledFacilitators.values(); } /// @inheritdoc IGhoBucketSteward function isControlledFacilitator(address facilitator) external view returns (bool) { return _controlledFacilitatorsByAddress[facilitator]; } /// @inheritdoc IGhoBucketSteward function getFacilitatorBucketCapacityTimelock( address facilitator ) external view returns (uint40) { return _facilitatorsBucketCapacityTimelocks[facilitator]; } /// @inheritdoc IGhoBucketSteward function RISK_COUNCIL() public view override returns (address) { return _riskCouncil; } /** * @dev Ensures that the change is positive and the difference is lower than max. * @param from current value * @param to new value * @param max maximum difference between from and to * @return bool true if difference between values is positive and lower than max, false otherwise */ function _isIncreaseLowerThanMax( uint256 from, uint256 to, uint256 max ) internal pure returns (bool) { return to >= from && to - from <= max; } } ================================================ FILE: src/contracts/misc/GhoCcipSteward.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {IUpgradeableLockReleaseTokenPool, RateLimiter} from './dependencies/Ccip.sol'; import {IGhoCcipSteward} from './interfaces/IGhoCcipSteward.sol'; import {RiskCouncilControlled} from './RiskCouncilControlled.sol'; /** * @title GhoCcipSteward * @author Aave Labs * @notice Helper contract for managing parameters of the CCIP token pools * @dev Only the Risk Council is able to action contract's functions, based on specific conditions that have been agreed upon with the community. * @dev Requires roles RateLimitAdmin and BridgeLimitAdmin (if on Ethereum) on GhoTokenPool */ contract GhoCcipSteward is RiskCouncilControlled, IGhoCcipSteward { /// @inheritdoc IGhoCcipSteward uint256 public constant MINIMUM_DELAY = 1 days; /// @inheritdoc IGhoCcipSteward address public immutable GHO_TOKEN; /// @inheritdoc IGhoCcipSteward address public immutable GHO_TOKEN_POOL; /// @inheritdoc IGhoCcipSteward bool public immutable BRIDGE_LIMIT_ENABLED; CcipDebounce internal _ccipTimelocks; /** * @dev Only methods that are not timelocked can be called if marked by this modifier. */ modifier notTimelocked(uint40 timelock) { require(block.timestamp - timelock > MINIMUM_DELAY, 'DEBOUNCE_NOT_RESPECTED'); _; } /** * @dev Constructor * @param ghoToken The address of the GhoToken * @param ghoTokenPool The address of the Gho CCIP Token Pool * @param riskCouncil The address of the risk council * @param bridgeLimitEnabled Whether the bridge limit feature is supported in the GhoTokenPool */ constructor( address ghoToken, address ghoTokenPool, address riskCouncil, bool bridgeLimitEnabled ) RiskCouncilControlled(riskCouncil) { require(ghoToken != address(0), 'INVALID_GHO_TOKEN'); require(ghoTokenPool != address(0), 'INVALID_GHO_TOKEN_POOL'); GHO_TOKEN = ghoToken; GHO_TOKEN_POOL = ghoTokenPool; BRIDGE_LIMIT_ENABLED = bridgeLimitEnabled; } /// @inheritdoc IGhoCcipSteward function updateBridgeLimit( uint256 newBridgeLimit ) external onlyRiskCouncil notTimelocked(_ccipTimelocks.bridgeLimitLastUpdate) { require(BRIDGE_LIMIT_ENABLED, 'BRIDGE_LIMIT_DISABLED'); uint256 currentBridgeLimit = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).getBridgeLimit(); require(newBridgeLimit != currentBridgeLimit, 'NO_CHANGE_IN_BRIDGE_LIMIT'); require( _isDifferenceLowerThanMax(currentBridgeLimit, newBridgeLimit, currentBridgeLimit), 'INVALID_BRIDGE_LIMIT_UPDATE' ); _ccipTimelocks.bridgeLimitLastUpdate = uint40(block.timestamp); IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).setBridgeLimit(newBridgeLimit); } /// @inheritdoc IGhoCcipSteward function updateRateLimit( uint64 remoteChainSelector, bool outboundEnabled, uint128 outboundCapacity, uint128 outboundRate, bool inboundEnabled, uint128 inboundCapacity, uint128 inboundRate ) external onlyRiskCouncil notTimelocked(_ccipTimelocks.rateLimitLastUpdate) { RateLimiter.TokenBucket memory outboundConfig = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) .getCurrentOutboundRateLimiterState(remoteChainSelector); RateLimiter.TokenBucket memory inboundConfig = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) .getCurrentInboundRateLimiterState(remoteChainSelector); require( outboundEnabled != outboundConfig.isEnabled || outboundCapacity != outboundConfig.capacity || outboundRate != outboundConfig.rate || inboundEnabled != inboundConfig.isEnabled || inboundCapacity != inboundConfig.capacity || inboundRate != inboundConfig.rate, 'NO_CHANGE_IN_RATE_LIMIT' ); require( _isDifferenceLowerThanMax(outboundConfig.capacity, outboundCapacity, outboundConfig.capacity), 'INVALID_RATE_LIMIT_UPDATE' ); require( _isDifferenceLowerThanMax(outboundConfig.rate, outboundRate, outboundConfig.rate), 'INVALID_RATE_LIMIT_UPDATE' ); require( _isDifferenceLowerThanMax(inboundConfig.capacity, inboundCapacity, inboundConfig.capacity), 'INVALID_RATE_LIMIT_UPDATE' ); require( _isDifferenceLowerThanMax(inboundConfig.rate, inboundRate, inboundConfig.rate), 'INVALID_RATE_LIMIT_UPDATE' ); _ccipTimelocks.rateLimitLastUpdate = uint40(block.timestamp); IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).setChainRateLimiterConfig( remoteChainSelector, RateLimiter.Config({ isEnabled: outboundEnabled, capacity: outboundCapacity, rate: outboundRate }), RateLimiter.Config({isEnabled: inboundEnabled, capacity: inboundCapacity, rate: inboundRate}) ); } /// @inheritdoc IGhoCcipSteward function getCcipTimelocks() external view override returns (CcipDebounce memory) { return _ccipTimelocks; } /// @inheritdoc IGhoCcipSteward function RISK_COUNCIL() external view override returns (address) { return _riskCouncil; } /** * @dev Ensures that the change difference is lower than max. * @param from current value * @param to new value * @param max maximum difference between from and to * @return bool true if difference between values lower than max, false otherwise */ function _isDifferenceLowerThanMax( uint256 from, uint256 to, uint256 max ) internal pure returns (bool) { return from < to ? to - from <= max : from - to <= max; } } ================================================ FILE: src/contracts/misc/GhoGsmSteward.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {IGsm} from '../facilitators/gsm/interfaces/IGsm.sol'; import {IGsmFeeStrategy} from '../facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol'; import {IFixedFeeStrategyFactory} from '../facilitators/gsm/feeStrategy/interfaces/IFixedFeeStrategyFactory.sol'; import {IGhoGsmSteward} from './interfaces/IGhoGsmSteward.sol'; import {RiskCouncilControlled} from './RiskCouncilControlled.sol'; /** * @title GhoGsmSteward * @author Aave Labs * @notice Helper contract for managing parameters of the GSM * @dev Only the Risk Council is able to action contract's functions, based on specific conditions that have been agreed upon with the community. * @dev Requires role GSM_CONFIGURATOR_ROLE on every GSM contract to be managed */ contract GhoGsmSteward is RiskCouncilControlled, IGhoGsmSteward { /// @inheritdoc IGhoGsmSteward uint256 public constant GSM_FEE_RATE_CHANGE_MAX = 0.0050e4; // 0.50% /// @inheritdoc IGhoGsmSteward uint256 public constant MINIMUM_DELAY = 1 days; /// @inheritdoc IGhoGsmSteward address public immutable FIXED_FEE_STRATEGY_FACTORY; mapping(address => GsmDebounce) internal _gsmTimelocksByAddress; /** * @dev Only methods that are not timelocked can be called if marked by this modifier. */ modifier notTimelocked(uint40 timelock) { require(block.timestamp - timelock > MINIMUM_DELAY, 'DEBOUNCE_NOT_RESPECTED'); _; } /** * @dev Constructor * @param fixedFeeStrategyFactory The address of the Fixed Fee Strategy Factory * @param riskCouncil The address of the risk council */ constructor( address fixedFeeStrategyFactory, address riskCouncil ) RiskCouncilControlled(riskCouncil) { require(fixedFeeStrategyFactory != address(0), 'INVALID_FIXED_FEE_STRATEGY_FACTORY'); FIXED_FEE_STRATEGY_FACTORY = fixedFeeStrategyFactory; } /// @inheritdoc IGhoGsmSteward function updateGsmExposureCap( address gsm, uint128 newExposureCap ) external onlyRiskCouncil notTimelocked(_gsmTimelocksByAddress[gsm].gsmExposureCapLastUpdated) { uint128 currentExposureCap = IGsm(gsm).getExposureCap(); require(newExposureCap != currentExposureCap, 'NO_CHANGE_IN_EXPOSURE_CAP'); require( _isDifferenceLowerThanMax(currentExposureCap, newExposureCap, currentExposureCap), 'INVALID_EXPOSURE_CAP_UPDATE' ); _gsmTimelocksByAddress[gsm].gsmExposureCapLastUpdated = uint40(block.timestamp); IGsm(gsm).updateExposureCap(newExposureCap); } /// @inheritdoc IGhoGsmSteward function updateGsmBuySellFees( address gsm, uint256 buyFee, uint256 sellFee ) external onlyRiskCouncil notTimelocked(_gsmTimelocksByAddress[gsm].gsmFeeStrategyLastUpdated) { address currentFeeStrategy = IGsm(gsm).getFeeStrategy(); require(currentFeeStrategy != address(0), 'FIXED_FEE_STRATEGY_NOT_FOUND'); uint256 currentBuyFee = IGsmFeeStrategy(currentFeeStrategy).getBuyFee(1e4); uint256 currentSellFee = IGsmFeeStrategy(currentFeeStrategy).getSellFee(1e4); require(buyFee != currentBuyFee || sellFee != currentSellFee, 'NO_CHANGE_IN_FEES'); require( _isDifferenceLowerThanMax(currentBuyFee, buyFee, GSM_FEE_RATE_CHANGE_MAX), 'INVALID_BUY_FEE_UPDATE' ); require( _isDifferenceLowerThanMax(currentSellFee, sellFee, GSM_FEE_RATE_CHANGE_MAX), 'INVALID_SELL_FEE_UPDATE' ); IFixedFeeStrategyFactory strategyFactory = IFixedFeeStrategyFactory(FIXED_FEE_STRATEGY_FACTORY); uint256[] memory buyFeeList = new uint256[](1); uint256[] memory sellFeeList = new uint256[](1); buyFeeList[0] = buyFee; sellFeeList[0] = sellFee; address strategy = strategyFactory.createStrategies(buyFeeList, sellFeeList)[0]; _gsmTimelocksByAddress[gsm].gsmFeeStrategyLastUpdated = uint40(block.timestamp); IGsm(gsm).updateFeeStrategy(strategy); } /// @inheritdoc IGhoGsmSteward function getGsmTimelocks(address gsm) external view returns (GsmDebounce memory) { return _gsmTimelocksByAddress[gsm]; } /// @inheritdoc IGhoGsmSteward function RISK_COUNCIL() public view override returns (address) { return _riskCouncil; } /** * @dev Ensures that the change difference is lower than max. * @param from current value * @param to new value * @param max maximum difference between from and to * @return bool true if difference between values lower than max, false otherwise */ function _isDifferenceLowerThanMax( uint256 from, uint256 to, uint256 max ) internal pure returns (bool) { return from < to ? to - from <= max : from - to <= max; } } ================================================ FILE: src/contracts/misc/RiskCouncilControlled.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; /** * @title RiskCouncilControlled * @author Aave Labs * @notice Helper contract for controlling access to Steward and other functions restricted to Risk Council */ abstract contract RiskCouncilControlled { address internal immutable _riskCouncil; /** * @dev Constructor * @param riskCouncil The address of the risk council */ constructor(address riskCouncil) { require(riskCouncil != address(0), 'INVALID_RISK_COUNCIL'); _riskCouncil = riskCouncil; } /** * @dev Only Risk Council can call functions marked by this modifier. */ modifier onlyRiskCouncil() { require(_riskCouncil == msg.sender, 'INVALID_CALLER'); _; } } ================================================ FILE: src/contracts/misc/dependencies/AaveV3-1.sol ================================================ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; import {Address} from 'solidity-utils/contracts/oz-common/Address.sol'; import {SafeCast} from '@openzeppelin/contracts/utils/math/SafeCast.sol'; import {IERC165} from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; import {ConfiguratorInputTypes} from '@aave/core-v3/contracts/protocol/libraries/types/ConfiguratorInputTypes.sol'; library DataTypes { struct CalculateInterestRatesParams { uint256 unbacked; uint256 liquidityAdded; uint256 liquidityTaken; uint256 totalStableDebt; uint256 totalVariableDebt; uint256 averageStableBorrowRate; uint256 reserveFactor; address reserve; bool usingVirtualBalance; uint256 virtualUnderlyingBalance; } } /** * @title Errors library * @author Aave * @notice Defines the error messages emitted by the different contracts of the Aave protocol */ library Errors { string public constant CALLER_NOT_POOL_CONFIGURATOR = '10'; // 'The caller of the function is not the pool configurator' string public constant INVALID_ADDRESSES_PROVIDER = '12'; // 'The address of the pool addresses provider is invalid' string public constant ZERO_ADDRESS_NOT_VALID = '77'; // 'Zero address not valid' string public constant INVALID_OPTIMAL_USAGE_RATIO = '83'; // 'Invalid optimal usage ratio' string public constant INVALID_MAX_RATE = '92'; // The expect maximum borrow rate is invalid string public constant SLOPE_2_MUST_BE_GTE_SLOPE_1 = '95'; // Variable interest rate slope 2 can not be lower than slope 1 } /** * @title PercentageMath library * @author Aave * @notice Provides functions to perform percentage calculations * @dev Percentages are defined by default with 2 decimals of precision (100.00). The precision is indicated by PERCENTAGE_FACTOR * @dev Operations are rounded. If a value is >=.5, will be rounded up, otherwise rounded down. */ library PercentageMath { // Maximum percentage factor (100.00%) uint256 internal constant PERCENTAGE_FACTOR = 1e4; // Half percentage factor (50.00%) uint256 internal constant HALF_PERCENTAGE_FACTOR = 0.5e4; /** * @notice Executes a percentage multiplication * @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328 * @param value The value of which the percentage needs to be calculated * @param percentage The percentage of the value to be calculated * @return result value percentmul percentage */ function percentMul(uint256 value, uint256 percentage) internal pure returns (uint256 result) { // to avoid overflow, value <= (type(uint256).max - HALF_PERCENTAGE_FACTOR) / percentage assembly { if iszero( or( iszero(percentage), iszero(gt(value, div(sub(not(0), HALF_PERCENTAGE_FACTOR), percentage))) ) ) { revert(0, 0) } result := div(add(mul(value, percentage), HALF_PERCENTAGE_FACTOR), PERCENTAGE_FACTOR) } } /** * @notice Executes a percentage division * @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328 * @param value The value of which the percentage needs to be calculated * @param percentage The percentage of the value to be calculated * @return result value percentdiv percentage */ function percentDiv(uint256 value, uint256 percentage) internal pure returns (uint256 result) { // to avoid overflow, value <= (type(uint256).max - halfPercentage) / PERCENTAGE_FACTOR assembly { if or( iszero(percentage), iszero(iszero(gt(value, div(sub(not(0), div(percentage, 2)), PERCENTAGE_FACTOR)))) ) { revert(0, 0) } result := div(add(mul(value, PERCENTAGE_FACTOR), div(percentage, 2)), percentage) } } } /** * @title WadRayMath library * @author Aave * @notice Provides functions to perform calculations with Wad and Ray units * @dev Provides mul and div function for wads (decimal numbers with 18 digits of precision) and rays (decimal numbers * with 27 digits of precision) * @dev Operations are rounded. If a value is >=.5, will be rounded up, otherwise rounded down. */ library WadRayMath { // HALF_WAD and HALF_RAY expressed with extended notation as constant with operations are not supported in Yul assembly uint256 internal constant WAD = 1e18; uint256 internal constant HALF_WAD = 0.5e18; uint256 internal constant RAY = 1e27; uint256 internal constant HALF_RAY = 0.5e27; uint256 internal constant WAD_RAY_RATIO = 1e9; /** * @dev Multiplies two wad, rounding half up to the nearest wad * @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328 * @param a Wad * @param b Wad * @return c = a*b, in wad */ function wadMul(uint256 a, uint256 b) internal pure returns (uint256 c) { // to avoid overflow, a <= (type(uint256).max - HALF_WAD) / b assembly { if iszero(or(iszero(b), iszero(gt(a, div(sub(not(0), HALF_WAD), b))))) { revert(0, 0) } c := div(add(mul(a, b), HALF_WAD), WAD) } } /** * @dev Divides two wad, rounding half up to the nearest wad * @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328 * @param a Wad * @param b Wad * @return c = a/b, in wad */ function wadDiv(uint256 a, uint256 b) internal pure returns (uint256 c) { // to avoid overflow, a <= (type(uint256).max - halfB) / WAD assembly { if or(iszero(b), iszero(iszero(gt(a, div(sub(not(0), div(b, 2)), WAD))))) { revert(0, 0) } c := div(add(mul(a, WAD), div(b, 2)), b) } } /** * @notice Multiplies two ray, rounding half up to the nearest ray * @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328 * @param a Ray * @param b Ray * @return c = a raymul b */ function rayMul(uint256 a, uint256 b) internal pure returns (uint256 c) { // to avoid overflow, a <= (type(uint256).max - HALF_RAY) / b assembly { if iszero(or(iszero(b), iszero(gt(a, div(sub(not(0), HALF_RAY), b))))) { revert(0, 0) } c := div(add(mul(a, b), HALF_RAY), RAY) } } /** * @notice Divides two ray, rounding half up to the nearest ray * @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328 * @param a Ray * @param b Ray * @return c = a raydiv b */ function rayDiv(uint256 a, uint256 b) internal pure returns (uint256 c) { // to avoid overflow, a <= (type(uint256).max - halfB) / RAY assembly { if or(iszero(b), iszero(iszero(gt(a, div(sub(not(0), div(b, 2)), RAY))))) { revert(0, 0) } c := div(add(mul(a, RAY), div(b, 2)), b) } } /** * @dev Casts ray down to wad * @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328 * @param a Ray * @return b = a converted to wad, rounded half up to the nearest wad */ function rayToWad(uint256 a) internal pure returns (uint256 b) { assembly { b := div(a, WAD_RAY_RATIO) let remainder := mod(a, WAD_RAY_RATIO) if iszero(lt(remainder, div(WAD_RAY_RATIO, 2))) { b := add(b, 1) } } } /** * @dev Converts wad up to ray * @dev assembly optimized for improved gas savings, see https://twitter.com/transmissions11/status/1451131036377571328 * @param a Wad * @return b = a converted in ray */ function wadToRay(uint256 a) internal pure returns (uint256 b) { // to avoid overflow, b/WAD_RAY_RATIO == a assembly { b := mul(a, WAD_RAY_RATIO) if iszero(eq(div(b, WAD_RAY_RATIO), a)) { revert(0, 0) } } } } /// @notice This interface contains the only ARM-related functions that might be used on-chain by other CCIP contracts. interface IARM { /// @notice A Merkle root tagged with the address of the commit store contract it is destined for. struct TaggedRoot { address commitStore; bytes32 root; } /// @notice Callers MUST NOT cache the return value as a blessed tagged root could become unblessed. function isBlessed(TaggedRoot calldata taggedRoot) external view returns (bool); /// @notice When the ARM is "cursed", CCIP pauses until the curse is lifted. function isCursed() external view returns (bool); } /** * @title IPoolConfigurator * @author Aave * @notice Defines the basic interface for a Pool configurator. * @dev Reduced interface from https://github.com/aave-dao/aave-v3-origin/blob/main/src/core/contracts/interfaces/IPoolConfigurator.sol */ interface IPoolConfigurator { /** * @notice Sets interest rate data for a reserve * @param asset The address of the underlying asset of the reserve * @param rateData bytes-encoded rate data. In this format in order to allow the rate strategy contract * to de-structure custom data */ function setReserveInterestRateData(address asset, bytes calldata rateData) external; /** * @notice Updates the borrow cap of a reserve. * @param asset The address of the underlying asset of the reserve * @param newBorrowCap The new borrow cap of the reserve */ function setBorrowCap(address asset, uint256 newBorrowCap) external; /** * @notice Updates the supply cap of a reserve. * @param asset The address of the underlying asset of the reserve * @param newSupplyCap The new supply cap of the reserve */ function setSupplyCap(address asset, uint256 newSupplyCap) external; } /** * @title IReserveInterestRateStrategy * @author BGD Labs * @notice Basic interface for any rate strategy used by the Aave protocol */ interface IReserveInterestRateStrategy { /** * @notice Sets interest rate data for an Aave rate strategy * @param reserve The reserve to update * @param rateData The abi encoded reserve interest rate data to apply to the given reserve * Abstracted this way as rate strategies can be custom */ function setInterestRateParams(address reserve, bytes calldata rateData) external; /** * @notice Calculates the interest rates depending on the reserve's state and configurations * @param params The parameters needed to calculate interest rates * @return liquidityRate The liquidity rate expressed in ray * @return stableBorrowRate The stable borrow rate expressed in ray * @return variableBorrowRate The variable borrow rate expressed in ray */ function calculateInterestRates( DataTypes.CalculateInterestRatesParams memory params ) external view returns (uint256, uint256, uint256); } /** * @title IDefaultInterestRateStrategyV2 * @author BGD Labs * @notice Interface of the default interest rate strategy used by the Aave protocol */ interface IDefaultInterestRateStrategyV2 is IReserveInterestRateStrategy { struct CalcInterestRatesLocalVars { uint256 availableLiquidity; uint256 totalDebt; uint256 currentVariableBorrowRate; uint256 currentLiquidityRate; uint256 borrowUsageRatio; uint256 supplyUsageRatio; uint256 availableLiquidityPlusDebt; } /** * @notice Holds the interest rate data for a given reserve * * @dev Since values are in bps, they are multiplied by 1e23 in order to become rays with 27 decimals. This * in turn means that the maximum supported interest rate is 4294967295 (2**32-1) bps or 42949672.95%. * * @param optimalUsageRatio The optimal usage ratio, in bps * @param baseVariableBorrowRate The base variable borrow rate, in bps * @param variableRateSlope1 The slope of the variable interest curve, before hitting the optimal ratio, in bps * @param variableRateSlope2 The slope of the variable interest curve, after hitting the optimal ratio, in bps */ struct InterestRateData { uint16 optimalUsageRatio; uint32 baseVariableBorrowRate; uint32 variableRateSlope1; uint32 variableRateSlope2; } /** * @notice The interest rate data, where all values are in ray (fixed-point 27 decimal numbers) for a given reserve, * used in in-memory calculations. * * @param optimalUsageRatio The optimal usage ratio * @param baseVariableBorrowRate The base variable borrow rate * @param variableRateSlope1 The slope of the variable interest curve, before hitting the optimal ratio * @param variableRateSlope2 The slope of the variable interest curve, after hitting the optimal ratio */ struct InterestRateDataRay { uint256 optimalUsageRatio; uint256 baseVariableBorrowRate; uint256 variableRateSlope1; uint256 variableRateSlope2; } /** * @notice emitted when new interest rate data is set in a reserve * * @param reserve address of the reserve that has new interest rate data set * @param optimalUsageRatio The optimal usage ratio, in bps * @param baseVariableBorrowRate The base variable borrow rate, in bps * @param variableRateSlope1 The slope of the variable interest curve, before hitting the optimal ratio, in bps * @param variableRateSlope2 The slope of the variable interest curve, after hitting the optimal ratio, in bps */ event RateDataUpdate( address indexed reserve, uint256 optimalUsageRatio, uint256 baseVariableBorrowRate, uint256 variableRateSlope1, uint256 variableRateSlope2 ); /** * @notice Returns the address of the PoolAddressesProvider * @return The address of the PoolAddressesProvider contract */ function ADDRESSES_PROVIDER() external view returns (IPoolAddressesProvider); /** * @notice Returns the maximum value achievable for variable borrow rate, in bps * @return The maximum rate */ function MAX_BORROW_RATE() external view returns (uint256); /** * @notice Returns the minimum optimal point, in bps * @return The optimal point */ function MIN_OPTIMAL_POINT() external view returns (uint256); /** * @notice Returns the maximum optimal point, in bps * @return The optimal point */ function MAX_OPTIMAL_POINT() external view returns (uint256); /** * notice Returns the full InterestRateDataRay object for the given reserve, in bps * * @param reserve The reserve to get the data of * * @return The InterestRateData object for the given reserve */ function getInterestRateDataBps(address reserve) external view returns (InterestRateData memory); /** * @notice Returns the optimal usage rate for the given reserve in ray * * @param reserve The reserve to get the optimal usage rate of * * @return The optimal usage rate is the level of borrow / collateral at which the borrow rate */ function getOptimalUsageRatio(address reserve) external view returns (uint256); /** * @notice Returns the variable rate slope below optimal usage ratio in ray * @dev It's the variable rate when usage ratio > 0 and <= OPTIMAL_USAGE_RATIO * * @param reserve The reserve to get the variable rate slope 1 of * * @return The variable rate slope */ function getVariableRateSlope1(address reserve) external view returns (uint256); /** * @notice Returns the variable rate slope above optimal usage ratio in ray * @dev It's the variable rate when usage ratio > OPTIMAL_USAGE_RATIO * * @param reserve The reserve to get the variable rate slope 2 of * * @return The variable rate slope */ function getVariableRateSlope2(address reserve) external view returns (uint256); /** * @notice Returns the base variable borrow rate, in ray * * @param reserve The reserve to get the base variable borrow rate of * * @return The base variable borrow rate */ function getBaseVariableBorrowRate(address reserve) external view returns (uint256); /** * @notice Returns the maximum variable borrow rate, in ray * * @param reserve The reserve to get the maximum variable borrow rate of * * @return The maximum variable borrow rate */ function getMaxVariableBorrowRate(address reserve) external view returns (uint256); /** * @notice Sets interest rate data for an Aave rate strategy * @param reserve The reserve to update * @param rateData The reserve interest rate data to apply to the given reserve * Being specific to this custom implementation, with custom struct type, * overloading the function on the generic interface */ function setInterestRateParams(address reserve, InterestRateData calldata rateData) external; } /** * @title DefaultReserveInterestRateStrategyV2 contract * @author BGD Labs * @notice Default interest rate strategy used by the Aave protocol * @dev Strategies are pool-specific: each contract CAN'T be used across different Aave pools * due to the caching of the PoolAddressesProvider and the usage of underlying addresses as * index of the _interestRateData */ contract DefaultReserveInterestRateStrategyV2 is IDefaultInterestRateStrategyV2 { using WadRayMath for uint256; using PercentageMath for uint256; /// @inheritdoc IDefaultInterestRateStrategyV2 IPoolAddressesProvider public immutable ADDRESSES_PROVIDER; /// @inheritdoc IDefaultInterestRateStrategyV2 uint256 public constant MAX_BORROW_RATE = 1000_00; /// @inheritdoc IDefaultInterestRateStrategyV2 uint256 public constant MIN_OPTIMAL_POINT = 1_00; /// @inheritdoc IDefaultInterestRateStrategyV2 uint256 public constant MAX_OPTIMAL_POINT = 99_00; /// @dev Map of reserves address and their interest rate data (reserveAddress => interestRateData) mapping(address => InterestRateData) internal _interestRateData; modifier onlyPoolConfigurator() { require( msg.sender == ADDRESSES_PROVIDER.getPoolConfigurator(), Errors.CALLER_NOT_POOL_CONFIGURATOR ); _; } /** * @dev Constructor. * @param provider The address of the PoolAddressesProvider of the associated Aave pool */ constructor(address provider) { require(provider != address(0), Errors.INVALID_ADDRESSES_PROVIDER); ADDRESSES_PROVIDER = IPoolAddressesProvider(provider); } /// @inheritdoc IReserveInterestRateStrategy function setInterestRateParams( address reserve, bytes calldata rateData ) external onlyPoolConfigurator { _setInterestRateParams(reserve, abi.decode(rateData, (InterestRateData))); } /// @inheritdoc IDefaultInterestRateStrategyV2 function setInterestRateParams( address reserve, InterestRateData calldata rateData ) external onlyPoolConfigurator { _setInterestRateParams(reserve, rateData); } /// @inheritdoc IDefaultInterestRateStrategyV2 function getInterestRateDataBps(address reserve) external view returns (InterestRateData memory) { return _interestRateData[reserve]; } /// @inheritdoc IDefaultInterestRateStrategyV2 function getOptimalUsageRatio(address reserve) external view returns (uint256) { return _bpsToRay(uint256(_interestRateData[reserve].optimalUsageRatio)); } /// @inheritdoc IDefaultInterestRateStrategyV2 function getVariableRateSlope1(address reserve) external view returns (uint256) { return _bpsToRay(uint256(_interestRateData[reserve].variableRateSlope1)); } /// @inheritdoc IDefaultInterestRateStrategyV2 function getVariableRateSlope2(address reserve) external view returns (uint256) { return _bpsToRay(uint256(_interestRateData[reserve].variableRateSlope2)); } /// @inheritdoc IDefaultInterestRateStrategyV2 function getBaseVariableBorrowRate(address reserve) external view override returns (uint256) { return _bpsToRay(uint256(_interestRateData[reserve].baseVariableBorrowRate)); } /// @inheritdoc IDefaultInterestRateStrategyV2 function getMaxVariableBorrowRate(address reserve) external view override returns (uint256) { return _bpsToRay( uint256( _interestRateData[reserve].baseVariableBorrowRate + _interestRateData[reserve].variableRateSlope1 + _interestRateData[reserve].variableRateSlope2 ) ); } /// @inheritdoc IReserveInterestRateStrategy function calculateInterestRates( DataTypes.CalculateInterestRatesParams memory params ) external view virtual override returns (uint256, uint256, uint256) { InterestRateDataRay memory rateData = _rayifyRateData(_interestRateData[params.reserve]); // @note This is a short circuit to allow mintable assets (ex. GHO), which by definition cannot be supplied // and thus do not use virtual underlying balances. if (!params.usingVirtualBalance) { return (0, 0, rateData.baseVariableBorrowRate); } CalcInterestRatesLocalVars memory vars; vars.totalDebt = params.totalStableDebt + params.totalVariableDebt; vars.currentLiquidityRate = 0; vars.currentVariableBorrowRate = rateData.baseVariableBorrowRate; if (vars.totalDebt != 0) { vars.availableLiquidity = params.virtualUnderlyingBalance + params.liquidityAdded - params.liquidityTaken; vars.availableLiquidityPlusDebt = vars.availableLiquidity + vars.totalDebt; vars.borrowUsageRatio = vars.totalDebt.rayDiv(vars.availableLiquidityPlusDebt); vars.supplyUsageRatio = vars.totalDebt.rayDiv( vars.availableLiquidityPlusDebt + params.unbacked ); } else { return (0, 0, vars.currentVariableBorrowRate); } if (vars.borrowUsageRatio > rateData.optimalUsageRatio) { uint256 excessBorrowUsageRatio = (vars.borrowUsageRatio - rateData.optimalUsageRatio).rayDiv( WadRayMath.RAY - rateData.optimalUsageRatio ); vars.currentVariableBorrowRate += rateData.variableRateSlope1 + rateData.variableRateSlope2.rayMul(excessBorrowUsageRatio); } else { vars.currentVariableBorrowRate += rateData .variableRateSlope1 .rayMul(vars.borrowUsageRatio) .rayDiv(rateData.optimalUsageRatio); } vars.currentLiquidityRate = _getOverallBorrowRate( params.totalStableDebt, params.totalVariableDebt, vars.currentVariableBorrowRate, params.averageStableBorrowRate ).rayMul(vars.supplyUsageRatio).percentMul( PercentageMath.PERCENTAGE_FACTOR - params.reserveFactor ); return (vars.currentLiquidityRate, 0, vars.currentVariableBorrowRate); } /** * @dev Calculates the overall borrow rate as the weighted average between the total variable debt and total stable * debt * @param totalStableDebt The total borrowed from the reserve at a stable rate * @param totalVariableDebt The total borrowed from the reserve at a variable rate * @param currentVariableBorrowRate The current variable borrow rate of the reserve * @param currentAverageStableBorrowRate The current weighted average of all the stable rate loans * @return The weighted averaged borrow rate */ function _getOverallBorrowRate( uint256 totalStableDebt, uint256 totalVariableDebt, uint256 currentVariableBorrowRate, uint256 currentAverageStableBorrowRate ) internal pure returns (uint256) { uint256 totalDebt = totalStableDebt + totalVariableDebt; uint256 weightedVariableRate = totalVariableDebt.wadToRay().rayMul(currentVariableBorrowRate); uint256 weightedStableRate = totalStableDebt.wadToRay().rayMul(currentAverageStableBorrowRate); uint256 overallBorrowRate = (weightedVariableRate + weightedStableRate).rayDiv( totalDebt.wadToRay() ); return overallBorrowRate; } /** * @dev Doing validations and data update for an asset * @param reserve address of the underlying asset of the reserve * @param rateData Encoded reserve interest rate data to apply */ function _setInterestRateParams(address reserve, InterestRateData memory rateData) internal { require(reserve != address(0), Errors.ZERO_ADDRESS_NOT_VALID); require( rateData.optimalUsageRatio <= MAX_OPTIMAL_POINT && rateData.optimalUsageRatio >= MIN_OPTIMAL_POINT, Errors.INVALID_OPTIMAL_USAGE_RATIO ); require( rateData.variableRateSlope1 <= rateData.variableRateSlope2, Errors.SLOPE_2_MUST_BE_GTE_SLOPE_1 ); // The maximum rate should not be above certain threshold require( uint256(rateData.baseVariableBorrowRate) + uint256(rateData.variableRateSlope1) + uint256(rateData.variableRateSlope2) <= MAX_BORROW_RATE, Errors.INVALID_MAX_RATE ); _interestRateData[reserve] = rateData; emit RateDataUpdate( reserve, rateData.optimalUsageRatio, rateData.baseVariableBorrowRate, rateData.variableRateSlope1, rateData.variableRateSlope2 ); } /** * @dev Transforms an InterestRateData struct to an InterestRateDataRay struct by multiplying all values * by 1e23, turning them into ray values * * @param data The InterestRateData struct to transform * * @return The resulting InterestRateDataRay struct */ function _rayifyRateData( InterestRateData memory data ) internal pure returns (InterestRateDataRay memory) { return InterestRateDataRay({ optimalUsageRatio: _bpsToRay(uint256(data.optimalUsageRatio)), baseVariableBorrowRate: _bpsToRay(uint256(data.baseVariableBorrowRate)), variableRateSlope1: _bpsToRay(uint256(data.variableRateSlope1)), variableRateSlope2: _bpsToRay(uint256(data.variableRateSlope2)) }); } // @dev helper function added here, as generally the protocol doesn't use bps function _bpsToRay(uint256 n) internal pure returns (uint256) { return n * 1e23; } } ================================================ FILE: src/contracts/misc/dependencies/Ccip.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; // End consumer library. library Client { /// @dev RMN depends on this struct, if changing, please notify the RMN maintainers. struct EVMTokenAmount { address token; // token address on the local chain. uint256 amount; // Amount of tokens. } struct Any2EVMMessage { bytes32 messageId; // MessageId corresponding to ccipSend on source. uint64 sourceChainSelector; // Source chain selector. bytes sender; // abi.decode(sender) if coming from an EVM chain. bytes data; // payload sent in original message. EVMTokenAmount[] destTokenAmounts; // Tokens and their amounts in their destination chain representation. } // If extraArgs is empty bytes, the default is 200k gas limit. struct EVM2AnyMessage { bytes receiver; // abi.encode(receiver address) for dest EVM chains bytes data; // Data payload EVMTokenAmount[] tokenAmounts; // Token transfers address feeToken; // Address of feeToken. address(0) means you will send msg.value. bytes extraArgs; // Populate this with _argsToBytes(EVMExtraArgsV1) } // bytes4(keccak256("CCIP EVMExtraArgsV1")); bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9; struct EVMExtraArgsV1 { uint256 gasLimit; } function _argsToBytes(EVMExtraArgsV1 memory extraArgs) internal pure returns (bytes memory bts) { return abi.encodeWithSelector(EVM_EXTRA_ARGS_V1_TAG, extraArgs); } } /// @notice Implements Token Bucket rate limiting. /// @dev Reduced library from https://github.com/aave/ccip/blob/ccip-gho/contracts/src/v0.8/ccip/libraries/RateLimiter.sol /// @dev uint128 is safe for rate limiter state. /// For USD value rate limiting, it can adequately store USD value in 18 decimals. /// For ERC20 token amount rate limiting, all tokens that will be listed will have at most /// a supply of uint128.max tokens, and it will therefore not overflow the bucket. /// In exceptional scenarios where tokens consumed may be larger than uint128, /// e.g. compromised issuer, an enabled RateLimiter will check and revert. library RateLimiter { error InvalidRatelimitRate(Config rateLimiterConfig); error DisabledNonZeroRateLimit(Config config); error RateLimitMustBeDisabled(); event ConfigChanged(Config config); struct TokenBucket { uint128 tokens; // ──────╮ Current number of tokens that are in the bucket. uint32 lastUpdated; // │ Timestamp in seconds of the last token refill, good for 100+ years. bool isEnabled; // ──────╯ Indication whether the rate limiting is enabled or not uint128 capacity; // ────╮ Maximum number of tokens that can be in the bucket. uint128 rate; // ────────╯ Number of tokens per second that the bucket is refilled. } struct Config { bool isEnabled; // Indication whether the rate limiting should be enabled uint128 capacity; // ────╮ Specifies the capacity of the rate limiter uint128 rate; // ───────╯ Specifies the rate of the rate limiter } /// @notice Gets the token bucket with its values for the block it was requested at. /// @return The token bucket. function _currentTokenBucketState( TokenBucket memory bucket ) internal view returns (TokenBucket memory) { // We update the bucket to reflect the status at the exact time of the // call. This means we might need to refill a part of the bucket based // on the time that has passed since the last update. bucket.tokens = uint128( _calculateRefill( bucket.capacity, bucket.tokens, block.timestamp - bucket.lastUpdated, bucket.rate ) ); bucket.lastUpdated = uint32(block.timestamp); return bucket; } /// @notice Sets the rate limited config. /// @param s_bucket The token bucket /// @param config The new config function _setTokenBucketConfig(TokenBucket storage s_bucket, Config memory config) internal { // First update the bucket to make sure the proper rate is used for all the time // up until the config change. uint256 timeDiff = block.timestamp - s_bucket.lastUpdated; if (timeDiff != 0) { s_bucket.tokens = uint128( _calculateRefill(s_bucket.capacity, s_bucket.tokens, timeDiff, s_bucket.rate) ); s_bucket.lastUpdated = uint32(block.timestamp); } s_bucket.tokens = uint128(_min(config.capacity, s_bucket.tokens)); s_bucket.isEnabled = config.isEnabled; s_bucket.capacity = config.capacity; s_bucket.rate = config.rate; emit ConfigChanged(config); } /// @notice Validates the token bucket config function _validateTokenBucketConfig(Config memory config, bool mustBeDisabled) internal pure { if (config.isEnabled) { if (config.rate >= config.capacity || config.rate == 0) { revert InvalidRatelimitRate(config); } if (mustBeDisabled) { revert RateLimitMustBeDisabled(); } } else { if (config.rate != 0 || config.capacity != 0) { revert DisabledNonZeroRateLimit(config); } } } /// @notice Calculate refilled tokens /// @param capacity bucket capacity /// @param tokens current bucket tokens /// @param timeDiff block time difference since last refill /// @param rate bucket refill rate /// @return the value of tokens after refill function _calculateRefill( uint256 capacity, uint256 tokens, uint256 timeDiff, uint256 rate ) private pure returns (uint256) { return _min(capacity, tokens + timeDiff * rate); } /// @notice Return the smallest of two integers /// @param a first int /// @param b second int /// @return smallest function _min(uint256 a, uint256 b) internal pure returns (uint256) { return a < b ? a : b; } } /// @dev Reduced interface of CCIP Router contract with needed functions only /// @dev Adapted from https://github.com/aave/ccip/blob/ccip-gho/contracts/src/v0.8/ccip/interfaces/IRouter.sol interface IRouter { error OnlyOffRamp(); /// @notice Route the message to its intended receiver contract. /// @param message Client.Any2EVMMessage struct. /// @param gasForCallExactCheck of params for exec /// @param gasLimit set of params for exec /// @param receiver set of params for exec /// @dev if the receiver is a contracts that signals support for CCIP execution through EIP-165. /// the contract is called. If not, only tokens are transferred. /// @return success A boolean value indicating whether the ccip message was received without errors. /// @return retBytes A bytes array containing return data form CCIP receiver. /// @return gasUsed the gas used by the external customer call. Does not include any overhead. function routeMessage( Client.Any2EVMMessage calldata message, uint16 gasForCallExactCheck, uint256 gasLimit, address receiver ) external returns (bool success, bytes memory retBytes, uint256 gasUsed); /// @notice Returns the configured onramp for a specific destination chain. /// @param destChainSelector The destination chain Id to get the onRamp for. /// @return onRampAddress The address of the onRamp. function getOnRamp(uint64 destChainSelector) external view returns (address onRampAddress); /// @notice Return true if the given offRamp is a configured offRamp for the given source chain. /// @param sourceChainSelector The source chain selector to check. /// @param offRamp The address of the offRamp to check. function isOffRamp( uint64 sourceChainSelector, address offRamp ) external view returns (bool isOffRamp); } /// @dev Reduced interface of CCIP UpgradeableLockReleaseTokenPool contract with needed functions only /// @dev Adapted from https://github.com/aave/ccip/blob/ccip-gho/contracts/src/v0.8/ccip/pools/GHO/UpgradeableLockReleaseTokenPool.sol interface IUpgradeableLockReleaseTokenPool { function setBridgeLimit(uint256 newBridgeLimit) external; function setChainRateLimiterConfig( uint64 remoteChainSelector, RateLimiter.Config memory outboundConfig, RateLimiter.Config memory inboundConfig ) external; function setRateLimitAdmin(address rateLimitAdmin) external; function setBridgeLimitAdmin(address bridgeLimitAdmin) external; function getRateLimitAdmin() external view returns (address); function getBridgeLimitAdmin() external view returns (address); function getBridgeLimit() external view returns (uint256); function getCurrentOutboundRateLimiterState( uint64 remoteChainSelector ) external view returns (RateLimiter.TokenBucket memory); function getCurrentInboundRateLimiterState( uint64 remoteChainSelector ) external view returns (RateLimiter.TokenBucket memory); } ================================================ FILE: src/contracts/misc/interfaces/IGhoAaveSteward.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; /** * @title IGhoAaveSteward * @author Aave Labs * @notice Defines the basic interface of the GhoAaveSteward */ interface IGhoAaveSteward { /** * @notice Struct storing the last update by the steward of each borrow rate param */ struct GhoDebounce { uint40 ghoBorrowCapLastUpdate; uint40 ghoSupplyCapLastUpdate; uint40 ghoBorrowRateLastUpdate; } /** * @notice Struct storing the configuration for the borrow rate params */ struct BorrowRateConfig { uint16 optimalUsageRatioMaxChange; uint32 baseVariableBorrowRateMaxChange; uint32 variableRateSlope1MaxChange; uint32 variableRateSlope2MaxChange; } /** * @notice Updates the borrow rate of GHO, only if: * - respects `MINIMUM_DELAY`, the minimum time delay between updates * - the update changes parameters up to the maximum allowed change according to risk config * @dev Only callable by Risk Council * @dev Values are all expressed in BPS * @param optimalUsageRatio The new optimal usage ratio * @param baseVariableBorrowRate The new base variable borrow rate * @param variableRateSlope1 The new variable rate slope 1 * @param variableRateSlope2 The new variable rate slope 2 */ function updateGhoBorrowRate( uint16 optimalUsageRatio, uint32 baseVariableBorrowRate, uint32 variableRateSlope1, uint32 variableRateSlope2 ) external; /** * @notice Updates the GHO borrow cap, only if: * - respects `MINIMUM_DELAY`, the minimum time delay between updates * - the update changes up to 100% upwards or downwards * @dev Only callable by Risk Council * @param newBorrowCap The new borrow cap (in whole tokens) */ function updateGhoBorrowCap(uint256 newBorrowCap) external; /** * @notice Updates the GHO supply cap, only if: * - respects `MINIMUM_DELAY`, the minimum time delay between updates * - the update changes up to 100% upwards or downwards * @dev Only callable by Risk Council * @param newSupplyCap The new supply cap (in whole tokens) */ function updateGhoSupplyCap(uint256 newSupplyCap) external; /** * @notice Updates the configuration conditions for borrow rate changes * @dev Values are all expressed in BPS * @param optimalUsageRatioMaxChange The new allowed max percentage change for optimal usage ratio * @param baseVariableBorrowRateMaxChange The new allowed max percentage change for base variable borrow rate * @param variableRateSlope1MaxChange The new allowed max percentage change for variable rate slope 1 * @param variableRateSlope2MaxChange The new allowed max percentage change for variable rate slope 2 */ function setBorrowRateConfig( uint16 optimalUsageRatioMaxChange, uint32 baseVariableBorrowRateMaxChange, uint32 variableRateSlope1MaxChange, uint32 variableRateSlope2MaxChange ) external; /** * @notice Returns the configuration conditions for a GHO borrow rate change * @return struct containing the borrow rate configuration */ function getBorrowRateConfig() external view returns (BorrowRateConfig memory); /** * @notice Returns timestamp of the last update of GHO parameters * @return The GhoDebounce struct describing the last update of GHO parameters */ function getGhoTimelocks() external view returns (GhoDebounce memory); /** * @notice The address of pool data provider of the POOL the steward controls */ function POOL_DATA_PROVIDER() external view returns (address); /** * @notice Returns the minimum delay that must be respected between parameters update. * @return The minimum delay between parameter updates (in seconds) */ function MINIMUM_DELAY() external view returns (uint256); /** * @notice Returns the address of the Pool Addresses Provider of the Aave V3 Ethereum Pool * @return The address of the PoolAddressesProvider of Aave V3 Ethereum Pool */ function POOL_ADDRESSES_PROVIDER() external view returns (address); /** * @notice Returns the address of the Gho Token * @return The address of the GhoToken */ function GHO_TOKEN() external view returns (address); /** * @notice Returns the address of the risk council * @return The address of the RiskCouncil */ function RISK_COUNCIL() external view returns (address); } ================================================ FILE: src/contracts/misc/interfaces/IGhoBucketSteward.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; /** * @title IGhoBucketSteward * @author Aave Labs * @notice Defines the basic interface of the GhoBucketSteward */ interface IGhoBucketSteward { /** * @notice Updates the bucket capacity of facilitator, only if: * - respects `MINIMUM_DELAY`, the minimum time delay between updates * - the update changes up to 100% upwards * - the facilitator is controlled * @dev Only callable by Risk Council * @param facilitator The facilitator address * @param newBucketCapacity The new facilitator bucket capacity */ function updateFacilitatorBucketCapacity(address facilitator, uint128 newBucketCapacity) external; /** * @notice Adds/Removes controlled facilitators * @dev Only callable by owner * @param facilitatorList A list of facilitators addresses to add to control * @param approve True to add as controlled facilitators, false to remove */ function setControlledFacilitator(address[] memory facilitatorList, bool approve) external; /** * @notice Returns the list of controlled facilitators by this steward. * @return An array of facilitator addresses */ function getControlledFacilitators() external view returns (address[] memory); /** * @notice Checks if a facilitator is controlled by this steward * @param facilitator The facilitator address to check * @return True if the facilitator is controlled by this steward */ function isControlledFacilitator(address facilitator) external view returns (bool); /** * @notice Returns timestamp of the facilitators last bucket capacity update * @param facilitator The facilitator address * @return The unix time of the last bucket capacity (in seconds). */ function getFacilitatorBucketCapacityTimelock(address facilitator) external view returns (uint40); /** * @notice Returns the minimum delay that must be respected between parameters update. * @return The minimum delay between parameter updates (in seconds) */ function MINIMUM_DELAY() external view returns (uint256); /** * @notice Returns the address of the Gho Token * @return The address of the GhoToken */ function GHO_TOKEN() external view returns (address); /** * @notice Returns the address of the risk council * @return The address of the RiskCouncil */ function RISK_COUNCIL() external view returns (address); } ================================================ FILE: src/contracts/misc/interfaces/IGhoCcipSteward.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; /** * @title IGhoCcipSteward * @author Aave Labs * @notice Defines the basic interface of the GhoCcipSteward */ interface IGhoCcipSteward { /** * @notice Struct storing the last update by the steward of the bridge and rate limit param. */ struct CcipDebounce { uint40 bridgeLimitLastUpdate; uint40 rateLimitLastUpdate; } /** * @notice Updates the CCIP bridge limit * @dev Only callable by Risk Council * @param newBridgeLimit The new desired bridge limit */ function updateBridgeLimit(uint256 newBridgeLimit) external; /** * @notice Updates the CCIP rate limit config * @dev Only callable by Risk Council * @dev Rate limit update must be consistent with other pools' rate limit * @param remoteChainSelector The remote chain selector for which the rate limits apply. * @param outboundEnabled True if the outbound rate limiter is enabled. * @param outboundCapacity The outbound rate limiter capacity. * @param outboundRate The outbound rate limiter rate. * @param inboundEnabled True if the inbound rate limiter is enabled. * @param inboundCapacity The inbound rate limiter capacity. * @param inboundRate The inbound rate limiter rate. */ function updateRateLimit( uint64 remoteChainSelector, bool outboundEnabled, uint128 outboundCapacity, uint128 outboundRate, bool inboundEnabled, uint128 inboundCapacity, uint128 inboundRate ) external; /** * @notice Returns timestamp of the last update of Ccip parameters. * @return The CcipDebounce struct describing the last update of Ccip parameters. */ function getCcipTimelocks() external view returns (CcipDebounce memory); /** * @notice Returns the minimum delay that must be respected between parameters update. * @return The minimum delay between parameter updates (in seconds) */ function MINIMUM_DELAY() external view returns (uint256); /** * @notice Returns the address of the Gho Token * @return The address of the GhoToken */ function GHO_TOKEN() external view returns (address); /** * @notice Returns the address of the Gho CCIP Token Pool * @return The address of the Gho CCIP Token Pool */ function GHO_TOKEN_POOL() external view returns (address); /** * @notice Returns whether the bridge limit feature is supported in the GhoTokenPool * @return True if bridge limit is enabled in the CCIP GhoTokenPool, false otherwise */ function BRIDGE_LIMIT_ENABLED() external view returns (bool); /** * @notice Returns the address of the risk council * @return The address of the RiskCouncil */ function RISK_COUNCIL() external view returns (address); } ================================================ FILE: src/contracts/misc/interfaces/IGhoGsmSteward.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; /** * @title IGhoGsmSteward * @author Aave Labs * @notice Defines the basic interface of the GhoGsmSteward */ interface IGhoGsmSteward { struct GsmDebounce { uint40 gsmExposureCapLastUpdated; uint40 gsmFeeStrategyLastUpdated; } /** * @notice Updates the exposure cap of the GSM, only if: * - respects `MINIMUM_DELAY`, the minimum time delay between updates * - the update changes up to 100% upwards or downwards * @dev Only callable by Risk Council * @param gsm The gsm address to update * @param newExposureCap The new exposure cap (in underlying asset terms) */ function updateGsmExposureCap(address gsm, uint128 newExposureCap) external; /** * @notice Updates the fixed percent fees of the GSM, only if: * - respects `MINIMUM_DELAY`, the minimum time delay between updates * - the update changes up to `GSM_FEE_RATE_CHANGE_MAX` upwards or downwards (for both buy and sell individually) * @dev Only callable by Risk Council * @dev Reverts if fee strategy is not set, or zero fees. Must be updated via AIP in this case * @param gsm The gsm address to update * @param buyFee The new buy fee (expressed in bps) (e.g. 0.0150e4 results in 1.50%) * @param sellFee The new sell fee (expressed in bps) (e.g. 0.0150e4 results in 1.50%) */ function updateGsmBuySellFees(address gsm, uint256 buyFee, uint256 sellFee) external; /** * @notice Returns timestamp of the last update of Gsm parameters * @param gsm The GSM address * @return The GsmDebounce struct describing the last update of GSM parameters */ function getGsmTimelocks(address gsm) external view returns (GsmDebounce memory); /** * @notice Returns the maximum increase for GSM fee rates (buy or sell). * @return The maximum increase change for GSM fee rates updates in bps (e.g. 0.010e4 results in 1.00%) */ function GSM_FEE_RATE_CHANGE_MAX() external view returns (uint256); /** * @notice Returns the minimum delay that must be respected between parameters update. * @return The minimum delay between parameter updates (in seconds) */ function MINIMUM_DELAY() external view returns (uint256); /** * @notice Returns the address of the GSM Fee Strategy Factory * @return The address of the GSM Fee Strategy Factory */ function FIXED_FEE_STRATEGY_FACTORY() external view returns (address); /** * @notice Returns the address of the risk council * @return The address of the RiskCouncil */ function RISK_COUNCIL() external view returns (address); } ================================================ FILE: src/script/DeployGsmLaunch.s.sol ================================================ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; import {Script, console2} from 'forge-std/Script.sol'; import {AaveV3Ethereum, AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; import {Gsm} from '../contracts/facilitators/gsm/Gsm.sol'; import {IGsm} from '../contracts/facilitators/gsm/interfaces/IGsm.sol'; import {FixedPriceStrategy} from '../contracts/facilitators/gsm/priceStrategy/FixedPriceStrategy.sol'; import {FixedFeeStrategy} from '../contracts/facilitators/gsm/feeStrategy/FixedFeeStrategy.sol'; import {GsmRegistry} from '../contracts/facilitators/gsm/misc/GsmRegistry.sol'; import {OracleSwapFreezer} from '../contracts/facilitators/gsm/swapFreezer/OracleSwapFreezer.sol'; // GSM USDC uint8 constant USDC_DECIMALS = 6; uint128 constant USDC_EXPOSURE_CAP = 500_000e6; string constant GSM_USDC_FACILITATOR_LABEL = 'GSM USDC'; uint128 constant GSM_USDC_BUCKET_CAPACITY = 500_000e18; // GSM USDT uint8 constant USDT_DECIMALS = 6; uint128 constant USDT_EXPOSURE_CAP = 500_000e6; string constant GSM_USDT_FACILITATOR_LABEL = 'GSM USDT'; uint128 constant GSM_USDT_BUCKET_CAPACITY = 500_000e18; uint256 constant GSM_PRICE_RATIO = 1e18; uint256 constant GSM_BUY_FEE_BPS = 0.002e4; // 0.2%, 0.5e4 is 50% uint256 constant GSM_SELL_FEE_BPS = 0.002e4; // 0.2% uint128 constant SWAP_FREEZE_LOWER_BOUND = 0.99e8; uint128 constant SWAP_FREEZE_UPPER_BOUND = 1.01e8; uint128 constant SWAP_UNFREEZE_LOWER_BOUND = 0.995e8; uint128 constant SWAP_UNFREEZE_UPPER_BOUND = 1.005e8; bool constant SWAP_UNFREEZE_ALLOWED = true; contract DeployGsmLaunch is Script { function run() external { uint256 deployerPrivateKey = vm.envUint('PRIVATE_KEY'); address deployerAddress = vm.addr(deployerPrivateKey); console2.log('Deployer Address: ', deployerAddress); console2.log('Deployer Balance: ', address(deployerAddress).balance); console2.log('Block Number: ', block.number); vm.startBroadcast(deployerPrivateKey); _deploy(); vm.stopBroadcast(); } function _deploy() internal { // ------------------------------------------------ // 1. FixedPriceStrategy // ------------------------------------------------ FixedPriceStrategy gsmUsdcPriceStrategy = new FixedPriceStrategy( GSM_PRICE_RATIO, AaveV3EthereumAssets.USDC_UNDERLYING, USDC_DECIMALS ); console2.log('GSM USDC FixedPriceStrategy: ', address(gsmUsdcPriceStrategy)); FixedPriceStrategy gsmUsdtPriceStrategy = new FixedPriceStrategy( GSM_PRICE_RATIO, AaveV3EthereumAssets.USDT_UNDERLYING, USDT_DECIMALS ); console2.log('GSM USDT FixedPriceStrategy: ', address(gsmUsdtPriceStrategy)); // ------------------------------------------------ // 2. GSM implementations // ------------------------------------------------ Gsm gsmUsdcImpl = new Gsm( AaveV3EthereumAssets.GHO_UNDERLYING, AaveV3EthereumAssets.USDC_UNDERLYING, address(gsmUsdcPriceStrategy) ); console2.log('GSM USDC Implementation: ', address(gsmUsdcImpl)); Gsm gsmUsdtImpl = new Gsm( AaveV3EthereumAssets.GHO_UNDERLYING, AaveV3EthereumAssets.USDT_UNDERLYING, address(gsmUsdtPriceStrategy) ); console2.log('GSM USDT Implementation: ', address(gsmUsdtImpl)); gsmUsdcImpl.initialize( GovernanceV3Ethereum.EXECUTOR_LVL_1, address(AaveV3Ethereum.COLLECTOR), USDC_EXPOSURE_CAP ); gsmUsdtImpl.initialize( GovernanceV3Ethereum.EXECUTOR_LVL_1, address(AaveV3Ethereum.COLLECTOR), USDT_EXPOSURE_CAP ); // ------------------------------------------------ // 3. GSM proxy deployment and initialization // ------------------------------------------------ bytes memory gsmUsdcInitParams = abi.encodeWithSignature( 'initialize(address,address,uint128)', GovernanceV3Ethereum.EXECUTOR_LVL_1, address(AaveV3Ethereum.COLLECTOR), USDC_EXPOSURE_CAP ); TransparentUpgradeableProxy gsmUsdcProxy = new TransparentUpgradeableProxy( address(gsmUsdcImpl), MiscEthereum.PROXY_ADMIN, gsmUsdcInitParams ); Gsm gsmUsdc = Gsm(address(gsmUsdcProxy)); console2.log('GSM USDC Proxy: ', address(gsmUsdcProxy)); bytes memory gsmUsdtInitParams = abi.encodeWithSignature( 'initialize(address,address,uint128)', GovernanceV3Ethereum.EXECUTOR_LVL_1, address(AaveV3Ethereum.COLLECTOR), USDT_EXPOSURE_CAP ); TransparentUpgradeableProxy gsmUsdtProxy = new TransparentUpgradeableProxy( address(gsmUsdtImpl), MiscEthereum.PROXY_ADMIN, gsmUsdtInitParams ); Gsm gsmUsdt = Gsm(address(gsmUsdtProxy)); console2.log('GSM USDT Proxy: ', address(gsmUsdtProxy)); // ------------------------------------------------ // 4. FixedFeeStrategy // ------------------------------------------------ FixedFeeStrategy fixedFeeStrategy = new FixedFeeStrategy(GSM_BUY_FEE_BPS, GSM_SELL_FEE_BPS); console2.log('GSM FixedFeeStrategy: ', address(fixedFeeStrategy)); // ------------------------------------------------ // 5. OracleSwapFreezers // ------------------------------------------------ OracleSwapFreezer gsmUsdcOracleSwapFreezer = new OracleSwapFreezer( IGsm(address(gsmUsdc)), AaveV3EthereumAssets.USDC_UNDERLYING, IPoolAddressesProvider(address(AaveV3Ethereum.POOL_ADDRESSES_PROVIDER)), SWAP_FREEZE_LOWER_BOUND, SWAP_FREEZE_UPPER_BOUND, SWAP_UNFREEZE_LOWER_BOUND, SWAP_UNFREEZE_UPPER_BOUND, SWAP_UNFREEZE_ALLOWED ); console2.log('GSM USDC OracleSwapFreezer: ', address(gsmUsdcOracleSwapFreezer)); OracleSwapFreezer gsmUsdtOracleSwapFreezer = new OracleSwapFreezer( IGsm(address(gsmUsdt)), AaveV3EthereumAssets.USDT_UNDERLYING, IPoolAddressesProvider(address(AaveV3Ethereum.POOL_ADDRESSES_PROVIDER)), SWAP_FREEZE_LOWER_BOUND, SWAP_FREEZE_UPPER_BOUND, SWAP_UNFREEZE_LOWER_BOUND, SWAP_UNFREEZE_UPPER_BOUND, SWAP_UNFREEZE_ALLOWED ); console2.log('GSM USDT OracleSwapFreezer: ', address(gsmUsdtOracleSwapFreezer)); // ------------------------------------------------ // 6. Deploy GsmRegistry // ------------------------------------------------ GsmRegistry gsmRegistry = new GsmRegistry(GovernanceV3Ethereum.EXECUTOR_LVL_1); console2.log('GsmRegistry: ', address(gsmRegistry)); } } ================================================ FILE: src/script/ExternalDependencyCompiler.s.sol ================================================ // SPDX-License-Identifier: UNLICENSED // Importing contracts from dependencies libraries so it can be used by Hardhat scripts import 'aave-stk-v1-5/src/contracts/StakedAaveV3.sol'; ================================================ FILE: src/test/TestFixedRateStrategyFactory.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestFixedRateStrategyFactory is TestGhoBase { using ReserveConfiguration for DataTypes.ReserveConfigurationMap; function testConstructor() public { assertEq(FIXED_RATE_STRATEGY_FACTORY.POOL_ADDRESSES_PROVIDER(), address(PROVIDER)); address[] memory strategies = FIXED_RATE_STRATEGY_FACTORY.getAllStrategies(); assertEq(strategies.length, 0); } function testRevertConstructorInvalidExecutor() public { vm.expectRevert('INVALID_ADDRESSES_PROVIDER'); new FixedRateStrategyFactory(address(0)); } function testInitialize() public { address[] memory strategies = new address[](1); strategies[0] = address(new GhoInterestRateStrategy(address(PROVIDER), 100)); vm.expectEmit(true, true, false, false, address(FIXED_RATE_STRATEGY_FACTORY)); emit RateStrategyCreated(strategies[0], 100); FIXED_RATE_STRATEGY_FACTORY.initialize(strategies); address[] memory strategiesCall = FIXED_RATE_STRATEGY_FACTORY.getAllStrategies(); assertEq(strategiesCall.length, 1); assertEq(strategiesCall[0], strategies[0]); } function testInitializeMultiple() public { address[] memory strategies = new address[](3); strategies[0] = address(new GhoInterestRateStrategy(address(PROVIDER), 100)); strategies[1] = address(new GhoInterestRateStrategy(address(PROVIDER), 200)); strategies[2] = address(new GhoInterestRateStrategy(address(PROVIDER), 300)); vm.expectEmit(true, true, false, false, address(FIXED_RATE_STRATEGY_FACTORY)); emit RateStrategyCreated(strategies[0], 100); vm.expectEmit(true, true, false, false, address(FIXED_RATE_STRATEGY_FACTORY)); emit RateStrategyCreated(strategies[1], 200); vm.expectEmit(true, true, false, false, address(FIXED_RATE_STRATEGY_FACTORY)); emit RateStrategyCreated(strategies[2], 300); FIXED_RATE_STRATEGY_FACTORY.initialize(strategies); address[] memory strategiesCall = FIXED_RATE_STRATEGY_FACTORY.getAllStrategies(); assertEq(strategiesCall.length, 3); assertEq(strategiesCall[0], strategies[0]); assertEq(strategiesCall[1], strategies[1]); assertEq(strategiesCall[2], strategies[2]); } function testRevertInitializeTwice() public { address[] memory strategies = new address[](1); strategies[0] = address(new GhoInterestRateStrategy(address(PROVIDER), 100)); FIXED_RATE_STRATEGY_FACTORY.initialize(strategies); vm.expectRevert('Contract instance has already been initialized'); FIXED_RATE_STRATEGY_FACTORY.initialize(strategies); } function testCreateStrategies() public { uint256[] memory rates = new uint256[](1); rates[0] = 100; uint256 nonce = vm.getNonce(address(FIXED_RATE_STRATEGY_FACTORY)); address deployedStrategy = computeCreateAddress(address(FIXED_RATE_STRATEGY_FACTORY), nonce); vm.expectEmit(true, true, false, false, address(FIXED_RATE_STRATEGY_FACTORY)); emit RateStrategyCreated(deployedStrategy, 100); address[] memory strategies = FIXED_RATE_STRATEGY_FACTORY.createStrategies(rates); assertEq(strategies.length, 1); assertEq(GhoInterestRateStrategy(strategies[0]).getBaseVariableBorrowRate(), rates[0]); } function testCreateStrategiesMultiple() public { uint256[] memory rates = new uint256[](3); rates[0] = 100; rates[1] = 200; rates[2] = 300; uint256 nonce = vm.getNonce(address(FIXED_RATE_STRATEGY_FACTORY)); address deployedStrategy1 = computeCreateAddress(address(FIXED_RATE_STRATEGY_FACTORY), nonce); vm.expectEmit(true, true, false, false, address(FIXED_RATE_STRATEGY_FACTORY)); emit RateStrategyCreated(deployedStrategy1, 100); address deployedStrategy2 = computeCreateAddress( address(FIXED_RATE_STRATEGY_FACTORY), nonce + 1 ); vm.expectEmit(true, true, false, false, address(FIXED_RATE_STRATEGY_FACTORY)); emit RateStrategyCreated(deployedStrategy2, 200); address deployedStrategy3 = computeCreateAddress( address(FIXED_RATE_STRATEGY_FACTORY), nonce + 2 ); vm.expectEmit(true, true, false, false, address(FIXED_RATE_STRATEGY_FACTORY)); emit RateStrategyCreated(deployedStrategy3, 300); address[] memory strategies = FIXED_RATE_STRATEGY_FACTORY.createStrategies(rates); assertEq(strategies.length, 3); assertEq(GhoInterestRateStrategy(strategies[0]).getBaseVariableBorrowRate(), rates[0]); assertEq(GhoInterestRateStrategy(strategies[1]).getBaseVariableBorrowRate(), rates[1]); assertEq(GhoInterestRateStrategy(strategies[2]).getBaseVariableBorrowRate(), rates[2]); } function testCreateStrategiesCached() public { uint256[] memory rates = new uint256[](2); rates[0] = 100; rates[1] = 100; address[] memory strategies = FIXED_RATE_STRATEGY_FACTORY.createStrategies(rates); assertEq(strategies.length, 2); assertEq(strategies[0], strategies[1]); } function testCreatedStrategiesCachedDifferentCalls() public { uint256[] memory rates = new uint256[](1); rates[0] = 100; address[] memory strategies = FIXED_RATE_STRATEGY_FACTORY.createStrategies(rates); address[] memory strategies2 = FIXED_RATE_STRATEGY_FACTORY.createStrategies(rates); assertEq(strategies[0], strategies2[0]); } function testGetAllStrategies() public { uint256[] memory rates = new uint256[](3); rates[0] = 100; rates[1] = 200; rates[2] = 300; address[] memory strategies = FIXED_RATE_STRATEGY_FACTORY.createStrategies(rates); address[] memory strategiesCall = FIXED_RATE_STRATEGY_FACTORY.getAllStrategies(); assertEq(strategies.length, strategiesCall.length); assertEq(strategies[0], strategiesCall[0]); assertEq(strategies[1], strategiesCall[1]); assertEq(strategies[2], strategiesCall[2]); } function testGetAllStrategiesCached() public { uint256[] memory rates = new uint256[](2); rates[0] = 100; rates[1] = 100; FIXED_RATE_STRATEGY_FACTORY.createStrategies(rates); address[] memory strategies = FIXED_RATE_STRATEGY_FACTORY.getAllStrategies(); assertEq(strategies.length, 1); } function testGetStrategyByRate() public { uint256[] memory rates = new uint256[](3); rates[0] = 100; rates[1] = 200; rates[2] = 300; address[] memory strategies = FIXED_RATE_STRATEGY_FACTORY.createStrategies(rates); assertEq(FIXED_RATE_STRATEGY_FACTORY.getStrategyByRate(rates[0]), strategies[0]); assertEq(FIXED_RATE_STRATEGY_FACTORY.getStrategyByRate(rates[1]), strategies[1]); assertEq(FIXED_RATE_STRATEGY_FACTORY.getStrategyByRate(rates[2]), strategies[2]); } function testGetFixedRateStrategyRevision() public { assertEq(FIXED_RATE_STRATEGY_FACTORY.REVISION(), FIXED_RATE_STRATEGY_FACTORY_REVISION); } } ================================================ FILE: src/test/TestGhoAToken.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGhoAToken is TestGhoBase { function testConstructor() public { GhoAToken aToken = new GhoAToken(IPool(address(POOL))); assertEq(aToken.name(), 'GHO_ATOKEN_IMPL', 'Wrong default ERC20 name'); assertEq(aToken.symbol(), 'GHO_ATOKEN_IMPL', 'Wrong default ERC20 symbol'); assertEq(aToken.decimals(), 0, 'Wrong default ERC20 decimals'); } function testInitialize() public { GhoAToken aToken = new GhoAToken(IPool(address(POOL))); string memory tokenName = 'Aave GHO'; string memory tokenSymbol = 'aGHO'; bytes memory empty; aToken.initialize( IPool(address(POOL)), TREASURY, address(GHO_TOKEN), IAaveIncentivesController(address(0)), 18, tokenName, tokenSymbol, empty ); assertEq(aToken.name(), tokenName, 'Wrong initialized name'); assertEq(aToken.symbol(), tokenSymbol, 'Wrong initialized symbol'); assertEq(aToken.decimals(), 18, 'Wrong ERC20 decimals'); } function testInitializePoolRevert() public { string memory tokenName = 'Aave GHO'; string memory tokenSymbol = 'aGHO'; bytes memory empty; GhoAToken aToken = new GhoAToken(IPool(address(POOL))); vm.expectRevert(bytes(Errors.POOL_ADDRESSES_DO_NOT_MATCH)); aToken.initialize( IPool(address(0)), TREASURY, address(GHO_TOKEN), IAaveIncentivesController(address(0)), 18, tokenName, tokenSymbol, empty ); } function testReInitRevert() public { string memory tokenName = 'Aave GHO'; string memory tokenSymbol = 'aGHO'; bytes memory empty; vm.expectRevert(bytes('Contract instance has already been initialized')); GHO_ATOKEN.initialize( IPool(address(POOL)), TREASURY, address(GHO_TOKEN), IAaveIncentivesController(address(0)), 18, tokenName, tokenSymbol, empty ); } function testUnderlying() public { assertEq( GHO_ATOKEN.UNDERLYING_ASSET_ADDRESS(), address(GHO_TOKEN), 'Underlying should match token' ); } function testGetVariableDebtToken() public { assertEq( GHO_ATOKEN.getVariableDebtToken(), address(GHO_DEBT_TOKEN), 'Variable debt token getter should match Gho Variable Debt Token' ); } function testUnauthorizedMint() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.CALLER_MUST_BE_POOL)); GHO_ATOKEN.mint(ALICE, ALICE, 0, 0); } function testUnauthorizedBurn() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.CALLER_MUST_BE_POOL)); GHO_ATOKEN.burn(ALICE, ALICE, 0, 0); } function testUnauthorizedSetVariableDebtToken() public { GhoAToken aToken = new GhoAToken(IPool(address(POOL))); vm.startPrank(ALICE); ACL_MANAGER.setState(false); vm.expectRevert(bytes(Errors.CALLER_NOT_POOL_ADMIN)); aToken.setVariableDebtToken(ALICE); } function testSetVariableDebtToken() public { GhoAToken aToken = new GhoAToken(IPool(address(POOL))); vm.expectEmit(true, true, true, true, address(aToken)); emit VariableDebtTokenSet(address(GHO_DEBT_TOKEN)); aToken.setVariableDebtToken(address(GHO_DEBT_TOKEN)); } function testUpdateVariableDebtToken() public { vm.startPrank(ALICE); vm.expectRevert(bytes('VARIABLE_DEBT_TOKEN_ALREADY_SET')); GHO_ATOKEN.setVariableDebtToken(ALICE); } function testZeroVariableDebtToken() public { GhoAToken aToken = new GhoAToken(IPool(address(POOL))); vm.startPrank(ALICE); vm.expectRevert(bytes('ZERO_ADDRESS_NOT_VALID')); aToken.setVariableDebtToken(address(0)); } function testMintRevert() public { vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); vm.prank(address(POOL)); GHO_ATOKEN.mint(CHARLES, CHARLES, 1, 1); } function testPermitRevert() public { bytes32 empty; vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); vm.prank(address(POOL)); GHO_ATOKEN.permit(CHARLES, CHARLES, 1, 1, 1, empty, empty); } function testBurnRevert() public { vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); vm.prank(address(POOL)); GHO_ATOKEN.burn(CHARLES, CHARLES, 1, 1); } function testMintToTreasuryRevert() public { vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); vm.prank(address(POOL)); GHO_ATOKEN.mintToTreasury(1, 1); } function testTransferOnLiquidationRevert() public { vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); vm.prank(address(POOL)); GHO_ATOKEN.transferOnLiquidation(CHARLES, CHARLES, 1); } function testStandardTransferRevert() public { vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); vm.prank(CHARLES); GHO_ATOKEN.transfer(ALICE, 0); } function testBalanceOfAlwaysZero() public { uint256 balance = GHO_ATOKEN.balanceOf(CHARLES); assertEq(balance, 0, 'AToken balance should always be zero'); } function testTotalSupplyAlwaysZero() public { uint256 supply = GHO_ATOKEN.totalSupply(); assertEq(supply, 0, 'AToken total supply should always be zero'); } function testReserveTreasuryAddress() public { assertEq( GHO_ATOKEN.RESERVE_TREASURY_ADDRESS(), TREASURY, 'AToken treasury address should match the initialized address' ); } function testDistributeFees() public { borrowAction(CHARLES, 1000e18); vm.warp(block.timestamp + 640000); ghoFaucet(CHARLES, 5e18); repayAction(CHARLES, GHO_DEBT_TOKEN.balanceOf(CHARLES)); vm.expectEmit(true, true, true, true, address(GHO_ATOKEN)); emit FeesDistributedToTreasury( TREASURY, address(GHO_TOKEN), GHO_TOKEN.balanceOf(address(GHO_ATOKEN)) ); GHO_ATOKEN.distributeFeesToTreasury(); } function testRescueToken() public { vm.prank(FAUCET); AAVE_TOKEN.mint(address(GHO_ATOKEN), 1); GHO_ATOKEN.rescueTokens(address(AAVE_TOKEN), CHARLES, 1); assertEq(AAVE_TOKEN.balanceOf(CHARLES), 1, 'Token rescue should transfer 1 wei'); } function testRescueTokenRevertIfUnderlying() public { vm.expectRevert(bytes(Errors.UNDERLYING_CANNOT_BE_RESCUED)); vm.prank(FAUCET); GHO_ATOKEN.rescueTokens(address(GHO_TOKEN), CHARLES, 1); } function testUpdateGhoTreasuryRevertIfZero() public { vm.expectRevert(bytes('ZERO_ADDRESS_NOT_VALID')); GHO_ATOKEN.updateGhoTreasury(address(0)); } function testUpdateGhoTreasury() public { vm.expectEmit(true, true, true, true, address(GHO_ATOKEN)); emit GhoTreasuryUpdated(TREASURY, ALICE); GHO_ATOKEN.updateGhoTreasury(ALICE); assertEq(GHO_ATOKEN.getGhoTreasury(), ALICE); } function testUnauthorizedUpdateGhoTreasuryRevert() public { ACL_MANAGER.setState(false); vm.prank(ALICE); vm.expectRevert(bytes(Errors.CALLER_NOT_POOL_ADMIN)); GHO_ATOKEN.updateGhoTreasury(ALICE); } function testDomainSeparator() public { bytes32 EIP712_DOMAIN = keccak256( 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' ); bytes memory EIP712_REVISION = bytes('1'); bytes32 expected = keccak256( abi.encode( EIP712_DOMAIN, keccak256(bytes(GHO_ATOKEN.name())), keccak256(EIP712_REVISION), block.chainid, address(GHO_ATOKEN) ) ); bytes32 result = GHO_ATOKEN.DOMAIN_SEPARATOR(); assertEq(result, expected, 'Unexpected domain separator'); } function testNonces() public { assertEq(GHO_ATOKEN.nonces(ALICE), 0, 'Unexpected non-zero nonce'); assertEq(GHO_ATOKEN.nonces(BOB), 0, 'Unexpected non-zero nonce'); assertEq(GHO_ATOKEN.nonces(CHARLES), 0, 'Unexpected non-zero nonce'); } } ================================================ FILE: src/test/TestGhoAaveSteward.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; import {Constants} from './helpers/Constants.sol'; import {IGhoAaveSteward} from '../contracts/misc/interfaces/IGhoAaveSteward.sol'; import {IDefaultInterestRateStrategyV2, DefaultReserveInterestRateStrategyV2} from '../contracts/misc/dependencies/AaveV3-1.sol'; contract TestGhoAaveSteward is TestGhoBase { using ReserveConfiguration for DataTypes.ReserveConfigurationMap; IGhoAaveSteward.BorrowRateConfig public defaultBorrowRateConfig = IGhoAaveSteward.BorrowRateConfig({ optimalUsageRatioMaxChange: 5_00, baseVariableBorrowRateMaxChange: 5_00, variableRateSlope1MaxChange: 5_00, variableRateSlope2MaxChange: 5_00 }); IDefaultInterestRateStrategyV2.InterestRateData public defaultRateParams = IDefaultInterestRateStrategyV2.InterestRateData({ optimalUsageRatio: 1_00, baseVariableBorrowRate: 0.20e4, variableRateSlope1: 0, variableRateSlope2: 0 }); function setUp() public { // Deploy Gho Aave Steward GHO_AAVE_STEWARD = new GhoAaveSteward( SHORT_EXECUTOR, address(PROVIDER), address(MOCK_POOL_DATA_PROVIDER), address(GHO_TOKEN), RISK_COUNCIL, defaultBorrowRateConfig ); // Set a new strategy because the default is old strategy type DefaultReserveInterestRateStrategyV2 newRateStrategy = new DefaultReserveInterestRateStrategyV2( address(PROVIDER) ); CONFIGURATOR.setReserveInterestRateStrategyAddress( address(GHO_TOKEN), address(newRateStrategy), abi.encode(defaultRateParams) ); /// @dev Since block.timestamp starts at 0 this is a necessary condition (block.timestamp > `MINIMUM_DELAY`) for the timelocked contract methods to work. vm.warp(GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); } function testConstructor() public { assertEq(GHO_AAVE_STEWARD.owner(), SHORT_EXECUTOR); assertEq(GHO_AAVE_STEWARD.MINIMUM_DELAY(), MINIMUM_DELAY_V2); assertEq(GHO_AAVE_STEWARD.POOL_ADDRESSES_PROVIDER(), address(PROVIDER)); assertEq(GHO_AAVE_STEWARD.POOL_DATA_PROVIDER(), address(MOCK_POOL_DATA_PROVIDER)); assertEq(GHO_AAVE_STEWARD.GHO_TOKEN(), address(GHO_TOKEN)); assertEq(GHO_AAVE_STEWARD.RISK_COUNCIL(), RISK_COUNCIL); IGhoAaveSteward.GhoDebounce memory ghoTimelocks = GHO_AAVE_STEWARD.getGhoTimelocks(); assertEq(ghoTimelocks.ghoBorrowCapLastUpdate, 0); } function testRevertConstructorInvalidOwner() public { vm.expectRevert('INVALID_OWNER'); new GhoAaveSteward( address(0), address(0x002), address(0x003), address(0x004), address(0x005), defaultBorrowRateConfig ); } function testRevertConstructorInvalidAddressesProvider() public { vm.expectRevert('INVALID_ADDRESSES_PROVIDER'); new GhoAaveSteward( address(0x001), address(0), address(0x003), address(0x004), address(0x005), defaultBorrowRateConfig ); } function testRevertConstructorInvalidDataProvider() public { vm.expectRevert('INVALID_DATA_PROVIDER'); new GhoAaveSteward( address(0x001), address(0x002), address(0), address(0x004), address(0x005), defaultBorrowRateConfig ); } function testRevertConstructorInvalidGhoToken() public { vm.expectRevert('INVALID_GHO_TOKEN'); new GhoAaveSteward( address(0x001), address(0x002), address(0x003), address(0), address(0x005), defaultBorrowRateConfig ); } function testRevertConstructorInvalidRiskCouncil() public { vm.expectRevert('INVALID_RISK_COUNCIL'); new GhoAaveSteward( address(0x001), address(0x002), address(0x003), address(0x004), address(0), defaultBorrowRateConfig ); } function testChangeOwnership() public { address newOwner = makeAddr('newOwner'); assertEq(GHO_AAVE_STEWARD.owner(), SHORT_EXECUTOR); vm.prank(SHORT_EXECUTOR); GHO_AAVE_STEWARD.transferOwnership(newOwner); assertEq(GHO_AAVE_STEWARD.owner(), newOwner); } function testChangeOwnershipRevert() public { vm.expectRevert('Ownable: new owner is the zero address'); vm.prank(SHORT_EXECUTOR); GHO_AAVE_STEWARD.transferOwnership(address(0)); } function testUpdateGhoBorrowCap() public { uint256 oldBorrowCap = 1e6; _setGhoBorrowCapViaConfigurator(oldBorrowCap); uint256 newBorrowCap = oldBorrowCap + 1; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowCap(newBorrowCap); uint256 currentBorrowCap = _getGhoBorrowCap(); assertEq(newBorrowCap, currentBorrowCap); } function testUpdateGhoBorrowCapMaxIncrease() public { uint256 oldBorrowCap = 1e6; _setGhoBorrowCapViaConfigurator(oldBorrowCap); uint256 newBorrowCap = oldBorrowCap * 2; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowCap(newBorrowCap); uint256 currentBorrowCap = _getGhoBorrowCap(); assertEq(newBorrowCap, currentBorrowCap); } function testUpdateGhoBorrowCapMaxDecrease() public { uint256 oldBorrowCap = 1e6; _setGhoBorrowCapViaConfigurator(oldBorrowCap); vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowCap(0); uint256 currentBorrowCap = _getGhoBorrowCap(); assertEq(currentBorrowCap, 0); } function testUpdateGhoBorrowCapTimelock() public { uint256 oldBorrowCap = 1e6; _setGhoBorrowCapViaConfigurator(oldBorrowCap); vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowCap(oldBorrowCap + 1); IGhoAaveSteward.GhoDebounce memory ghoTimelocks = GHO_AAVE_STEWARD.getGhoTimelocks(); assertEq(ghoTimelocks.ghoBorrowCapLastUpdate, block.timestamp); } function testUpdateGhoBorrowCapAfterTimelock() public { uint256 oldBorrowCap = 1e6; _setGhoBorrowCapViaConfigurator(oldBorrowCap); vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowCap(oldBorrowCap + 1); skip(GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); uint256 newBorrowCap = oldBorrowCap + 2; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowCap(newBorrowCap); uint256 currentBorrowCap = _getGhoBorrowCap(); assertEq(newBorrowCap, currentBorrowCap); } function testRevertUpdateGhoBorrowCapIfUnauthorized() public { vm.prank(ALICE); vm.expectRevert('INVALID_CALLER'); GHO_AAVE_STEWARD.updateGhoBorrowCap(50e6); } function testRevertUpdateGhoBorrowCapIfUpdatedTooSoon() public { uint256 oldBorrowCap = 1e6; _setGhoBorrowCapViaConfigurator(oldBorrowCap); vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowCap(oldBorrowCap + 1); vm.prank(RISK_COUNCIL); vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); GHO_AAVE_STEWARD.updateGhoBorrowCap(oldBorrowCap + 2); } function testRevertUpdateGhoBorrowCapNoChange() public { uint256 oldBorrowCap = 1e6; _setGhoBorrowCapViaConfigurator(oldBorrowCap); vm.prank(RISK_COUNCIL); vm.expectRevert('NO_CHANGE_IN_BORROW_CAP'); GHO_AAVE_STEWARD.updateGhoBorrowCap(oldBorrowCap); } function testRevertUpdateGhoBorrowCapIfValueMoreThanDouble() public { uint256 oldBorrowCap = 1e6; _setGhoBorrowCapViaConfigurator(oldBorrowCap); vm.prank(RISK_COUNCIL); vm.expectRevert('INVALID_BORROW_CAP_UPDATE'); GHO_AAVE_STEWARD.updateGhoBorrowCap(oldBorrowCap * 2 + 1); } function testUpdateGhoSupplyCap() public { uint256 oldSupplyCap = 1e6; _setGhoSupplyCapViaConfigurator(oldSupplyCap); uint256 newSupplyCap = oldSupplyCap + 1; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoSupplyCap(newSupplyCap); uint256 currentSupplyCap = _getGhoSupplyCap(); assertEq(newSupplyCap, currentSupplyCap); } function testUpdateGhoSupplyCapMaxIncrease() public { uint256 oldSupplyCap = 1e6; _setGhoSupplyCapViaConfigurator(oldSupplyCap); uint256 newSupplyCap = oldSupplyCap * 2; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoSupplyCap(newSupplyCap); uint256 currentSupplyCap = _getGhoSupplyCap(); assertEq(newSupplyCap, currentSupplyCap); } function testUpdateGhoSupplyCapMaxDecrease() public { uint256 oldSupplyCap = 1e6; _setGhoSupplyCapViaConfigurator(oldSupplyCap); vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoSupplyCap(0); uint256 currentSupplyCap = _getGhoSupplyCap(); assertEq(currentSupplyCap, 0); } function testUpdateGhoSupplyCapTimelock() public { uint256 oldSupplyCap = 1e6; _setGhoSupplyCapViaConfigurator(oldSupplyCap); vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoSupplyCap(oldSupplyCap + 1); IGhoAaveSteward.GhoDebounce memory ghoTimelocks = GHO_AAVE_STEWARD.getGhoTimelocks(); assertEq(ghoTimelocks.ghoSupplyCapLastUpdate, block.timestamp); } function testUpdateGhoSupplyCapAfterTimelock() public { uint256 oldSupplyCap = 1e6; _setGhoSupplyCapViaConfigurator(oldSupplyCap); vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoSupplyCap(oldSupplyCap + 1); skip(GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); uint256 newSupplyCap = oldSupplyCap + 2; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoSupplyCap(newSupplyCap); uint256 currentSupplyCap = _getGhoSupplyCap(); assertEq(newSupplyCap, currentSupplyCap); } function testRevertUpdateGhoSupplyCapIfUnauthorized() public { vm.prank(ALICE); vm.expectRevert('INVALID_CALLER'); GHO_AAVE_STEWARD.updateGhoSupplyCap(50e6); } function testRevertUpdateGhoSupplyCapIfUpdatedTooSoon() public { uint256 oldSupplyCap = 1e6; _setGhoSupplyCapViaConfigurator(oldSupplyCap); vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoSupplyCap(oldSupplyCap + 1); vm.prank(RISK_COUNCIL); vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); GHO_AAVE_STEWARD.updateGhoSupplyCap(oldSupplyCap + 2); } function testRevertUpdateGhoSupplyCapNoChange() public { uint256 oldSupplyCap = 1e6; _setGhoSupplyCapViaConfigurator(oldSupplyCap); vm.prank(RISK_COUNCIL); vm.expectRevert('NO_CHANGE_IN_SUPPLY_CAP'); GHO_AAVE_STEWARD.updateGhoSupplyCap(oldSupplyCap); } function testRevertUpdateGhoSupplyCapIfValueMoreThanDouble() public { uint256 oldSupplyCap = 1e6; _setGhoSupplyCapViaConfigurator(oldSupplyCap); vm.prank(RISK_COUNCIL); vm.expectRevert('INVALID_SUPPLY_CAP_UPDATE'); GHO_AAVE_STEWARD.updateGhoSupplyCap(oldSupplyCap * 2 + 1); } function testUpdateGhoBorrowRate() public { vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, 0.21e4, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); assertEq(_getGhoBorrowRate(), 0.21e4); } function testUpdateGhoBorrowRateUpwards() public { uint32 oldBorrowRate = _getGhoBorrowRate(); uint32 newBorrowRate = oldBorrowRate + 1; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, newBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); uint32 currentBorrowRate = _getGhoBorrowRate(); assertEq(currentBorrowRate, newBorrowRate); } function testUpdateGhoBorrowRateUpwardsFromHigh() public { // set a very high borrow rate of 80% uint32 highBaseBorrowRate = 0.80e4; _setGhoBorrowRateViaConfigurator(highBaseBorrowRate); highBaseBorrowRate += 0.04e4; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, highBaseBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); assertEq(highBaseBorrowRate, _getGhoBorrowRate()); } function testUpdateGhoBorrowRateDownwards() public { uint32 oldBorrowRate = _getGhoBorrowRate(); uint32 newBorrowRate = oldBorrowRate - 1; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, newBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); uint32 currentBorrowRate = _getGhoBorrowRate(); assertEq(currentBorrowRate, newBorrowRate); } function testUpdateGhoBorrowRateDownwardsFromHigh() public { // set a very high borrow rate of 80% uint32 highBaseBorrowRate = 0.80e4; _setGhoBorrowRateViaConfigurator(highBaseBorrowRate); highBaseBorrowRate -= 0.04e4; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, highBaseBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); assertEq(highBaseBorrowRate, _getGhoBorrowRate()); } function testUpdateGhoBorrowRateMaxIncrement() public { uint32 oldBorrowRate = _getGhoBorrowRate(); uint32 newBorrowRate = oldBorrowRate + GHO_BORROW_RATE_CHANGE_MAX; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, newBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); uint32 currentBorrowRate = _getGhoBorrowRate(); assertEq(currentBorrowRate, newBorrowRate); } function testUpdateGhoBorrowRateDecrement() public { uint32 oldBorrowRate = _getGhoBorrowRate(); uint32 newBorrowRate = oldBorrowRate - 1; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, newBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); uint32 currentBorrowRate = _getGhoBorrowRate(); assertEq(currentBorrowRate, newBorrowRate); } function testUpdateGhoBorrowRateMaxDecrement() public { vm.startPrank(RISK_COUNCIL); // set a high borrow rate GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, _getGhoBorrowRate() + GHO_BORROW_RATE_CHANGE_MAX, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); vm.warp(block.timestamp + GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); uint32 oldBorrowRate = _getGhoBorrowRate(); uint32 newBorrowRate = oldBorrowRate - GHO_BORROW_RATE_CHANGE_MAX; GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, newBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); uint32 currentBorrowRate = _getGhoBorrowRate(); assertEq(currentBorrowRate, newBorrowRate); vm.stopPrank(); } function testUpdateGhoBorrowRateTimelock() public { uint32 oldBorrowRate = _getGhoBorrowRate(); vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, oldBorrowRate + 1, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); IGhoAaveSteward.GhoDebounce memory ghoTimelocks = GHO_AAVE_STEWARD.getGhoTimelocks(); assertEq(ghoTimelocks.ghoBorrowRateLastUpdate, block.timestamp); } function testUpdateGhoBorrowRateAfterTimelock() public { uint32 oldBorrowRate = _getGhoBorrowRate(); vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, oldBorrowRate + 1, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); skip(GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); uint32 newBorrowRate = oldBorrowRate + 2; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, newBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); uint32 currentBorrowRate = _getGhoBorrowRate(); assertEq(currentBorrowRate, newBorrowRate); } function testUpdateGhoBorrowRateOptimalUsageRatio() public { uint16 oldOptimalUsageRatio = _getOptimalUsageRatio(); uint16 newOptimalUsageRatio = oldOptimalUsageRatio + 1; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( newOptimalUsageRatio, defaultRateParams.baseVariableBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); uint16 currentOptimalUsageRatio = _getOptimalUsageRatio(); assertEq(currentOptimalUsageRatio, newOptimalUsageRatio); } function testRevertUpdateGhoBorrowRateOptimalUsageRatioIfMaxExceededUpwards() public { uint16 oldOptimalUsageRatio = _getOptimalUsageRatio(); uint16 newOptimalUsageRatio = oldOptimalUsageRatio + defaultBorrowRateConfig.optimalUsageRatioMaxChange + 1; vm.prank(RISK_COUNCIL); vm.expectRevert('INVALID_OPTIMAL_USAGE_RATIO'); GHO_AAVE_STEWARD.updateGhoBorrowRate( newOptimalUsageRatio, defaultRateParams.baseVariableBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); } function testRevertUpdateGhoBorrowRateOptimalUsageRatioIfMaxExceededDownwards() public { uint16 oldOptimalUsageRatio = _getOptimalUsageRatio(); uint16 newOptimalUsageRatio = oldOptimalUsageRatio + defaultBorrowRateConfig.optimalUsageRatioMaxChange; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( newOptimalUsageRatio, defaultRateParams.baseVariableBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); vm.warp(block.timestamp + GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); vm.prank(RISK_COUNCIL); vm.expectRevert('INVALID_OPTIMAL_USAGE_RATIO'); GHO_AAVE_STEWARD.updateGhoBorrowRate( newOptimalUsageRatio - defaultBorrowRateConfig.optimalUsageRatioMaxChange - 1, defaultRateParams.baseVariableBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); } function testUpdateGhoBorrowRateVariableRateSlope1() public { uint32 oldVariableRateSlope1 = _getVariableRateSlope1(); uint32 newVariableRateSlope1 = oldVariableRateSlope1 + 1; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, defaultRateParams.baseVariableBorrowRate, newVariableRateSlope1, newVariableRateSlope1 + 1 // variableRateSlope2 has to be gte variableRateSlope1 ); uint32 currentVariableRateSlope1 = _getVariableRateSlope1(); assertEq(currentVariableRateSlope1, newVariableRateSlope1); } function testRevertUpdateGhoBorrowRateVariableRateSlope1IfMaxExceededUpwards() public { uint32 oldVariableRateSlope1 = _getVariableRateSlope1(); uint32 newVariableRateSlope1 = oldVariableRateSlope1 + defaultBorrowRateConfig.variableRateSlope1MaxChange + 1; vm.prank(RISK_COUNCIL); vm.expectRevert('INVALID_VARIABLE_RATE_SLOPE1'); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, defaultRateParams.baseVariableBorrowRate, newVariableRateSlope1, defaultRateParams.variableRateSlope2 ); } function testRevertUpdateGhoBorrowRateVariableRateSlope1IfMaxExceededDownwards() public { uint32 oldVariableRateSlope1 = _getVariableRateSlope1(); uint32 newVariableRateSlope1 = oldVariableRateSlope1 + defaultBorrowRateConfig.variableRateSlope1MaxChange; _setGhoBorrowRateViaConfigurator(1); // Change Gho borrow rate to not exceed max uint32 ghoBorrowRate = _getGhoBorrowRate(); vm.startPrank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, ghoBorrowRate, newVariableRateSlope1, newVariableRateSlope1 // variableRateSlope2 has to be gte variableRateSlope1 ); newVariableRateSlope1 += 1; // Set higher than max allowed vm.warp(block.timestamp + GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, ghoBorrowRate, newVariableRateSlope1, newVariableRateSlope1 ); vm.warp(block.timestamp + GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); vm.expectRevert('INVALID_VARIABLE_RATE_SLOPE1'); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, ghoBorrowRate, newVariableRateSlope1 - defaultBorrowRateConfig.variableRateSlope1MaxChange - 1, newVariableRateSlope1 ); vm.stopPrank(); } function testUpdateGhoBorrowRateVariableRateSlope2() public { uint32 oldVariableRateSlope2 = _getVariableRateSlope2(); uint32 newVariableRateSlope2 = oldVariableRateSlope2 + 1; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, defaultRateParams.baseVariableBorrowRate, defaultRateParams.variableRateSlope1, newVariableRateSlope2 ); uint32 currentVariableRateSlope2 = _getVariableRateSlope2(); assertEq(currentVariableRateSlope2, newVariableRateSlope2); } function testRevertUpdateGhoBorrowRateVariableRateSlope2IfMaxExceededUpwards() public { uint32 oldVariableRateSlope2 = _getVariableRateSlope2(); uint32 newVariableRateSlope2 = oldVariableRateSlope2 + defaultBorrowRateConfig.variableRateSlope2MaxChange + 1; vm.prank(RISK_COUNCIL); vm.expectRevert('INVALID_VARIABLE_RATE_SLOPE2'); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, defaultRateParams.baseVariableBorrowRate, defaultRateParams.variableRateSlope1, newVariableRateSlope2 ); } function testRevertUpdateGhoBorrowRateVariableRateSlope2IfMaxExceededDownwards() public { uint32 oldVariableRateSlope2 = _getVariableRateSlope2(); uint32 newVariableRateSlope2 = oldVariableRateSlope2 + defaultBorrowRateConfig.variableRateSlope2MaxChange; _setGhoBorrowRateViaConfigurator(1); uint32 ghoBorrowRate = _getGhoBorrowRate(); vm.startPrank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, ghoBorrowRate, defaultRateParams.variableRateSlope1, newVariableRateSlope2 ); newVariableRateSlope2 += 1; // Set higher than max allowed vm.warp(block.timestamp + GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, ghoBorrowRate, defaultRateParams.variableRateSlope1, newVariableRateSlope2 ); vm.warp(block.timestamp + GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); vm.expectRevert('INVALID_VARIABLE_RATE_SLOPE2'); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, ghoBorrowRate, defaultRateParams.variableRateSlope1, newVariableRateSlope2 - defaultBorrowRateConfig.variableRateSlope2MaxChange - 1 ); vm.stopPrank(); } function testRevertUpdateGhoBorrowRateIfUnauthorized() public { vm.expectRevert('INVALID_CALLER'); vm.prank(ALICE); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, 0.07e4, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); } function testRevertUpdateGhoBorrowRateIfUpdatedTooSoon() public { uint32 oldBorrowRate = _getGhoBorrowRate(); vm.prank(RISK_COUNCIL); uint32 newBorrowRate = oldBorrowRate + 1; GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, newBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); vm.prank(RISK_COUNCIL); vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, newBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); } function testRevertUpdateGhoBorrowRateNoChange() public { uint32 oldBorrowRate = _getGhoBorrowRate(); vm.prank(RISK_COUNCIL); vm.expectRevert('NO_CHANGE_IN_RATES'); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, oldBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); } function testRevertUpdateGhoBorrowRateIfMaxExceededUpwards() public { uint32 oldBorrowRate = _getGhoBorrowRate(); uint32 newBorrowRate = oldBorrowRate + GHO_BORROW_RATE_CHANGE_MAX + 1; vm.prank(RISK_COUNCIL); vm.expectRevert('INVALID_BORROW_RATE_UPDATE'); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, newBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); } function testRevertUpdateGhoBorrowRateIfMaxExceededDownwards() public { vm.startPrank(RISK_COUNCIL); // set a high borrow rate GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, _getGhoBorrowRate() + GHO_BORROW_RATE_CHANGE_MAX, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); vm.warp(block.timestamp + GHO_AAVE_STEWARD.MINIMUM_DELAY() + 1); uint32 oldBorrowRate = _getGhoBorrowRate(); uint32 newBorrowRate = oldBorrowRate - GHO_BORROW_RATE_CHANGE_MAX - 1; vm.expectRevert('INVALID_BORROW_RATE_UPDATE'); GHO_AAVE_STEWARD.updateGhoBorrowRate( defaultRateParams.optimalUsageRatio, newBorrowRate, defaultRateParams.variableRateSlope1, defaultRateParams.variableRateSlope2 ); vm.stopPrank(); } function testSetRiskConfig() public { defaultBorrowRateConfig.optimalUsageRatioMaxChange += 1; vm.prank(SHORT_EXECUTOR); GHO_AAVE_STEWARD.setBorrowRateConfig( defaultBorrowRateConfig.optimalUsageRatioMaxChange, defaultBorrowRateConfig.baseVariableBorrowRateMaxChange, defaultBorrowRateConfig.variableRateSlope1MaxChange, defaultBorrowRateConfig.variableRateSlope2MaxChange ); IGhoAaveSteward.BorrowRateConfig memory currentBorrowRateConfig = GHO_AAVE_STEWARD .getBorrowRateConfig(); assertEq( currentBorrowRateConfig.optimalUsageRatioMaxChange, defaultBorrowRateConfig.optimalUsageRatioMaxChange ); } function _setGhoBorrowCapViaConfigurator(uint256 newBorrowCap) internal { CONFIGURATOR.setBorrowCap(address(GHO_TOKEN), newBorrowCap); } function _getGhoBorrowCap() internal view returns (uint256) { DataTypes.ReserveConfigurationMap memory configuration = POOL.getConfiguration( address(GHO_TOKEN) ); return configuration.getBorrowCap(); } function _setGhoSupplyCapViaConfigurator(uint256 newSupplyCap) internal { CONFIGURATOR.setSupplyCap(address(GHO_TOKEN), newSupplyCap); } function _getGhoSupplyCap() internal view returns (uint256) { DataTypes.ReserveConfigurationMap memory configuration = POOL.getConfiguration( address(GHO_TOKEN) ); return configuration.getSupplyCap(); } function _setGhoBorrowRateViaConfigurator(uint32 newBorrowRate) internal { IDefaultInterestRateStrategyV2.InterestRateData memory rateParams = IDefaultInterestRateStrategyV2.InterestRateData({ optimalUsageRatio: 1_00, baseVariableBorrowRate: newBorrowRate, variableRateSlope1: 0, variableRateSlope2: 0 }); CONFIGURATOR.setReserveInterestRateData(address(GHO_TOKEN), abi.encode(rateParams)); uint256 currentBorrowRate = _getGhoBorrowRate(); assertEq(currentBorrowRate, newBorrowRate); } function _getGhoBorrowRate() internal view returns (uint32) { address currentInterestRateStrategy = POOL.getReserveInterestRateStrategyAddress( address(GHO_TOKEN) ); return uint32( IDefaultInterestRateStrategyV2(currentInterestRateStrategy).getBaseVariableBorrowRate( address(GHO_TOKEN) ) / 1e23 ); // Convert to bps } function _getOptimalUsageRatio() internal view returns (uint16) { address currentInterestRateStrategy = POOL.getReserveInterestRateStrategyAddress( address(GHO_TOKEN) ); return uint16( IDefaultInterestRateStrategyV2(currentInterestRateStrategy).getOptimalUsageRatio( address(GHO_TOKEN) ) / 1e23 ); // Convert to bps } function _getVariableRateSlope1() internal view returns (uint32) { address currentInterestRateStrategy = POOL.getReserveInterestRateStrategyAddress( address(GHO_TOKEN) ); return uint32( IDefaultInterestRateStrategyV2(currentInterestRateStrategy).getVariableRateSlope1( address(GHO_TOKEN) ) / 1e23 ); // Convert to bps } function _getVariableRateSlope2() internal view returns (uint32) { address currentInterestRateStrategy = POOL.getReserveInterestRateStrategyAddress( address(GHO_TOKEN) ); return uint32( IDefaultInterestRateStrategyV2(currentInterestRateStrategy).getVariableRateSlope2( address(GHO_TOKEN) ) / 1e23 ); // Convert to bps } } ================================================ FILE: src/test/TestGhoBase.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import 'forge-std/Test.sol'; import 'forge-std/console2.sol'; // helpers import {Constants} from './helpers/Constants.sol'; import {DebtUtils} from './helpers/DebtUtils.sol'; import {Events} from './helpers/Events.sol'; import {AccessControlErrorsLib, OwnableErrorsLib} from './helpers/ErrorsLib.sol'; // generic libs import {DataTypes} from '@aave/core-v3/contracts/protocol/libraries/types/DataTypes.sol'; import {Errors} from '@aave/core-v3/contracts/protocol/libraries/helpers/Errors.sol'; import {PercentageMath} from '@aave/core-v3/contracts/protocol/libraries/math/PercentageMath.sol'; import {SafeCast} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/SafeCast.sol'; import {WadRayMath} from '@aave/core-v3/contracts/protocol/libraries/math/WadRayMath.sol'; // mocks import {MockAclManager} from './mocks/MockAclManager.sol'; import {MockConfigurator} from './mocks/MockConfigurator.sol'; import {MockFlashBorrower} from './mocks/MockFlashBorrower.sol'; import {MockGsmV2} from './mocks/MockGsmV2.sol'; import {MockPool} from './mocks/MockPool.sol'; import {MockAddressesProvider} from './mocks/MockAddressesProvider.sol'; import {MockERC4626} from './mocks/MockERC4626.sol'; import {MockUpgradeable} from './mocks/MockUpgradeable.sol'; import {PriceOracle} from '@aave/core-v3/contracts/mocks/oracle/PriceOracle.sol'; import {TestnetERC20} from '@aave/periphery-v3/contracts/mocks/testnet-helpers/TestnetERC20.sol'; import {WETH9Mock} from '@aave/periphery-v3/contracts/mocks/WETH9Mock.sol'; import {MockPoolDataProvider} from './mocks/MockPoolDataProvider.sol'; // interfaces import {IAaveIncentivesController} from '@aave/core-v3/contracts/interfaces/IAaveIncentivesController.sol'; import {IAToken} from '@aave/core-v3/contracts/interfaces/IAToken.sol'; import {IERC20} from 'aave-stk-v1-5/src/interfaces/IERC20.sol'; import {IERC3156FlashBorrower} from '@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol'; import {IERC3156FlashLender} from '@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol'; import {IERC4626} from '@openzeppelin/contracts/interfaces/IERC4626.sol'; import {IGhoToken} from '../contracts/gho/interfaces/IGhoToken.sol'; import {IGhoVariableDebtTokenTransferHook} from 'aave-stk-v1-5/src/interfaces/IGhoVariableDebtTokenTransferHook.sol'; import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; import {IStakedAaveV3} from 'aave-stk-v1-5/src/interfaces/IStakedAaveV3.sol'; // non-GHO contracts import {AdminUpgradeabilityProxy} from '@aave/core-v3/contracts/dependencies/openzeppelin/upgradeability/AdminUpgradeabilityProxy.sol'; import {ERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/ERC20.sol'; import {StakedAaveV3} from 'aave-stk-v1-5/src/contracts/StakedAaveV3.sol'; import {ReserveConfiguration} from '@aave/core-v3/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; // GHO contracts import {GhoAToken} from '../contracts/facilitators/aave/tokens/GhoAToken.sol'; import {GhoDiscountRateStrategy} from '../contracts/facilitators/aave/interestStrategy/GhoDiscountRateStrategy.sol'; import {GhoFlashMinter} from '../contracts/facilitators/flashMinter/GhoFlashMinter.sol'; import {GhoInterestRateStrategy} from '../contracts/facilitators/aave/interestStrategy/GhoInterestRateStrategy.sol'; import {IGhoAaveSteward} from '../contracts/misc/interfaces/IGhoAaveSteward.sol'; import {GhoAaveSteward} from '../contracts/misc/GhoAaveSteward.sol'; import {GhoOracle} from '../contracts/facilitators/aave/oracle/GhoOracle.sol'; import {GhoStableDebtToken} from '../contracts/facilitators/aave/tokens/GhoStableDebtToken.sol'; import {GhoToken} from '../contracts/gho/GhoToken.sol'; import {UpgradeableGhoToken} from '../contracts/gho/UpgradeableGhoToken.sol'; import {GhoVariableDebtToken} from '../contracts/facilitators/aave/tokens/GhoVariableDebtToken.sol'; import {FixedRateStrategyFactory} from '../contracts/facilitators/aave/interestStrategy/FixedRateStrategyFactory.sol'; // GSM contracts import {IGsm} from '../contracts/facilitators/gsm/interfaces/IGsm.sol'; import {Gsm} from '../contracts/facilitators/gsm/Gsm.sol'; import {Gsm4626} from '../contracts/facilitators/gsm/Gsm4626.sol'; import {FixedPriceStrategy} from '../contracts/facilitators/gsm/priceStrategy/FixedPriceStrategy.sol'; import {FixedPriceStrategy4626} from '../contracts/facilitators/gsm/priceStrategy/FixedPriceStrategy4626.sol'; import {IGsmFeeStrategy} from '../contracts/facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol'; import {FixedFeeStrategy} from '../contracts/facilitators/gsm/feeStrategy/FixedFeeStrategy.sol'; import {SampleLiquidator} from '../contracts/facilitators/gsm/misc/SampleLiquidator.sol'; import {SampleSwapFreezer} from '../contracts/facilitators/gsm/misc/SampleSwapFreezer.sol'; import {GsmRegistry} from '../contracts/facilitators/gsm/misc/GsmRegistry.sol'; import {IGhoGsmSteward} from '../contracts/misc/interfaces/IGhoGsmSteward.sol'; import {GhoGsmSteward} from '../contracts/misc/GhoGsmSteward.sol'; import {FixedFeeStrategyFactory} from '../contracts/facilitators/gsm/feeStrategy/FixedFeeStrategyFactory.sol'; // CCIP contracts import {MockUpgradeableLockReleaseTokenPool} from './mocks/MockUpgradeableLockReleaseTokenPool.sol'; import {RateLimiter} from '../contracts/misc/dependencies/Ccip.sol'; import {IGhoCcipSteward} from '../contracts/misc/interfaces/IGhoCcipSteward.sol'; import {GhoCcipSteward} from '../contracts/misc/GhoCcipSteward.sol'; import {GhoBucketSteward} from '../contracts/misc/GhoBucketSteward.sol'; contract TestGhoBase is Test, Constants, Events { using WadRayMath for uint256; using SafeCast for uint256; using PercentageMath for uint256; // helper for state tracking struct BorrowState { uint256 supplyBeforeAction; uint256 debtSupplyBeforeAction; uint256 debtScaledSupplyBeforeAction; uint256 balanceBeforeAction; uint256 debtScaledBalanceBeforeAction; uint256 debtBalanceBeforeAction; uint256 userIndexBeforeAction; uint256 userInterestsBeforeAction; uint256 assetIndexBefore; uint256 discountPercent; } GhoToken GHO_TOKEN; TestnetERC20 AAVE_TOKEN; IStakedAaveV3 STK_TOKEN; TestnetERC20 USDC_TOKEN; MockERC4626 USDC_4626_TOKEN; MockPool POOL; MockAclManager ACL_MANAGER; MockAddressesProvider PROVIDER; MockConfigurator CONFIGURATOR; PriceOracle PRICE_ORACLE; WETH9Mock WETH; GhoVariableDebtToken GHO_DEBT_TOKEN; GhoStableDebtToken GHO_STABLE_DEBT_TOKEN; GhoAToken GHO_ATOKEN; GhoFlashMinter GHO_FLASH_MINTER; GhoDiscountRateStrategy GHO_DISCOUNT_STRATEGY; MockFlashBorrower FLASH_BORROWER; Gsm GHO_GSM; Gsm4626 GHO_GSM_4626; FixedPriceStrategy GHO_GSM_FIXED_PRICE_STRATEGY; FixedPriceStrategy4626 GHO_GSM_4626_FIXED_PRICE_STRATEGY; FixedFeeStrategy GHO_GSM_FIXED_FEE_STRATEGY; SampleLiquidator GHO_GSM_LAST_RESORT_LIQUIDATOR; SampleSwapFreezer GHO_GSM_SWAP_FREEZER; GsmRegistry GHO_GSM_REGISTRY; GhoOracle GHO_ORACLE; GhoAaveSteward GHO_AAVE_STEWARD; GhoCcipSteward GHO_CCIP_STEWARD; GhoGsmSteward GHO_GSM_STEWARD; GhoBucketSteward GHO_BUCKET_STEWARD; MockPoolDataProvider MOCK_POOL_DATA_PROVIDER; FixedRateStrategyFactory FIXED_RATE_STRATEGY_FACTORY; FixedFeeStrategyFactory FIXED_FEE_STRATEGY_FACTORY; MockUpgradeableLockReleaseTokenPool GHO_TOKEN_POOL; constructor() { setupGho(); } function test_coverage_ignore() public virtual { // Intentionally left blank. // Excludes contract from coverage. } function setupGho() public { bytes memory empty; ACL_MANAGER = new MockAclManager(); PROVIDER = new MockAddressesProvider(address(ACL_MANAGER)); MOCK_POOL_DATA_PROVIDER = new MockPoolDataProvider(address(PROVIDER)); POOL = new MockPool(IPoolAddressesProvider(address(PROVIDER))); CONFIGURATOR = new MockConfigurator(IPool(POOL)); PRICE_ORACLE = new PriceOracle(); PROVIDER.setPool(address(POOL)); PROVIDER.setConfigurator(address(CONFIGURATOR)); PROVIDER.setPriceOracle(address(PRICE_ORACLE)); GHO_ORACLE = new GhoOracle(); GHO_TOKEN = new GhoToken(address(this)); GHO_TOKEN.grantRole(GHO_TOKEN_FACILITATOR_MANAGER_ROLE, address(this)); GHO_TOKEN.grantRole(GHO_TOKEN_BUCKET_MANAGER_ROLE, address(this)); AAVE_TOKEN = new TestnetERC20('AAVE', 'AAVE', 18, FAUCET); StakedAaveV3 stkAave = new StakedAaveV3( IERC20(address(AAVE_TOKEN)), IERC20(address(AAVE_TOKEN)), 1, address(0), address(0), 1 ); AdminUpgradeabilityProxy stkAaveProxy = new AdminUpgradeabilityProxy( address(stkAave), STKAAVE_PROXY_ADMIN, '' ); StakedAaveV3(address(stkAaveProxy)).initialize( STKAAVE_PROXY_ADMIN, STKAAVE_PROXY_ADMIN, STKAAVE_PROXY_ADMIN, 0, 1 ); STK_TOKEN = IStakedAaveV3(address(stkAaveProxy)); USDC_TOKEN = new TestnetERC20('USD Coin', 'USDC', 6, FAUCET); USDC_4626_TOKEN = new MockERC4626('USD Coin 4626', '4626', address(USDC_TOKEN)); IPool iPool = IPool(address(POOL)); WETH = new WETH9Mock('Wrapped Ether', 'WETH', FAUCET); GHO_DEBT_TOKEN = new GhoVariableDebtToken(iPool); GHO_STABLE_DEBT_TOKEN = new GhoStableDebtToken(iPool); GHO_ATOKEN = new GhoAToken(iPool); GHO_DEBT_TOKEN.initialize( iPool, address(GHO_TOKEN), IAaveIncentivesController(address(0)), 18, 'Aave Variable Debt GHO', 'variableDebtGHO', empty ); GHO_STABLE_DEBT_TOKEN.initialize( iPool, address(GHO_TOKEN), IAaveIncentivesController(address(0)), 18, 'Aave Stable Debt GHO', 'stableDebtGHO', empty ); GHO_ATOKEN.initialize( iPool, TREASURY, address(GHO_TOKEN), IAaveIncentivesController(address(0)), 18, 'Aave GHO', 'aGHO', empty ); GHO_ATOKEN.updateGhoTreasury(TREASURY); GHO_DEBT_TOKEN.updateDiscountToken(address(STK_TOKEN)); GHO_DISCOUNT_STRATEGY = new GhoDiscountRateStrategy(); GHO_DEBT_TOKEN.updateDiscountRateStrategy(address(GHO_DISCOUNT_STRATEGY)); GHO_DEBT_TOKEN.setAToken(address(GHO_ATOKEN)); GHO_ATOKEN.setVariableDebtToken(address(GHO_DEBT_TOKEN)); vm.prank(SHORT_EXECUTOR); STK_TOKEN.setGHODebtToken(IGhoVariableDebtTokenTransferHook(address(GHO_DEBT_TOKEN))); GHO_TOKEN.addFacilitator(address(GHO_ATOKEN), 'Aave V3 Pool', DEFAULT_CAPACITY); POOL.setGhoTokens(GHO_DEBT_TOKEN, GHO_ATOKEN); GHO_FLASH_MINTER = new GhoFlashMinter( address(GHO_TOKEN), TREASURY, DEFAULT_FLASH_FEE, address(PROVIDER) ); FLASH_BORROWER = new MockFlashBorrower(IERC3156FlashLender(GHO_FLASH_MINTER)); GHO_TOKEN.addFacilitator( address(GHO_FLASH_MINTER), 'FlashMinter Facilitator', DEFAULT_CAPACITY ); GHO_TOKEN.addFacilitator(address(FLASH_BORROWER), 'Gho Flash Borrower', DEFAULT_CAPACITY); GHO_GSM_FIXED_PRICE_STRATEGY = new FixedPriceStrategy( DEFAULT_FIXED_PRICE, address(USDC_TOKEN), 6 ); GHO_GSM_4626_FIXED_PRICE_STRATEGY = new FixedPriceStrategy4626( DEFAULT_FIXED_PRICE, address(USDC_4626_TOKEN), 6 ); GHO_GSM_LAST_RESORT_LIQUIDATOR = new SampleLiquidator(); GHO_GSM_SWAP_FREEZER = new SampleSwapFreezer(); Gsm gsm = new Gsm( address(GHO_TOKEN), address(USDC_TOKEN), address(GHO_GSM_FIXED_PRICE_STRATEGY) ); AdminUpgradeabilityProxy gsmProxy = new AdminUpgradeabilityProxy( address(gsm), SHORT_EXECUTOR, '' ); GHO_GSM = Gsm(address(gsmProxy)); GHO_GSM.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); GHO_GSM_4626 = new Gsm4626( address(GHO_TOKEN), address(USDC_4626_TOKEN), address(GHO_GSM_4626_FIXED_PRICE_STRATEGY) ); GHO_GSM_4626.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); GHO_GSM_FIXED_FEE_STRATEGY = new FixedFeeStrategy(DEFAULT_GSM_BUY_FEE, DEFAULT_GSM_SELL_FEE); GHO_GSM.updateFeeStrategy(address(GHO_GSM_FIXED_FEE_STRATEGY)); GHO_GSM_4626.updateFeeStrategy(address(GHO_GSM_FIXED_FEE_STRATEGY)); GHO_GSM.grantRole(GSM_LIQUIDATOR_ROLE, address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); GHO_GSM.grantRole(GSM_SWAP_FREEZER_ROLE, address(GHO_GSM_SWAP_FREEZER)); GHO_GSM_4626.grantRole(GSM_LIQUIDATOR_ROLE, address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); GHO_GSM_4626.grantRole(GSM_SWAP_FREEZER_ROLE, address(GHO_GSM_SWAP_FREEZER)); GHO_TOKEN.addFacilitator(address(GHO_GSM), 'GSM Facilitator', DEFAULT_CAPACITY); GHO_TOKEN.addFacilitator(address(GHO_GSM_4626), 'GSM 4626 Facilitator', DEFAULT_CAPACITY); GHO_TOKEN.addFacilitator(FAUCET, 'Faucet Facilitator', type(uint128).max); GHO_GSM_REGISTRY = new GsmRegistry(address(this)); FIXED_RATE_STRATEGY_FACTORY = new FixedRateStrategyFactory(address(PROVIDER)); // Deploy Gho Token Pool address ARM_PROXY = makeAddr('ARM_PROXY'); address OWNER = makeAddr('OWNER'); address ROUTER = makeAddr('ROUTER'); address PROXY_ADMIN = makeAddr('PROXY_ADMIN'); uint256 INITIAL_BRIDGE_LIMIT = 100e6 * 1e18; MockUpgradeableLockReleaseTokenPool tokenPoolImpl = new MockUpgradeableLockReleaseTokenPool( address(GHO_TOKEN), ARM_PROXY, false, true ); // proxy deploy and init address[] memory emptyArray = new address[](0); bytes memory tokenPoolInitParams = abi.encodeWithSignature( 'initialize(address,address[],address,uint256)', OWNER, emptyArray, ROUTER, INITIAL_BRIDGE_LIMIT ); TransparentUpgradeableProxy tokenPoolProxy = new TransparentUpgradeableProxy( address(tokenPoolImpl), PROXY_ADMIN, tokenPoolInitParams ); // Manage ownership vm.prank(OWNER); MockUpgradeableLockReleaseTokenPool(address(tokenPoolProxy)).acceptOwnership(); GHO_TOKEN_POOL = MockUpgradeableLockReleaseTokenPool(address(tokenPoolProxy)); // Setup GHO Token Pool uint64 SOURCE_CHAIN_SELECTOR = 1; uint64 DEST_CHAIN_SELECTOR = 2; RateLimiter.Config memory initialOutboundRateLimit = RateLimiter.Config({ isEnabled: true, capacity: 100e28, rate: 1e15 }); RateLimiter.Config memory initialInboundRateLimit = RateLimiter.Config({ isEnabled: true, capacity: 222e30, rate: 1e18 }); MockUpgradeableLockReleaseTokenPool.ChainUpdate[] memory chainUpdate = new MockUpgradeableLockReleaseTokenPool.ChainUpdate[](1); chainUpdate[0] = MockUpgradeableLockReleaseTokenPool.ChainUpdate({ remoteChainSelector: DEST_CHAIN_SELECTOR, allowed: true, outboundRateLimiterConfig: initialOutboundRateLimit, inboundRateLimiterConfig: initialInboundRateLimit }); vm.prank(OWNER); GHO_TOKEN_POOL.applyChainUpdates(chainUpdate); } function ghoFaucet(address to, uint256 amount) public { vm.prank(FAUCET); GHO_TOKEN.mint(to, amount); } function borrowAction(address user, uint256 amount) public { borrowActionOnBehalf(user, user, amount); } function borrowActionOnBehalf(address caller, address onBehalfOf, uint256 amount) public { BorrowState memory bs; bs.supplyBeforeAction = GHO_TOKEN.totalSupply(); bs.debtSupplyBeforeAction = GHO_DEBT_TOKEN.totalSupply(); bs.debtScaledSupplyBeforeAction = GHO_DEBT_TOKEN.scaledTotalSupply(); bs.balanceBeforeAction = GHO_TOKEN.balanceOf(onBehalfOf); bs.debtScaledBalanceBeforeAction = GHO_DEBT_TOKEN.scaledBalanceOf(onBehalfOf); bs.debtBalanceBeforeAction = GHO_DEBT_TOKEN.balanceOf(onBehalfOf); bs.userIndexBeforeAction = GHO_DEBT_TOKEN.getPreviousIndex(onBehalfOf); bs.userInterestsBeforeAction = GHO_DEBT_TOKEN.getBalanceFromInterest(onBehalfOf); bs.assetIndexBefore = POOL.getReserveNormalizedVariableDebt(address(GHO_TOKEN)); bs.discountPercent = GHO_DEBT_TOKEN.getDiscountPercent(onBehalfOf); if (bs.userIndexBeforeAction == 0) { bs.userIndexBeforeAction = 1e27; } (uint256 computedInterest, uint256 discountScaled, ) = DebtUtils.computeDebt( bs.userIndexBeforeAction, bs.assetIndexBefore, bs.debtScaledBalanceBeforeAction, bs.userInterestsBeforeAction, bs.discountPercent ); uint256 newDiscountRate = GHO_DISCOUNT_STRATEGY.calculateDiscountRate( (bs.debtScaledBalanceBeforeAction - discountScaled).rayMul(bs.assetIndexBefore) + amount, IERC20(address(STK_TOKEN)).balanceOf(onBehalfOf) ); if (newDiscountRate != bs.discountPercent) { vm.expectEmit(true, true, true, true, address(GHO_DEBT_TOKEN)); emit DiscountPercentUpdated(onBehalfOf, bs.discountPercent, newDiscountRate); } vm.expectEmit(true, true, true, true, address(GHO_DEBT_TOKEN)); emit Transfer(address(0), onBehalfOf, amount + computedInterest); vm.expectEmit(true, true, true, true, address(GHO_DEBT_TOKEN)); emit Mint(caller, onBehalfOf, amount + computedInterest, computedInterest, bs.assetIndexBefore); // Action vm.prank(caller); POOL.borrow(address(GHO_TOKEN), amount, 2, 0, onBehalfOf); // Checks assertEq( GHO_TOKEN.balanceOf(onBehalfOf), bs.balanceBeforeAction + amount, 'Gho amount does not match borrow' ); assertEq(GHO_DEBT_TOKEN.getDiscountPercent(onBehalfOf), newDiscountRate); assertEq( GHO_TOKEN.totalSupply(), bs.supplyBeforeAction + amount, 'Gho total supply does not match borrow' ); assertEq( GHO_DEBT_TOKEN.scaledBalanceOf(onBehalfOf), bs.debtScaledBalanceBeforeAction + amount.rayDiv(bs.assetIndexBefore) - discountScaled, 'Gho debt token balance does not match borrow' ); assertEq( GHO_DEBT_TOKEN.scaledTotalSupply(), bs.debtScaledSupplyBeforeAction + amount.rayDiv(bs.assetIndexBefore) - discountScaled, 'Gho debt token Supply does not match borrow' ); assertEq( GHO_DEBT_TOKEN.getBalanceFromInterest(onBehalfOf), bs.userInterestsBeforeAction + computedInterest, 'Gho debt interests does not match borrow' ); } function repayAction(address user, uint256 amount) public { BorrowState memory bs; bs.supplyBeforeAction = GHO_TOKEN.totalSupply(); bs.debtSupplyBeforeAction = GHO_DEBT_TOKEN.totalSupply(); bs.debtScaledSupplyBeforeAction = GHO_DEBT_TOKEN.scaledTotalSupply(); bs.balanceBeforeAction = GHO_TOKEN.balanceOf(user); bs.debtScaledBalanceBeforeAction = GHO_DEBT_TOKEN.scaledBalanceOf(user); bs.debtBalanceBeforeAction = GHO_DEBT_TOKEN.balanceOf(user); bs.userIndexBeforeAction = GHO_DEBT_TOKEN.getPreviousIndex(user); bs.userInterestsBeforeAction = GHO_DEBT_TOKEN.getBalanceFromInterest(user); bs.assetIndexBefore = POOL.getReserveNormalizedVariableDebt(address(GHO_TOKEN)); bs.discountPercent = GHO_DEBT_TOKEN.getDiscountPercent(user); uint256 expectedDebt = 0; uint256 expectedBurnOffset = 0; if (bs.userIndexBeforeAction == 0) { bs.userIndexBeforeAction = 1e27; } (uint256 computedInterest, uint256 discountScaled, ) = DebtUtils.computeDebt( bs.userIndexBeforeAction, bs.assetIndexBefore, bs.debtScaledBalanceBeforeAction, bs.userInterestsBeforeAction, bs.discountPercent ); uint256 newDiscountRate = GHO_DISCOUNT_STRATEGY.calculateDiscountRate( (bs.debtScaledBalanceBeforeAction - discountScaled).rayMul(bs.assetIndexBefore) - amount, IERC20(address(STK_TOKEN)).balanceOf(user) ); if (amount <= (bs.userInterestsBeforeAction + computedInterest)) { expectedDebt = bs.userInterestsBeforeAction + computedInterest - amount; } else { expectedBurnOffset = amount - bs.userInterestsBeforeAction + computedInterest; } // Action vm.startPrank(user); GHO_TOKEN.approve(address(POOL), amount); if (newDiscountRate != bs.discountPercent) { vm.expectEmit(true, true, true, true, address(GHO_DEBT_TOKEN)); emit DiscountPercentUpdated(user, bs.discountPercent, newDiscountRate); } if (computedInterest > amount) { vm.expectEmit(true, true, true, true, address(GHO_DEBT_TOKEN)); emit Transfer(address(0), user, computedInterest - amount); } else { vm.expectEmit(true, true, true, true, address(GHO_DEBT_TOKEN)); emit Transfer(user, address(0), amount - computedInterest); } POOL.repay(address(GHO_TOKEN), amount, 2, user); vm.stopPrank(); // Checks assertEq( GHO_TOKEN.balanceOf(user), bs.balanceBeforeAction - amount, 'Gho amount does not match repay' ); assertEq(GHO_DEBT_TOKEN.getDiscountPercent(user), newDiscountRate); if (expectedBurnOffset != 0) { assertEq( GHO_TOKEN.totalSupply(), bs.supplyBeforeAction - amount + computedInterest + bs.userInterestsBeforeAction, 'Gho total supply does not match repay b' ); } else { assertEq( GHO_TOKEN.totalSupply(), bs.supplyBeforeAction, 'Gho total supply does not match repay a' ); } assertEq( GHO_DEBT_TOKEN.scaledBalanceOf(user), bs.debtScaledBalanceBeforeAction - amount.rayDiv(bs.assetIndexBefore) - discountScaled, 'Gho debt token balance does not match repay' ); assertEq( GHO_DEBT_TOKEN.scaledTotalSupply(), bs.debtScaledSupplyBeforeAction - amount.rayDiv(bs.assetIndexBefore) - discountScaled, 'Gho debt token Supply does not match repay' ); assertEq( GHO_DEBT_TOKEN.getBalanceFromInterest(user), expectedDebt, 'Gho debt interests does not match repay' ); } function mintAndStakeDiscountToken(address user, uint256 amount) public { vm.prank(FAUCET); AAVE_TOKEN.mint(user, amount); vm.startPrank(user); AAVE_TOKEN.approve(address(STK_TOKEN), amount); STK_TOKEN.stake(user, amount); vm.stopPrank(); } function rebalanceDiscountAction(address user) public { BorrowState memory bs; bs.supplyBeforeAction = GHO_TOKEN.totalSupply(); bs.debtSupplyBeforeAction = GHO_DEBT_TOKEN.totalSupply(); bs.debtScaledSupplyBeforeAction = GHO_DEBT_TOKEN.scaledTotalSupply(); bs.balanceBeforeAction = GHO_TOKEN.balanceOf(user); bs.debtScaledBalanceBeforeAction = GHO_DEBT_TOKEN.scaledBalanceOf(user); bs.debtBalanceBeforeAction = GHO_DEBT_TOKEN.balanceOf(user); bs.userIndexBeforeAction = GHO_DEBT_TOKEN.getPreviousIndex(user); bs.userInterestsBeforeAction = GHO_DEBT_TOKEN.getBalanceFromInterest(user); bs.assetIndexBefore = POOL.getReserveNormalizedVariableDebt(address(GHO_TOKEN)); bs.discountPercent = GHO_DEBT_TOKEN.getDiscountPercent(user); if (bs.userIndexBeforeAction == 0) { bs.userIndexBeforeAction = 1e27; } (uint256 computedInterest, uint256 discountScaled, ) = DebtUtils.computeDebt( bs.userIndexBeforeAction, bs.assetIndexBefore, bs.debtScaledBalanceBeforeAction, bs.userInterestsBeforeAction, bs.discountPercent ); uint256 newDiscountRate = GHO_DISCOUNT_STRATEGY.calculateDiscountRate( (bs.debtScaledBalanceBeforeAction - discountScaled).rayMul(bs.assetIndexBefore), IERC20(address(STK_TOKEN)).balanceOf(user) ); if (newDiscountRate != bs.discountPercent) { vm.expectEmit(true, true, true, true, address(GHO_DEBT_TOKEN)); emit DiscountPercentUpdated(user, bs.discountPercent, newDiscountRate); } vm.expectEmit(true, true, true, true, address(GHO_DEBT_TOKEN)); emit Transfer(address(0), user, computedInterest); vm.expectEmit(true, true, true, true, address(GHO_DEBT_TOKEN)); emit Mint(address(0), user, computedInterest, computedInterest, bs.assetIndexBefore); // Action vm.prank(user); GHO_DEBT_TOKEN.rebalanceUserDiscountPercent(user); // Checks assertEq( GHO_TOKEN.balanceOf(user), bs.balanceBeforeAction, 'Gho amount does not match rebalance' ); assertEq(GHO_DEBT_TOKEN.getDiscountPercent(user), newDiscountRate); assertEq( GHO_TOKEN.totalSupply(), bs.supplyBeforeAction, 'Gho total supply does not match rebalance' ); assertEq( GHO_DEBT_TOKEN.scaledBalanceOf(user), bs.debtScaledBalanceBeforeAction - discountScaled, 'Gho debt token balance does not match rebalance' ); assertEq( GHO_DEBT_TOKEN.scaledTotalSupply(), bs.debtScaledSupplyBeforeAction - discountScaled, 'Gho debt token Supply does not match borrow' ); assertEq( GHO_DEBT_TOKEN.getBalanceFromInterest(user), bs.userInterestsBeforeAction + computedInterest, 'Gho debt interests does not match borrow' ); } /// Helper function to sell asset in the GSM function _sellAsset( Gsm gsm, TestnetERC20 token, address receiver, uint256 amount ) internal returns (uint256) { vm.startPrank(FAUCET); token.mint(FAUCET, amount); token.approve(address(gsm), amount); (, uint256 ghoBought) = gsm.sellAsset(amount, receiver); vm.stopPrank(); return ghoBought; } /// Helper function to mint an amount of assets of an ERC4626 token function _mintVaultAssets( MockERC4626 vault, TestnetERC20 token, address receiver, uint256 amount ) internal { vm.startPrank(FAUCET); token.mint(FAUCET, amount); token.approve(address(vault), amount); vault.deposit(amount, receiver); vm.stopPrank(); } /// Helper function to mint an amount of shares of an ERC4626 token function _mintVaultShares( MockERC4626 vault, TestnetERC20 token, address receiver, uint256 sharesAmount ) internal { uint256 assets = vault.previewMint(sharesAmount); vm.startPrank(FAUCET); token.mint(FAUCET, assets); token.approve(address(vault), assets); vault.deposit(assets, receiver); vm.stopPrank(); } /// Helper function to sell shares of an ERC4626 token in the GSM function _sellAsset( Gsm4626 gsm, MockERC4626 vault, TestnetERC20 token, address receiver, uint256 amount ) internal returns (uint256) { uint256 assetsToMint = vault.previewRedeem(amount); _mintVaultAssets(vault, token, address(this), assetsToMint); vault.approve(address(gsm), amount); (, uint256 ghoBought) = gsm.sellAsset(amount, receiver); return ghoBought; } /// Helper function to alter the exchange rate between shares and assets in a ERC4626 vault function _changeExchangeRate( MockERC4626 vault, TestnetERC20 token, uint256 amount, bool inflate ) internal { if (inflate) { // Inflate vm.prank(FAUCET); token.mint(address(vault), amount); } else { // Deflate vm.prank(address(vault)); token.transfer(address(1), amount); } } function _contains(address[] memory list, address item) internal pure returns (bool) { for (uint256 i = 0; i < list.length; i++) { if (list[i] == item) { return true; } } return false; } function getProxyAdminAddress(address proxy) internal view returns (address) { bytes32 adminSlot = vm.load(proxy, ERC1967_ADMIN_SLOT); return address(uint160(uint256(adminSlot))); } function getProxyImplementationAddress(address proxy) internal view returns (address) { bytes32 implSlot = vm.load(proxy, ERC1967_IMPLEMENTATION_SLOT); return address(uint160(uint256(implSlot))); } } ================================================ FILE: src/test/TestGhoBucketSteward.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGhoBucketSteward is TestGhoBase { function setUp() public { // Deploy Gho Bucket Steward GHO_BUCKET_STEWARD = new GhoBucketSteward(SHORT_EXECUTOR, address(GHO_TOKEN), RISK_COUNCIL); address[] memory controlledFacilitators = new address[](2); controlledFacilitators[0] = address(GHO_ATOKEN); controlledFacilitators[1] = address(GHO_GSM); vm.prank(SHORT_EXECUTOR); GHO_BUCKET_STEWARD.setControlledFacilitator(controlledFacilitators, true); /// @dev Since block.timestamp starts at 0 this is a necessary condition (block.timestamp > `MINIMUM_DELAY`) for the timelocked contract methods to work. vm.warp(GHO_BUCKET_STEWARD.MINIMUM_DELAY() + 1); // Grant roles GHO_TOKEN.grantRole(GHO_TOKEN_BUCKET_MANAGER_ROLE, address(GHO_BUCKET_STEWARD)); } function testConstructor() public { assertEq(GHO_BUCKET_STEWARD.owner(), SHORT_EXECUTOR); assertEq(GHO_BUCKET_STEWARD.GHO_TOKEN(), address(GHO_TOKEN)); assertEq(GHO_BUCKET_STEWARD.RISK_COUNCIL(), RISK_COUNCIL); address[] memory controlledFacilitators = GHO_BUCKET_STEWARD.getControlledFacilitators(); assertEq(controlledFacilitators.length, 2); uint40 facilitatorTimelock = GHO_BUCKET_STEWARD.getFacilitatorBucketCapacityTimelock( controlledFacilitators[0] ); assertEq(facilitatorTimelock, 0); } function testRevertConstructorInvalidOwner() public { vm.expectRevert('INVALID_OWNER'); new GhoBucketSteward(address(0), address(0x002), address(0x003)); } function testRevertConstructorInvalidGhoToken() public { vm.expectRevert('INVALID_GHO_TOKEN'); new GhoBucketSteward(address(0x001), address(0), address(0x003)); } function testRevertConstructorInvalidRiskCouncil() public { vm.expectRevert('INVALID_RISK_COUNCIL'); new GhoBucketSteward(address(0x001), address(0x002), address(0)); } function testChangeOwnership() public { address newOwner = makeAddr('newOwner'); assertEq(GHO_BUCKET_STEWARD.owner(), SHORT_EXECUTOR); vm.prank(SHORT_EXECUTOR); GHO_BUCKET_STEWARD.transferOwnership(newOwner); assertEq(GHO_BUCKET_STEWARD.owner(), newOwner); } function testChangeOwnershipRevert() public { vm.expectRevert('Ownable: new owner is the zero address'); vm.prank(SHORT_EXECUTOR); GHO_BUCKET_STEWARD.transferOwnership(address(0)); } function testUpdateFacilitatorBucketCapacity() public { (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); vm.prank(RISK_COUNCIL); uint128 newBucketCapacity = uint128(currentBucketCapacity) + 1; GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity(address(GHO_ATOKEN), newBucketCapacity); (uint256 capacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); assertEq(newBucketCapacity, capacity); } function testUpdateFacilitatorBucketCapacityMaxValue() public { (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); uint128 newBucketCapacity = uint128(currentBucketCapacity * 2); vm.prank(RISK_COUNCIL); GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity(address(GHO_ATOKEN), newBucketCapacity); (uint256 capacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); assertEq(capacity, newBucketCapacity); } function testUpdateFacilitatorBucketCapacityTimelock() public { (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); vm.prank(RISK_COUNCIL); GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( address(GHO_ATOKEN), uint128(currentBucketCapacity) + 1 ); uint40 timelock = GHO_BUCKET_STEWARD.getFacilitatorBucketCapacityTimelock(address(GHO_ATOKEN)); assertEq(timelock, block.timestamp); } function testUpdateFacilitatorBucketCapacityAfterTimelock() public { (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); vm.prank(RISK_COUNCIL); uint128 newBucketCapacity = uint128(currentBucketCapacity) + 1; GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity(address(GHO_ATOKEN), newBucketCapacity); skip(GHO_BUCKET_STEWARD.MINIMUM_DELAY() + 1); uint128 newBucketCapacityAfterTimelock = newBucketCapacity + 1; vm.prank(RISK_COUNCIL); GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( address(GHO_ATOKEN), newBucketCapacityAfterTimelock ); (uint256 capacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); assertEq(capacity, newBucketCapacityAfterTimelock); } function testRevertUpdateFacilitatorBucketCapacityIfUnauthorized() public { vm.expectRevert('INVALID_CALLER'); vm.prank(ALICE); GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity(address(GHO_ATOKEN), 123); } function testRevertUpdateFacilitatorBucketCapacityIfUpdatedTooSoon() public { (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); vm.prank(RISK_COUNCIL); GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( address(GHO_ATOKEN), uint128(currentBucketCapacity) + 1 ); vm.prank(RISK_COUNCIL); vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( address(GHO_ATOKEN), uint128(currentBucketCapacity) + 2 ); } function testRevertUpdateFacilitatorBucketCapacityNoChange() public { (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); vm.prank(RISK_COUNCIL); vm.expectRevert('NO_CHANGE_IN_BUCKET_CAPACITY'); GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( address(GHO_ATOKEN), uint128(currentBucketCapacity) ); } function testRevertUpdateFacilitatorBucketCapacityIfFacilitatorNotInControl() public { (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); vm.prank(RISK_COUNCIL); vm.expectRevert('FACILITATOR_NOT_CONTROLLED'); GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( address(GHO_GSM_4626), uint128(currentBucketCapacity) + 1 ); } function testRevertUpdateFacilitatorBucketCapacityIfStewardLostBucketManagerRole() public { (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); GHO_TOKEN.revokeRole(GHO_TOKEN_BUCKET_MANAGER_ROLE, address(GHO_BUCKET_STEWARD)); vm.expectRevert( AccessControlErrorsLib.MISSING_ROLE( GHO_TOKEN_BUCKET_MANAGER_ROLE, address(GHO_BUCKET_STEWARD) ) ); vm.prank(RISK_COUNCIL); GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( address(GHO_ATOKEN), uint128(currentBucketCapacity) + 1 ); } function testRevertUpdateFacilitatorBucketCapacityIfMoreThanDouble() public { (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); vm.prank(RISK_COUNCIL); vm.expectRevert('INVALID_BUCKET_CAPACITY_UPDATE'); GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity( address(GHO_ATOKEN), uint128(currentBucketCapacity * 2) + 1 ); } function testRevertUpdateFacilitatorBucketCapacityDecrement() public { (uint256 currentBucketCapacity, ) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); vm.prank(RISK_COUNCIL); uint128 newBucketCapacity = uint128(currentBucketCapacity) - 1; vm.expectRevert('INVALID_BUCKET_CAPACITY_UPDATE'); GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity(address(GHO_ATOKEN), newBucketCapacity); } function testSetControlledFacilitatorAdd() public { address[] memory oldControlledFacilitators = GHO_BUCKET_STEWARD.getControlledFacilitators(); address[] memory newGsmList = new address[](1); newGsmList[0] = address(GHO_GSM_4626); vm.prank(SHORT_EXECUTOR); GHO_BUCKET_STEWARD.setControlledFacilitator(newGsmList, true); address[] memory newControlledFacilitators = GHO_BUCKET_STEWARD.getControlledFacilitators(); assertEq(newControlledFacilitators.length, oldControlledFacilitators.length + 1); assertTrue(_contains(newControlledFacilitators, address(GHO_GSM_4626))); } function testSetControlledFacilitatorsRemove() public { address[] memory oldControlledFacilitators = GHO_BUCKET_STEWARD.getControlledFacilitators(); address[] memory disableGsmList = new address[](1); disableGsmList[0] = address(GHO_GSM); vm.prank(SHORT_EXECUTOR); GHO_BUCKET_STEWARD.setControlledFacilitator(disableGsmList, false); address[] memory newControlledFacilitators = GHO_BUCKET_STEWARD.getControlledFacilitators(); assertEq(newControlledFacilitators.length, oldControlledFacilitators.length - 1); assertFalse(_contains(newControlledFacilitators, address(GHO_GSM))); } function testRevertSetControlledFacilitatorIfUnauthorized() public { vm.expectRevert(OwnableErrorsLib.CALLER_NOT_OWNER()); vm.prank(RISK_COUNCIL); address[] memory newGsmList = new address[](1); newGsmList[0] = address(GHO_GSM_4626); GHO_BUCKET_STEWARD.setControlledFacilitator(newGsmList, true); } function testIsControlledFacilitator() public { address facilitator = makeAddr('FACILITATOR'); address[] memory controlledFacilitators = new address[](1); controlledFacilitators[0] = facilitator; vm.prank(SHORT_EXECUTOR); GHO_BUCKET_STEWARD.setControlledFacilitator(controlledFacilitators, true); assertTrue(GHO_BUCKET_STEWARD.isControlledFacilitator(facilitator)); address nonFacilitator = makeAddr('NON_FACILITATOR'); assertFalse(GHO_BUCKET_STEWARD.isControlledFacilitator(nonFacilitator)); } } ================================================ FILE: src/test/TestGhoCcipSteward.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; import {RateLimiter} from '../contracts/misc/dependencies/Ccip.sol'; contract TestGhoCcipSteward is TestGhoBase { RateLimiter.Config rateLimitConfig = RateLimiter.Config({isEnabled: true, capacity: type(uint128).max, rate: 1e15}); uint64 remoteChainSelector = 2; event ChainConfigured( uint64 remoteChainSelector, RateLimiter.Config outboundRateLimiterConfig, RateLimiter.Config inboundRateLimiterConfig ); function setUp() public { // Deploy Gho CCIP Steward GHO_CCIP_STEWARD = new GhoCcipSteward( address(GHO_TOKEN), address(GHO_TOKEN_POOL), RISK_COUNCIL, true ); /// @dev Since block.timestamp starts at 0 this is a necessary condition (block.timestamp > `MINIMUM_DELAY`) for the timelocked contract methods to work. vm.warp(GHO_CCIP_STEWARD.MINIMUM_DELAY() + 1); // Grant accesses to the Steward vm.startPrank(GHO_TOKEN_POOL.owner()); GHO_TOKEN_POOL.setRateLimitAdmin(address(GHO_CCIP_STEWARD)); GHO_TOKEN_POOL.setBridgeLimitAdmin(address(GHO_CCIP_STEWARD)); vm.stopPrank(); } function testConstructor() public { assertEq(GHO_CCIP_STEWARD.MINIMUM_DELAY(), MINIMUM_DELAY_V2); assertEq(GHO_CCIP_STEWARD.GHO_TOKEN(), address(GHO_TOKEN)); assertEq(GHO_CCIP_STEWARD.GHO_TOKEN_POOL(), address(GHO_TOKEN_POOL)); assertEq(GHO_CCIP_STEWARD.RISK_COUNCIL(), RISK_COUNCIL); } function testRevertConstructorInvalidGhoToken() public { vm.expectRevert('INVALID_GHO_TOKEN'); new GhoCcipSteward(address(0), address(0x002), address(0x003), true); } function testRevertConstructorInvalidGhoTokenPool() public { vm.expectRevert('INVALID_GHO_TOKEN_POOL'); new GhoCcipSteward(address(0x001), address(0), address(0x003), true); } function testRevertConstructorInvalidRiskCouncil() public { vm.expectRevert('INVALID_RISK_COUNCIL'); new GhoCcipSteward(address(0x001), address(0x002), address(0), true); } function testUpdateBridgeLimit() public { uint256 oldBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); uint256 newBridgeLimit = oldBridgeLimit + 1; vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); uint256 currentBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); assertEq(currentBridgeLimit, newBridgeLimit); } function testRevertUpdateBridgeLimitIfUnauthorized() public { uint256 oldBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); uint256 newBridgeLimit = oldBridgeLimit + 1; vm.prank(ALICE); vm.expectRevert('INVALID_CALLER'); GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); } function testRevertUpdateBridgeLimitIfUpdatedTooSoon() public { uint256 oldBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); uint256 newBridgeLimit = oldBridgeLimit + 1; vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); } function testRevertUpdateBridgeLimitNoChange() public { uint256 oldBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); vm.prank(RISK_COUNCIL); vm.expectRevert('NO_CHANGE_IN_BRIDGE_LIMIT'); GHO_CCIP_STEWARD.updateBridgeLimit(oldBridgeLimit); } function testRevertUpdateBridgeLimitIfDisabled() public { // Deploy new Gho CCIP Steward with bridge limit disabled GHO_CCIP_STEWARD = new GhoCcipSteward( address(GHO_TOKEN), address(GHO_TOKEN_POOL), RISK_COUNCIL, false ); /// @dev Since block.timestamp starts at 0 this is a necessary condition (block.timestamp > `MINIMUM_DELAY`) for the timelocked contract methods to work. vm.warp(GHO_CCIP_STEWARD.MINIMUM_DELAY() + 1); // Grant accesses to the Steward vm.startPrank(GHO_TOKEN_POOL.owner()); GHO_TOKEN_POOL.setRateLimitAdmin(address(GHO_CCIP_STEWARD)); GHO_TOKEN_POOL.setBridgeLimitAdmin(address(GHO_CCIP_STEWARD)); vm.stopPrank(); uint256 oldBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); uint256 newBridgeLimit = oldBridgeLimit + 1; vm.expectRevert('BRIDGE_LIMIT_DISABLED'); vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); } function testUpdateBridgeLimitTooHigh() public { uint256 oldBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); uint256 newBridgeLimit = (oldBridgeLimit + 1) * 2; vm.prank(RISK_COUNCIL); vm.expectRevert('INVALID_BRIDGE_LIMIT_UPDATE'); GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); } function testUpdateBridgeLimitFuzz(uint256 newBridgeLimit) public { uint256 oldBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); newBridgeLimit = bound(newBridgeLimit, 0, oldBridgeLimit * 2); vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); uint256 currentBridgeLimit = GHO_TOKEN_POOL.getBridgeLimit(); assertEq(currentBridgeLimit, newBridgeLimit); } function testUpdateRateLimit() public { RateLimiter.TokenBucket memory outboundConfig = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentOutboundRateLimiterState(remoteChainSelector); RateLimiter.TokenBucket memory inboundConfig = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentInboundRateLimiterState(remoteChainSelector); RateLimiter.Config memory newOutboundConfig = RateLimiter.Config({ isEnabled: true, capacity: outboundConfig.capacity + 1, rate: outboundConfig.rate + 1 }); RateLimiter.Config memory newInboundConfig = RateLimiter.Config({ isEnabled: true, capacity: inboundConfig.capacity + 1, rate: inboundConfig.rate + 1 }); vm.expectEmit(false, false, false, true); emit ChainConfigured(remoteChainSelector, newOutboundConfig, newInboundConfig); vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, newOutboundConfig.isEnabled, newOutboundConfig.capacity, newOutboundConfig.rate, newInboundConfig.isEnabled, newInboundConfig.capacity, newInboundConfig.rate ); } function testRevertUpdateRateLimitIfUnauthorized() public { vm.prank(ALICE); vm.expectRevert('INVALID_CALLER'); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, rateLimitConfig.isEnabled, rateLimitConfig.capacity, rateLimitConfig.rate, rateLimitConfig.isEnabled, rateLimitConfig.capacity, rateLimitConfig.rate ); } function testRevertUpdateRateLimitIfUpdatedTooSoon() public { RateLimiter.TokenBucket memory outboundConfig = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentOutboundRateLimiterState(remoteChainSelector); RateLimiter.TokenBucket memory inboundConfig = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentInboundRateLimiterState(remoteChainSelector); vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, outboundConfig.isEnabled, outboundConfig.capacity + 1, outboundConfig.rate, inboundConfig.isEnabled, inboundConfig.capacity, inboundConfig.rate ); vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, outboundConfig.isEnabled, outboundConfig.capacity + 2, outboundConfig.rate, inboundConfig.isEnabled, inboundConfig.capacity, inboundConfig.rate ); } function testRevertUpdateRateLimitNoChange() public { RateLimiter.TokenBucket memory outboundConfig = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentOutboundRateLimiterState(remoteChainSelector); RateLimiter.TokenBucket memory inboundConfig = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentInboundRateLimiterState(remoteChainSelector); vm.prank(RISK_COUNCIL); vm.expectRevert('NO_CHANGE_IN_RATE_LIMIT'); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, outboundConfig.isEnabled, outboundConfig.capacity, outboundConfig.rate, inboundConfig.isEnabled, inboundConfig.capacity, inboundConfig.rate ); } function testRevertUpdateRateLimitToZero() public { RateLimiter.Config memory invalidConfig = RateLimiter.Config({ isEnabled: true, capacity: 0, rate: 0 }); // reverts because capacity or rate cannot be set to 0 when rate limit is enabled // this check is enforced on the token pool (see RateLimiter._validateTokenBucketConfig) vm.prank(RISK_COUNCIL); vm.expectRevert( abi.encodeWithSelector(RateLimiter.InvalidRatelimitRate.selector, invalidConfig) ); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, invalidConfig.isEnabled, invalidConfig.capacity, invalidConfig.rate, invalidConfig.isEnabled, invalidConfig.capacity, invalidConfig.rate ); invalidConfig.rate = 10; vm.prank(RISK_COUNCIL); vm.expectRevert( abi.encodeWithSelector(RateLimiter.InvalidRatelimitRate.selector, invalidConfig) ); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, invalidConfig.isEnabled, invalidConfig.capacity, invalidConfig.rate, invalidConfig.isEnabled, invalidConfig.capacity, invalidConfig.rate ); } function testRevertUpdateRateLimitToZeroWhenDisabled() public { RateLimiter.Config memory invalidConfig = RateLimiter.Config({ isEnabled: false, capacity: 10, rate: 0 }); // reverts because capacity and rate both have be set to 0 when rate limit is disabled // this check is enforced on the token pool (see RateLimiter._validateTokenBucketConfig) vm.prank(RISK_COUNCIL); vm.expectRevert( abi.encodeWithSelector(RateLimiter.DisabledNonZeroRateLimit.selector, invalidConfig) ); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, invalidConfig.isEnabled, invalidConfig.capacity, invalidConfig.rate, invalidConfig.isEnabled, invalidConfig.capacity, invalidConfig.rate ); } function testDisableRateLimit() public { RateLimiter.TokenBucket memory outboundConfig = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentOutboundRateLimiterState(remoteChainSelector); RateLimiter.TokenBucket memory inboundConfig = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentInboundRateLimiterState(remoteChainSelector); // assert both inbound & outbound rate limiters are enabled assertTrue(outboundConfig.isEnabled); assertGt(outboundConfig.capacity, 0); assertGt(outboundConfig.rate, 0); assertTrue(inboundConfig.isEnabled); assertGt(inboundConfig.capacity, 0); assertGt(inboundConfig.rate, 0); // capacity and rate both have be set to 0 when rate limit is disabled, enforced by token pool RateLimiter.Config memory disableLimitConfig = RateLimiter.Config({ isEnabled: false, capacity: 0, rate: 0 }); // disable both inbound & outbound config vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, disableLimitConfig.isEnabled, disableLimitConfig.capacity, disableLimitConfig.rate, disableLimitConfig.isEnabled, disableLimitConfig.capacity, disableLimitConfig.rate ); outboundConfig = MockUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) .getCurrentOutboundRateLimiterState(remoteChainSelector); inboundConfig = MockUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) .getCurrentInboundRateLimiterState(remoteChainSelector); assertFalse(outboundConfig.isEnabled); assertEq(outboundConfig.capacity, 0); assertEq(outboundConfig.rate, 0); assertFalse(inboundConfig.isEnabled); assertEq(inboundConfig.capacity, 0); assertEq(inboundConfig.rate, 0); } function testChangeEnabledRateLimit() public { RateLimiter.TokenBucket memory outboundConfig = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentOutboundRateLimiterState(remoteChainSelector); RateLimiter.TokenBucket memory inboundConfig = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentInboundRateLimiterState(remoteChainSelector); RateLimiter.Config memory disableLimitConfig = RateLimiter.Config({ isEnabled: false, capacity: 0, rate: 0 }); // disable both inbound & outbound config vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, disableLimitConfig.isEnabled, disableLimitConfig.capacity, disableLimitConfig.rate, disableLimitConfig.isEnabled, disableLimitConfig.capacity, disableLimitConfig.rate ); skip(GHO_CCIP_STEWARD.MINIMUM_DELAY() + 1); // steward is not allowed to re-enable rate limit vm.expectRevert('INVALID_RATE_LIMIT_UPDATE'); vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, outboundConfig.isEnabled, outboundConfig.capacity, outboundConfig.rate, inboundConfig.isEnabled, inboundConfig.capacity, inboundConfig.rate ); // risk admin/DAO can re-enable rate limit on token pool vm.prank(GHO_TOKEN_POOL.owner()); GHO_TOKEN_POOL.setChainRateLimiterConfig( remoteChainSelector, _castTokenBucketToConfig(outboundConfig), _castTokenBucketToConfig(inboundConfig) ); RateLimiter.TokenBucket memory outboundConfigNew = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentOutboundRateLimiterState(remoteChainSelector); RateLimiter.TokenBucket memory inboundConfigNew = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentInboundRateLimiterState(remoteChainSelector); assertTrue(outboundConfigNew.isEnabled); assertEq(outboundConfigNew.capacity, outboundConfig.capacity); assertEq(outboundConfigNew.rate, outboundConfig.rate); assertTrue(inboundConfigNew.isEnabled); assertEq(inboundConfigNew.capacity, inboundConfig.capacity); assertEq(inboundConfigNew.rate, inboundConfig.rate); } function testChangeEnabledRateLimitOnlyOneSide() public { RateLimiter.TokenBucket memory outboundConfig = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentOutboundRateLimiterState(remoteChainSelector); RateLimiter.TokenBucket memory inboundConfig = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentInboundRateLimiterState(remoteChainSelector); assertTrue(outboundConfig.isEnabled); assertGt(outboundConfig.capacity, 0); assertGt(outboundConfig.rate, 0); assertTrue(inboundConfig.isEnabled); assertGt(inboundConfig.capacity, 0); assertGt(inboundConfig.rate, 0); RateLimiter.Config memory disableLimitConfig = RateLimiter.Config({ isEnabled: false, capacity: 0, rate: 0 }); // disable only outbound config vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, disableLimitConfig.isEnabled, disableLimitConfig.capacity, disableLimitConfig.rate, // preserve inboundConfig inboundConfig.isEnabled, inboundConfig.capacity, inboundConfig.rate ); RateLimiter.TokenBucket memory outboundConfigNew = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentOutboundRateLimiterState(remoteChainSelector); RateLimiter.TokenBucket memory inboundConfigNew = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentInboundRateLimiterState(remoteChainSelector); assertFalse(outboundConfigNew.isEnabled); assertEq(outboundConfigNew.capacity, 0); assertEq(outboundConfigNew.rate, 0); assertTrue(inboundConfigNew.isEnabled); assertEq(inboundConfigNew.capacity, inboundConfig.capacity); assertEq(inboundConfigNew.rate, inboundConfig.rate); } function testRevertUpdateRateLimitRateGreaterThanCapacity() public { RateLimiter.Config memory invalidConfig = RateLimiter.Config({ isEnabled: true, capacity: 10, rate: 100 }); vm.prank(RISK_COUNCIL); vm.expectRevert(); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, invalidConfig.isEnabled, invalidConfig.capacity, invalidConfig.rate, rateLimitConfig.isEnabled, rateLimitConfig.capacity, rateLimitConfig.rate ); } function testUpdateRateLimitFuzz( uint128 outboundCapacity, uint128 outboundRate, uint128 inboundCapacity, uint128 inboundRate ) public { RateLimiter.TokenBucket memory currentOutboundConfig = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentOutboundRateLimiterState(remoteChainSelector); RateLimiter.TokenBucket memory currentInboundConfig = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentInboundRateLimiterState(remoteChainSelector); // Capacity must be strictly greater than rate and nothing can change more than 100% outboundRate = uint128(bound(outboundRate, 1, currentOutboundConfig.rate * 2)); outboundCapacity = uint128( bound(outboundCapacity, outboundRate + 1, currentOutboundConfig.capacity * 2) ); inboundRate = uint128(bound(inboundRate, 1, currentInboundConfig.rate * 2)); inboundCapacity = uint128( bound(inboundCapacity, inboundRate + 1, currentInboundConfig.capacity * 2) ); vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, rateLimitConfig.isEnabled, outboundCapacity, outboundRate, rateLimitConfig.isEnabled, inboundCapacity, inboundRate ); } function _castTokenBucketToConfig( RateLimiter.TokenBucket memory arg ) private view returns (RateLimiter.Config memory) { return RateLimiter.Config({isEnabled: arg.isEnabled, capacity: arg.capacity, rate: arg.rate}); } } ================================================ FILE: src/test/TestGhoDiscountRateStrategy.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGhoDiscountRateStrategy is TestGhoBase { using WadRayMath for uint256; uint256 maxDiscountBalance; function setUp() public { // Calculate actual maximum value for discountTokenBalance based on wadMul usage maxDiscountBalance = (UINT256_MAX / GHO_DISCOUNT_STRATEGY.GHO_DISCOUNTED_PER_DISCOUNT_TOKEN()) - WadRayMath.HALF_WAD; } function testDebtBalanceBelowThreshold() public { uint256 result = GHO_DISCOUNT_STRATEGY.calculateDiscountRate( 0, GHO_DISCOUNT_STRATEGY.MIN_DISCOUNT_TOKEN_BALANCE() ); assertEq(result, 0, 'Unexpected discount rate'); } function testDiscountBalanceBelowThreshold() public { uint256 result = GHO_DISCOUNT_STRATEGY.calculateDiscountRate( GHO_DISCOUNT_STRATEGY.MIN_DEBT_TOKEN_BALANCE(), 0 ); assertEq(result, 0, 'Unexpected discount rate'); } function testEqualDiscountedTokenThanDebtBalance() public { assertGe( GHO_DISCOUNT_STRATEGY.GHO_DISCOUNTED_PER_DISCOUNT_TOKEN(), 1e18, 'Unexpected low value for discount token conversion' ); uint256 ratio = GHO_DISCOUNT_STRATEGY.MIN_DEBT_TOKEN_BALANCE() > GHO_DISCOUNT_STRATEGY.MIN_DISCOUNT_TOKEN_BALANCE() ? GHO_DISCOUNT_STRATEGY.MIN_DEBT_TOKEN_BALANCE().wadDiv( GHO_DISCOUNT_STRATEGY.MIN_DISCOUNT_TOKEN_BALANCE() ) : GHO_DISCOUNT_STRATEGY.MIN_DISCOUNT_TOKEN_BALANCE().wadDiv( GHO_DISCOUNT_STRATEGY.MIN_DEBT_TOKEN_BALANCE() ); uint256 minimumDiscountTokenBalance = (GHO_DISCOUNT_STRATEGY.MIN_DISCOUNT_TOKEN_BALANCE() * ratio) / GHO_DISCOUNT_STRATEGY.GHO_DISCOUNTED_PER_DISCOUNT_TOKEN(); uint256 result = GHO_DISCOUNT_STRATEGY.calculateDiscountRate( GHO_DISCOUNT_STRATEGY.MIN_DEBT_TOKEN_BALANCE(), minimumDiscountTokenBalance ); assertEq(result, GHO_DISCOUNT_STRATEGY.DISCOUNT_RATE(), 'Unexpected discount rate'); } function testMoreDiscountedTokenThanDebtBalance() public { assertGe( GHO_DISCOUNT_STRATEGY.GHO_DISCOUNTED_PER_DISCOUNT_TOKEN(), 1e18, 'Unexpected low value for discount token conversion' ); uint256 ratio = GHO_DISCOUNT_STRATEGY.MIN_DEBT_TOKEN_BALANCE() > GHO_DISCOUNT_STRATEGY.MIN_DISCOUNT_TOKEN_BALANCE() ? GHO_DISCOUNT_STRATEGY.MIN_DEBT_TOKEN_BALANCE().wadDiv( GHO_DISCOUNT_STRATEGY.MIN_DISCOUNT_TOKEN_BALANCE() ) : GHO_DISCOUNT_STRATEGY.MIN_DISCOUNT_TOKEN_BALANCE().wadDiv( GHO_DISCOUNT_STRATEGY.MIN_DEBT_TOKEN_BALANCE() ); uint256 minimumDiscountTokenBalance = (GHO_DISCOUNT_STRATEGY.MIN_DISCOUNT_TOKEN_BALANCE() * ratio) / GHO_DISCOUNT_STRATEGY.GHO_DISCOUNTED_PER_DISCOUNT_TOKEN(); uint256 result = GHO_DISCOUNT_STRATEGY.calculateDiscountRate( GHO_DISCOUNT_STRATEGY.MIN_DEBT_TOKEN_BALANCE(), minimumDiscountTokenBalance + 1 ); assertEq(result, GHO_DISCOUNT_STRATEGY.DISCOUNT_RATE(), 'Unexpected discount rate'); } function testFuzzMinBalance(uint256 debtBalance, uint256 discountTokenBalance) public { vm.assume( debtBalance < GHO_DISCOUNT_STRATEGY.MIN_DEBT_TOKEN_BALANCE() || discountTokenBalance < GHO_DISCOUNT_STRATEGY.MIN_DISCOUNT_TOKEN_BALANCE() ); uint256 result = GHO_DISCOUNT_STRATEGY.calculateDiscountRate(debtBalance, discountTokenBalance); assertEq(result, 0, 'Minimum balance not zero'); } function testFuzzNeverExceedHundredDiscount( uint256 debtBalance, uint256 discountTokenBalance ) public { vm.assume( (debtBalance >= GHO_DISCOUNT_STRATEGY.MIN_DEBT_TOKEN_BALANCE() || discountTokenBalance >= GHO_DISCOUNT_STRATEGY.MIN_DISCOUNT_TOKEN_BALANCE()) && discountTokenBalance < maxDiscountBalance ); uint256 result = GHO_DISCOUNT_STRATEGY.calculateDiscountRate(debtBalance, discountTokenBalance); assertLe(result, 10000, 'Discount rate higher than 100%'); } function testFuzzNeverExceedDiscountRate( uint256 debtBalance, uint256 discountTokenBalance ) public { vm.assume( (debtBalance >= GHO_DISCOUNT_STRATEGY.MIN_DEBT_TOKEN_BALANCE() || discountTokenBalance >= GHO_DISCOUNT_STRATEGY.MIN_DISCOUNT_TOKEN_BALANCE()) && discountTokenBalance < maxDiscountBalance ); uint256 result = GHO_DISCOUNT_STRATEGY.calculateDiscountRate(debtBalance, discountTokenBalance); assertLe(result, GHO_DISCOUNT_STRATEGY.DISCOUNT_RATE(), 'Discount rate higher than 100%'); } } ================================================ FILE: src/test/TestGhoFlashMinter.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGhoFlashMinter is TestGhoBase { function testConstructor() public { vm.expectEmit(true, true, false, false); emit GhoTreasuryUpdated(address(0), TREASURY); vm.expectEmit(false, false, false, true); emit FeeUpdated(0, DEFAULT_FLASH_FEE); GhoFlashMinter flashMinter = new GhoFlashMinter( address(GHO_TOKEN), TREASURY, DEFAULT_FLASH_FEE, address(PROVIDER) ); assertEq(address(flashMinter.GHO_TOKEN()), address(GHO_TOKEN), 'Wrong GHO token address'); assertEq(flashMinter.getFee(), DEFAULT_FLASH_FEE, 'Wrong fee'); assertEq(flashMinter.getGhoTreasury(), TREASURY, 'Wrong TREASURY address'); assertEq( address(flashMinter.ADDRESSES_PROVIDER()), address(PROVIDER), 'Wrong addresses provider address' ); } function testRevertConstructorFeeOutOfRange() public { vm.expectRevert('FlashMinter: Fee out of range'); new GhoFlashMinter(address(GHO_TOKEN), TREASURY, 10001, address(PROVIDER)); } function testRevertFlashloanNonRecipient() public { vm.expectRevert(); GHO_FLASH_MINTER.flashLoan( IERC3156FlashBorrower(address(this)), address(GHO_TOKEN), DEFAULT_BORROW_AMOUNT, '' ); } function testRevertFlashloanWrongToken() public { vm.expectRevert('FlashMinter: Unsupported currency'); GHO_FLASH_MINTER.flashLoan( IERC3156FlashBorrower(address(FLASH_BORROWER)), address(0), DEFAULT_BORROW_AMOUNT, '' ); } function testRevertFlashloanMoreThanCapacity() public { vm.expectRevert('FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); GHO_FLASH_MINTER.flashLoan( IERC3156FlashBorrower(address(FLASH_BORROWER)), address(GHO_TOKEN), DEFAULT_CAPACITY + 1, '' ); } function testRevertFlashloanInsufficientReturned() public { ACL_MANAGER.setState(false); assertEq( ACL_MANAGER.isFlashBorrower(address(FLASH_BORROWER)), false, 'Flash borrower should not be a whitelisted borrower' ); vm.expectRevert(stdError.arithmeticError); FLASH_BORROWER.flashBorrow(address(GHO_TOKEN), DEFAULT_BORROW_AMOUNT); } function testRevertFlashloanWrongCallback() public { FLASH_BORROWER.setAllowCallback(false); vm.expectRevert('FlashMinter: Callback failed'); FLASH_BORROWER.flashBorrow(address(GHO_TOKEN), DEFAULT_BORROW_AMOUNT); } function testRevertUpdateFeeNotPoolAdmin() public { ACL_MANAGER.setState(false); assertEq( ACL_MANAGER.isPoolAdmin(address(GHO_FLASH_MINTER)), false, 'GhoFlashMinter should not be a pool admin' ); vm.expectRevert('CALLER_NOT_POOL_ADMIN'); GHO_FLASH_MINTER.updateFee(100); } function testRevertUpdateFeeOutOfRange() public { vm.expectRevert('FlashMinter: Fee out of range'); GHO_FLASH_MINTER.updateFee(10001); } function testRevertUpdateTreasuryNotPoolAdmin() public { ACL_MANAGER.setState(false); assertEq( ACL_MANAGER.isPoolAdmin(address(GHO_FLASH_MINTER)), false, 'GhoFlashMinter should not be a pool admin' ); vm.expectRevert('CALLER_NOT_POOL_ADMIN'); GHO_FLASH_MINTER.updateGhoTreasury(address(0)); } function testRevertFlashfeeNotGho() public { vm.expectRevert('FlashMinter: Unsupported currency'); GHO_FLASH_MINTER.flashFee(address(0), DEFAULT_BORROW_AMOUNT); } // Positives function testFlashloan() public { ACL_MANAGER.setState(false); assertEq( ACL_MANAGER.isFlashBorrower(address(FLASH_BORROWER)), false, 'Flash borrower should not be a whitelisted borrower' ); uint256 feeAmount = (DEFAULT_FLASH_FEE * DEFAULT_BORROW_AMOUNT) / 100e2; ghoFaucet(address(FLASH_BORROWER), feeAmount); vm.expectEmit(true, true, true, true, address(GHO_FLASH_MINTER)); emit FlashMint( address(FLASH_BORROWER), address(FLASH_BORROWER), address(GHO_TOKEN), DEFAULT_BORROW_AMOUNT, feeAmount ); FLASH_BORROWER.flashBorrow(address(GHO_TOKEN), DEFAULT_BORROW_AMOUNT); } function testDistributeFeesToTreasury() public { uint256 treasuryBalanceBefore = GHO_TOKEN.balanceOf(TREASURY); ghoFaucet(address(GHO_FLASH_MINTER), 100e18); assertEq( GHO_TOKEN.balanceOf(address(GHO_FLASH_MINTER)), 100e18, 'GhoFlashMinter should have 100 GHO' ); vm.expectEmit(true, true, false, true, address(GHO_FLASH_MINTER)); emit FeesDistributedToTreasury(TREASURY, address(GHO_TOKEN), 100e18); GHO_FLASH_MINTER.distributeFeesToTreasury(); assertEq( GHO_TOKEN.balanceOf(address(GHO_FLASH_MINTER)), 0, 'GhoFlashMinter should have no GHO left after fee distribution' ); assertEq( GHO_TOKEN.balanceOf(TREASURY), treasuryBalanceBefore + 100e18, 'Treasury should have 100 more GHO' ); } function testUpdateFee() public { assertEq(GHO_FLASH_MINTER.getFee(), DEFAULT_FLASH_FEE, 'Flashminter non-default fee'); assertTrue(DEFAULT_FLASH_FEE != 100); vm.expectEmit(false, false, false, true, address(GHO_FLASH_MINTER)); emit FeeUpdated(DEFAULT_FLASH_FEE, 100); GHO_FLASH_MINTER.updateFee(100); } function testUpdateGhoTreasury() public { assertEq(GHO_FLASH_MINTER.getGhoTreasury(), TREASURY, 'Flashminter non-default TREASURY'); assertTrue(TREASURY != address(this)); vm.expectEmit(true, true, false, false, address(GHO_FLASH_MINTER)); emit GhoTreasuryUpdated(TREASURY, address(this)); GHO_FLASH_MINTER.updateGhoTreasury(address(this)); } function testMaxFlashloanNotGho() public { assertEq( GHO_FLASH_MINTER.maxFlashLoan(address(0)), 0, 'Max flash loan should be 0 for non-GHO token' ); } function testMaxFlashloanGho() public { assertEq( GHO_FLASH_MINTER.maxFlashLoan(address(GHO_TOKEN)), DEFAULT_CAPACITY, 'Max flash loan should be DEFAULT_CAPACITY for GHO token' ); } function testWhitelistedFlashFee() public { assertEq( GHO_FLASH_MINTER.flashFee(address(GHO_TOKEN), DEFAULT_BORROW_AMOUNT), 0, 'Flash fee should be 0 for whitelisted borrowers' ); } function testNotWhitelistedFlashFee() public { ACL_MANAGER.setState(false); assertEq( ACL_MANAGER.isFlashBorrower(address(this)), false, 'Flash borrower should not be a whitelisted borrower' ); uint256 fee = GHO_FLASH_MINTER.flashFee(address(GHO_TOKEN), DEFAULT_BORROW_AMOUNT); uint256 expectedFee = (DEFAULT_FLASH_FEE * DEFAULT_BORROW_AMOUNT) / 100e2; assertEq(fee, expectedFee, 'Flash fee should be correct'); } // Fuzzing function testFuzzFlashFee(uint256 feeToSet, uint256 amount) public { vm.assume(feeToSet <= 10000); vm.assume(amount <= DEFAULT_CAPACITY); GHO_FLASH_MINTER.updateFee(feeToSet); ACL_MANAGER.setState(false); // Set ACL manager to return false so there are no whitelisted borrowers. uint256 fee = GHO_FLASH_MINTER.flashFee(address(GHO_TOKEN), amount); uint256 expectedFee = (feeToSet * amount) / 100e2; // We account for +/- 1 wei of rounding error. assertTrue( fee >= (expectedFee == 0 ? 0 : expectedFee - 1), 'Flash fee should be greater than or equal to expected fee - 1' ); assertTrue( fee <= expectedFee + 1, 'Flash fee should be less than or equal to expected fee + 1' ); } } ================================================ FILE: src/test/TestGhoGsmSteward.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; import {IGhoGsmSteward} from '../contracts/misc/interfaces/IGhoGsmSteward.sol'; contract TestGhoGsmSteward is TestGhoBase { function setUp() public { // Deploy Gho GSM Steward FIXED_FEE_STRATEGY_FACTORY = new FixedFeeStrategyFactory(); GHO_GSM_STEWARD = new GhoGsmSteward(address(FIXED_FEE_STRATEGY_FACTORY), RISK_COUNCIL); /// @dev Since block.timestamp starts at 0 this is a necessary condition (block.timestamp > `MINIMUM_DELAY`) for the timelocked contract methods to work. vm.warp(GHO_GSM_STEWARD.MINIMUM_DELAY() + 1); // Grant required roles GHO_GSM.grantRole(GSM_CONFIGURATOR_ROLE, address(GHO_GSM_STEWARD)); } function testConstructor() public { assertEq(GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(), GSM_FEE_RATE_CHANGE_MAX); assertEq(GHO_GSM_STEWARD.MINIMUM_DELAY(), MINIMUM_DELAY_V2); assertEq(GHO_GSM_STEWARD.FIXED_FEE_STRATEGY_FACTORY(), address(FIXED_FEE_STRATEGY_FACTORY)); assertEq(GHO_GSM_STEWARD.RISK_COUNCIL(), RISK_COUNCIL); address[] memory gsmFeeStrategies = FIXED_FEE_STRATEGY_FACTORY.getFixedFeeStrategies(); assertEq(gsmFeeStrategies.length, 0); } function testRevertConstructorInvalidGsmFeeStrategyFactory() public { vm.expectRevert('INVALID_FIXED_FEE_STRATEGY_FACTORY'); new GhoGsmSteward(address(0), address(0x002)); } function testRevertConstructorInvalidRiskCouncil() public { vm.expectRevert('INVALID_RISK_COUNCIL'); new GhoGsmSteward(address(0x001), address(0)); } function testUpdateGsmExposureCapUpwards() public { uint128 oldExposureCap = GHO_GSM.getExposureCap(); vm.prank(RISK_COUNCIL); uint128 newExposureCap = oldExposureCap + 1; GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), newExposureCap); uint128 currentExposureCap = GHO_GSM.getExposureCap(); assertEq(currentExposureCap, newExposureCap); } function testUpdateGsmExposureCapDownwards() public { uint128 oldExposureCap = GHO_GSM.getExposureCap(); vm.prank(RISK_COUNCIL); uint128 newExposureCap = oldExposureCap - 1; GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), newExposureCap); uint128 currentExposureCap = GHO_GSM.getExposureCap(); assertEq(currentExposureCap, newExposureCap); } function testUpdateGsmExposureCapMaxIncrease() public { uint128 oldExposureCap = GHO_GSM.getExposureCap(); uint128 newExposureCap = oldExposureCap * 2; vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), newExposureCap); uint128 currentExposureCap = GHO_GSM.getExposureCap(); assertEq(currentExposureCap, newExposureCap); } function testUpdateGsmExposureCapMaxDecrease() public { vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), 0); uint128 currentExposureCap = GHO_GSM.getExposureCap(); assertEq(currentExposureCap, 0); } function testUpdateGsmExposureCapTimelock() public { uint128 oldExposureCap = GHO_GSM.getExposureCap(); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), oldExposureCap + 1); IGhoGsmSteward.GsmDebounce memory timelocks = GHO_GSM_STEWARD.getGsmTimelocks(address(GHO_GSM)); assertEq(timelocks.gsmExposureCapLastUpdated, block.timestamp); } function testUpdateGsmExposureCapAfterTimelock() public { uint128 oldExposureCap = GHO_GSM.getExposureCap(); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), oldExposureCap + 1); skip(GHO_GSM_STEWARD.MINIMUM_DELAY() + 1); uint128 newExposureCap = oldExposureCap + 2; vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), newExposureCap); uint128 currentExposureCap = GHO_GSM.getExposureCap(); assertEq(currentExposureCap, newExposureCap); } function testRevertUpdateGsmExposureCapIfUnauthorized() public { vm.expectRevert('INVALID_CALLER'); vm.prank(ALICE); GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), 50_000_000e18); } function testRevertUpdateGsmExposureCapIfTooSoon() public { uint128 oldExposureCap = GHO_GSM.getExposureCap(); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), oldExposureCap + 1); vm.prank(RISK_COUNCIL); vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), oldExposureCap + 2); } function testRevertUpdateGsmExposureCapNoChange() public { uint128 oldExposureCap = GHO_GSM.getExposureCap(); vm.prank(RISK_COUNCIL); vm.expectRevert('NO_CHANGE_IN_EXPOSURE_CAP'); GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), oldExposureCap); } function testRevertUpdateGsmExposureCapIfValueMoreThanDouble() public { uint128 oldExposureCap = GHO_GSM.getExposureCap(); vm.prank(RISK_COUNCIL); vm.expectRevert('INVALID_EXPOSURE_CAP_UPDATE'); GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), oldExposureCap * 2 + 1); } function testRevertUpdateGsmExposureCapIfStewardLostConfiguratorRole() public { uint128 oldExposureCap = GHO_GSM.getExposureCap(); GHO_GSM.revokeRole(GSM_CONFIGURATOR_ROLE, address(GHO_GSM_STEWARD)); vm.expectRevert( AccessControlErrorsLib.MISSING_ROLE(GSM_CONFIGURATOR_ROLE, address(GHO_GSM_STEWARD)) ); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmExposureCap(address(GHO_GSM), oldExposureCap + 1); } function testUpdateGsmBuySellFeesBuyFeeUpwards() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee); address newStrategy = GHO_GSM.getFeeStrategy(); uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); assertEq(newBuyFee, buyFee + 1); } function testUpdateGsmBuySellFeesBuyFeeDownwards() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee - 1, sellFee); address newStrategy = GHO_GSM.getFeeStrategy(); uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); assertEq(newBuyFee, buyFee - 1); } function testUpdateGsmBuySellFeesBuyFeeMax() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + maxFeeUpdate, sellFee); address newStrategy = GHO_GSM.getFeeStrategy(); uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); assertEq(newBuyFee, buyFee + maxFeeUpdate); } function testUpdateGsmBuySellFeesBuyFeeMin() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee - maxFeeUpdate, sellFee); address newStrategy = GHO_GSM.getFeeStrategy(); uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); assertEq(newBuyFee, buyFee - maxFeeUpdate); } function testUpdateGsmBuySellFeesSellFeeUpwards() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee + 1); address newStrategy = GHO_GSM.getFeeStrategy(); uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); assertEq(newSellFee, sellFee + 1); } function testUpdateGsmBuySellFeesSellFeeDownwards() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee - 1); address newStrategy = GHO_GSM.getFeeStrategy(); uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); assertEq(newSellFee, sellFee - 1); } function testUpdateGsmBuySellFeesSellFeeMax() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee + maxFeeUpdate); address newStrategy = GHO_GSM.getFeeStrategy(); uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); assertEq(newSellFee, sellFee + maxFeeUpdate); } function testUpdateGsmBuySellFeesSellFeeMin() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee - maxFeeUpdate); address newStrategy = GHO_GSM.getFeeStrategy(); uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); assertEq(newSellFee, sellFee - maxFeeUpdate); } function testUpdateGsmBuySellFeesBothFeesUpwards() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); address newStrategy = GHO_GSM.getFeeStrategy(); uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); assertEq(newBuyFee, buyFee + 1); assertEq(newSellFee, sellFee + 1); } function testUpdateGsmBuySellFeesBothFeesDownwards() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee - 1, sellFee - 1); address newStrategy = GHO_GSM.getFeeStrategy(); uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); assertEq(newBuyFee, buyFee - 1); assertEq(newSellFee, sellFee - 1); } function testUpdateGsmBuySellFeesBothFeesMax() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees( address(GHO_GSM), buyFee + maxFeeUpdate, sellFee + maxFeeUpdate ); address newStrategy = GHO_GSM.getFeeStrategy(); uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); assertEq(newBuyFee, buyFee + maxFeeUpdate); assertEq(newSellFee, sellFee + maxFeeUpdate); } function testUpdateGsmBuySellFeesBothFeesMin() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees( address(GHO_GSM), buyFee - maxFeeUpdate, sellFee - maxFeeUpdate ); address newStrategy = GHO_GSM.getFeeStrategy(); uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); uint256 newSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); assertEq(newBuyFee, buyFee - maxFeeUpdate); assertEq(newSellFee, sellFee - maxFeeUpdate); } function testUpdateGsmBuySellFeesTimelock() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); IGhoGsmSteward.GsmDebounce memory timelocks = GHO_GSM_STEWARD.getGsmTimelocks(address(GHO_GSM)); assertEq(timelocks.gsmFeeStrategyLastUpdated, block.timestamp); } function testUpdateGsmBuySellFeesAfterTimelock() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); skip(GHO_GSM_STEWARD.MINIMUM_DELAY() + 1); uint256 newBuyFee = buyFee + 2; uint256 newSellFee = sellFee + 2; vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), newBuyFee, newSellFee); address newStrategy = GHO_GSM.getFeeStrategy(); uint256 currentBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); uint256 currentSellFee = IGsmFeeStrategy(newStrategy).getSellFee(1e4); assertEq(currentBuyFee, newBuyFee); assertEq(currentSellFee, newSellFee); } function testUpdateGsmBuySellFeesNewStrategy() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); address[] memory cachedStrategies = FIXED_FEE_STRATEGY_FACTORY.getFixedFeeStrategies(); assertEq(cachedStrategies.length, 1); address newStrategy = GHO_GSM.getFeeStrategy(); assertEq(newStrategy, cachedStrategies[0]); } function testUpdateGsmBuySellFeesIfZeroFees() public { address currentFeeStrategy = GHO_GSM.getFeeStrategy(); vm.mockCall( currentFeeStrategy, abi.encodeWithSelector(GHO_GSM_FIXED_FEE_STRATEGY.getBuyFee.selector), abi.encode(0) ); vm.mockCall( currentFeeStrategy, abi.encodeWithSelector(GHO_GSM_FIXED_FEE_STRATEGY.getSellFee.selector), abi.encode(0) ); uint256 buyFee = IGsmFeeStrategy(currentFeeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(currentFeeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee); address[] memory cachedStrategies = FIXED_FEE_STRATEGY_FACTORY.getFixedFeeStrategies(); assertEq(cachedStrategies.length, 1); address newStrategy = GHO_GSM.getFeeStrategy(); assertEq(newStrategy, cachedStrategies[0]); } function testRevertUpdateGsmBuySellFeesIfZeroFeeStrategyAddress() public { vm.mockCall( address(GHO_GSM), abi.encodeWithSelector(GHO_GSM.getFeeStrategy.selector), abi.encode(address(0)) ); vm.expectRevert('FIXED_FEE_STRATEGY_NOT_FOUND'); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), 0.01e4, 0.01e4); } function testRevertUpdateGsmBuySellFeesIfUnauthorized() public { vm.prank(ALICE); vm.expectRevert('INVALID_CALLER'); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), 0.01e4, 0.01e4); } function testRevertUpdateGsmBuySellFeesIfTooSoon() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); vm.prank(RISK_COUNCIL); vm.expectRevert('DEBOUNCE_NOT_RESPECTED'); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 2, sellFee + 2); } function testRevertUpdateGsmBuySellFeesNoChange() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); vm.expectRevert('NO_CHANGE_IN_FEES'); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee); } function testRevertUpdateGsmBuySellFeesIfBuyFeeMoreThanMax() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); vm.expectRevert('INVALID_BUY_FEE_UPDATE'); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + maxFeeUpdate + 1, sellFee); } function testRevertUpdateGsmBuySellFeesIfBuyFeeLessThanMin() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); vm.expectRevert('INVALID_BUY_FEE_UPDATE'); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee - maxFeeUpdate - 1, sellFee); } function testRevertUpdateGsmBuySellFeesIfSellFeeMoreThanMax() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); vm.expectRevert('INVALID_SELL_FEE_UPDATE'); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee + maxFeeUpdate + 1); } function testRevertUpdateGsmBuySellFeesIfSellFeeLessThanMin() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); vm.expectRevert('INVALID_SELL_FEE_UPDATE'); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee, sellFee - maxFeeUpdate - 1); } function testRevertUpdateGsmBuySellFeesIfBothMoreThanMax() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); vm.expectRevert('INVALID_BUY_FEE_UPDATE'); GHO_GSM_STEWARD.updateGsmBuySellFees( address(GHO_GSM), buyFee + maxFeeUpdate + 1, sellFee + maxFeeUpdate + 1 ); } function testRevertUpdateGsmBuySellFeesIfBothLessThanMin() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 maxFeeUpdate = GHO_GSM_STEWARD.GSM_FEE_RATE_CHANGE_MAX(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); vm.expectRevert('INVALID_BUY_FEE_UPDATE'); GHO_GSM_STEWARD.updateGsmBuySellFees( address(GHO_GSM), buyFee - maxFeeUpdate - 1, sellFee - maxFeeUpdate - 1 ); } function testRevertUpdateGsmBuySellFeesIfStewardLostConfiguratorRole() public { address feeStrategy = GHO_GSM.getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); GHO_GSM.revokeRole(GSM_CONFIGURATOR_ROLE, address(GHO_GSM_STEWARD)); vm.expectRevert( AccessControlErrorsLib.MISSING_ROLE(GSM_CONFIGURATOR_ROLE, address(GHO_GSM_STEWARD)) ); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(address(GHO_GSM), buyFee + 1, sellFee + 1); } } ================================================ FILE: src/test/TestGhoInterestRateStrategy.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGhoInterestRateStrategy is TestGhoBase { function testFuzzVariableRateSetOnly( address addressesProvider, uint256 variableBorrowRate, DataTypes.CalculateInterestRatesParams memory params ) public { GhoInterestRateStrategy ghoInterest = new GhoInterestRateStrategy( addressesProvider, variableBorrowRate ); assertEq(address(ghoInterest.ADDRESSES_PROVIDER()), addressesProvider); assertEq(ghoInterest.getBaseVariableBorrowRate(), variableBorrowRate); assertEq(ghoInterest.getMaxVariableBorrowRate(), variableBorrowRate); (uint256 x, uint256 y, uint256 z) = ghoInterest.calculateInterestRates(params); assertEq(x, 0, 'Unexpected first return value in interest rate'); assertEq(y, 0, 'Unexpected second return value in interest rate'); assertEq(z, variableBorrowRate, 'Unexpected variable borrow rate'); } } ================================================ FILE: src/test/TestGhoOracle.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGhoOracle is TestGhoBase { function testLatestAnswer() public { int256 latest = GHO_ORACLE.latestAnswer(); assertEq(latest, DEFAULT_GHO_PRICE, 'Wrong GHO price from oracle'); } function testDecimals() public { uint8 decimals = GHO_ORACLE.decimals(); assertEq(decimals, DEFAULT_ORACLE_DECIMALS, 'Wrong decimals from oracle'); } } ================================================ FILE: src/test/TestGhoStableDebtToken.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGhoStableDebtToken is TestGhoBase { function testConstructor() public { GhoStableDebtToken debtToken = new GhoStableDebtToken(IPool(address(POOL))); assertEq(debtToken.name(), 'GHO_STABLE_DEBT_TOKEN_IMPL', 'Wrong default ERC20 name'); assertEq(debtToken.symbol(), 'GHO_STABLE_DEBT_TOKEN_IMPL', 'Wrong default ERC20 symbol'); assertEq(debtToken.decimals(), 0, 'Wrong default ERC20 decimals'); } function testInitialize() public { GhoStableDebtToken debtToken = new GhoStableDebtToken(IPool(address(POOL))); string memory tokenName = 'Aave Stable Debt GHO'; string memory tokenSymbol = 'stableDebtGHO'; bytes memory empty; debtToken.initialize( IPool(address(POOL)), address(GHO_TOKEN), IAaveIncentivesController(address(0)), 18, tokenName, tokenSymbol, empty ); assertEq(debtToken.name(), tokenName, 'Wrong initialized name'); assertEq(debtToken.symbol(), tokenSymbol, 'Wrong initialized symbol'); assertEq(debtToken.decimals(), 18, 'Wrong ERC20 decimals'); } function testInitializePoolRevert() public { string memory tokenName = 'Aave Stable Debt GHO'; string memory tokenSymbol = 'stableDebtGHO'; bytes memory empty; GhoStableDebtToken debtToken = new GhoStableDebtToken(IPool(address(POOL))); vm.expectRevert(bytes(Errors.POOL_ADDRESSES_DO_NOT_MATCH)); debtToken.initialize( IPool(address(0)), address(GHO_TOKEN), IAaveIncentivesController(address(0)), 18, tokenName, tokenSymbol, empty ); } function testReInitRevert() public { string memory tokenName = 'Aave Stable Debt GHO'; string memory tokenSymbol = 'stableDebtGHO'; bytes memory empty; vm.expectRevert(bytes('Contract instance has already been initialized')); GHO_STABLE_DEBT_TOKEN.initialize( IPool(address(POOL)), address(GHO_TOKEN), IAaveIncentivesController(address(0)), 18, tokenName, tokenSymbol, empty ); } function testUnderlying() public { assertEq( GHO_STABLE_DEBT_TOKEN.UNDERLYING_ASSET_ADDRESS(), address(GHO_TOKEN), 'Underlying should match token' ); } function testTransferRevert() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); GHO_STABLE_DEBT_TOKEN.transfer(CHARLES, 1); } function testTransferFromRevert() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); GHO_STABLE_DEBT_TOKEN.transferFrom(ALICE, CHARLES, 1); } function testApproveRevert() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); GHO_STABLE_DEBT_TOKEN.approve(CHARLES, 1); } function testIncreaseAllowanceRevert() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); GHO_STABLE_DEBT_TOKEN.increaseAllowance(CHARLES, 1); } function testDecreaseAllowanceRevert() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); GHO_STABLE_DEBT_TOKEN.decreaseAllowance(CHARLES, 1); } function testAllowanceRevert() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); GHO_STABLE_DEBT_TOKEN.allowance(CHARLES, ALICE); } function testPrincipalBalanceOfZero() public { assertEq(GHO_STABLE_DEBT_TOKEN.principalBalanceOf(ALICE), 0, 'Unexpected principal balance'); assertEq(GHO_STABLE_DEBT_TOKEN.principalBalanceOf(BOB), 0, 'Unexpected principal balance'); assertEq(GHO_STABLE_DEBT_TOKEN.principalBalanceOf(CHARLES), 0, 'Unexpected principal balance'); } function testMintRevert() public { vm.prank(address(POOL)); vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); GHO_STABLE_DEBT_TOKEN.mint(ALICE, ALICE, 0, 0); } function testUnauthorizedMint() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.CALLER_MUST_BE_POOL)); GHO_STABLE_DEBT_TOKEN.mint(ALICE, ALICE, 0, 0); } function testBurnRevert() public { vm.prank(address(POOL)); vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); GHO_STABLE_DEBT_TOKEN.burn(ALICE, 0); } function testUnauthorizedBurn() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.CALLER_MUST_BE_POOL)); GHO_STABLE_DEBT_TOKEN.burn(ALICE, 0); } function testGetAverageStableRateZero() public { uint256 result = GHO_STABLE_DEBT_TOKEN.getAverageStableRate(); assertEq(result, 0, 'Unexpected stable rate'); } function testGetUserLastUpdatedZero() public { assertEq(GHO_STABLE_DEBT_TOKEN.getUserLastUpdated(ALICE), 0, 'Unexpected stable rate'); assertEq(GHO_STABLE_DEBT_TOKEN.getUserLastUpdated(BOB), 0, 'Unexpected stable rate'); assertEq(GHO_STABLE_DEBT_TOKEN.getUserLastUpdated(CHARLES), 0, 'Unexpected stable rate'); } function testGetUserStableRateZero() public { assertEq(GHO_STABLE_DEBT_TOKEN.getUserStableRate(ALICE), 0, 'Unexpected stable rate'); assertEq(GHO_STABLE_DEBT_TOKEN.getUserStableRate(BOB), 0, 'Unexpected stable rate'); assertEq(GHO_STABLE_DEBT_TOKEN.getUserStableRate(CHARLES), 0, 'Unexpected stable rate'); } function testGetUserBalanceZero() public { assertEq(GHO_STABLE_DEBT_TOKEN.balanceOf(ALICE), 0, 'Unexpected stable rate'); assertEq(GHO_STABLE_DEBT_TOKEN.balanceOf(BOB), 0, 'Unexpected stable rate'); assertEq(GHO_STABLE_DEBT_TOKEN.balanceOf(CHARLES), 0, 'Unexpected stable rate'); } function testGetSupplyDataZero() public { ( uint256 totalSupply, uint256 calcTotalSupply, uint256 avgRate, uint40 timestamp ) = GHO_STABLE_DEBT_TOKEN.getSupplyData(); assertEq(totalSupply, 0, 'Unexpected total supply'); assertEq(calcTotalSupply, 0, 'Unexpected total supply'); assertEq(avgRate, 0, 'Unexpected average rate'); assertEq(timestamp, 0, 'Unexpected timestamp'); } function testGetTotalSupplyAvgRateZero() public { (uint256 calcTotalSupply, uint256 avgRate) = GHO_STABLE_DEBT_TOKEN.getTotalSupplyAndAvgRate(); assertEq(calcTotalSupply, 0, 'Unexpected total supply'); assertEq(avgRate, 0, 'Unexpected average rate'); } function testTotalSupplyZero() public { uint256 result = GHO_STABLE_DEBT_TOKEN.totalSupply(); assertEq(result, 0, 'Unexpected total supply'); } function testTotalSupplyLastUpdatedZero() public { uint40 result = GHO_STABLE_DEBT_TOKEN.getTotalSupplyLastUpdated(); assertEq(result, 0, 'Unexpected timestamp'); } } ================================================ FILE: src/test/TestGhoStewardsForkEthereum.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import 'forge-std/Test.sol'; import {IAccessControl} from '@openzeppelin/contracts/access/IAccessControl.sol'; import {IACLManager} from '@aave/core-v3/contracts/interfaces/IACLManager.sol'; import {AaveV3Ethereum, AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; import {IPoolAddressesProvider, IPoolDataProvider, IPool} from 'aave-address-book/AaveV3.sol'; import {DataTypes} from 'aave-v3-core/contracts/protocol/libraries/types/DataTypes.sol'; import {ReserveConfiguration} from 'aave-v3-core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; import {FixedFeeStrategyFactory} from '../contracts/facilitators/gsm/feeStrategy/FixedFeeStrategyFactory.sol'; import {IGsmFeeStrategy} from '../contracts/facilitators/gsm/feeStrategy/interfaces/IGsmFeeStrategy.sol'; import {Gsm} from '../contracts/facilitators/gsm/Gsm.sol'; import {GhoToken} from '../contracts/gho/GhoToken.sol'; import {IGhoAaveSteward} from '../contracts/misc/interfaces/IGhoAaveSteward.sol'; import {GhoAaveSteward} from '../contracts/misc/GhoAaveSteward.sol'; import {GhoBucketSteward} from '../contracts/misc/GhoBucketSteward.sol'; import {GhoCcipSteward} from '../contracts/misc/GhoCcipSteward.sol'; import {GhoGsmSteward} from '../contracts/misc/GhoGsmSteward.sol'; import {RateLimiter, IUpgradeableLockReleaseTokenPool} from '../contracts/misc/dependencies/Ccip.sol'; import {IDefaultInterestRateStrategyV2} from '../contracts/misc/dependencies/AaveV3-1.sol'; import {MockPool} from './mocks/MockPool.sol'; import {MockUpgradeableLockReleaseTokenPool} from './mocks/MockUpgradeableLockReleaseTokenPool.sol'; contract TestGhoStewardsForkEthereum is Test { using ReserveConfiguration for DataTypes.ReserveConfigurationMap; address public OWNER = makeAddr('OWNER'); address public RISK_COUNCIL = makeAddr('RISK_COUNCIL'); IPoolDataProvider public POOL_DATA_PROVIDER = AaveV3Ethereum.AAVE_PROTOCOL_DATA_PROVIDER; IPoolAddressesProvider public POOL_ADDRESSES_PROVIDER = AaveV3Ethereum.POOL_ADDRESSES_PROVIDER; address public GHO_TOKEN = AaveV3EthereumAssets.GHO_UNDERLYING; address public GHO_ATOKEN = AaveV3EthereumAssets.GHO_A_TOKEN; IPool public POOL = AaveV3Ethereum.POOL; address public ACL_ADMIN = AaveV3Ethereum.ACL_ADMIN; address public GHO_TOKEN_POOL = MiscEthereum.GHO_CCIP_TOKEN_POOL; address public GHO_GSM_USDC = MiscEthereum.GSM_USDC; address public GHO_GSM_USDT = MiscEthereum.GSM_USDT; address public ACL_MANAGER; GhoAaveSteward public GHO_AAVE_STEWARD; GhoBucketSteward public GHO_BUCKET_STEWARD; GhoCcipSteward public GHO_CCIP_STEWARD; GhoGsmSteward public GHO_GSM_STEWARD; uint64 public remoteChainSelector = 4949039107694359620; event ChainConfigured( uint64 remoteChainSelector, RateLimiter.Config outboundRateLimiterConfig, RateLimiter.Config inboundRateLimiterConfig ); function setUp() public { vm.createSelectFork(vm.rpcUrl('mainnet'), 20580302); vm.startPrank(ACL_ADMIN); ACL_MANAGER = POOL_ADDRESSES_PROVIDER.getACLManager(); IGhoAaveSteward.BorrowRateConfig memory defaultBorrowRateConfig = IGhoAaveSteward .BorrowRateConfig({ optimalUsageRatioMaxChange: 5_00, baseVariableBorrowRateMaxChange: 5_00, variableRateSlope1MaxChange: 5_00, variableRateSlope2MaxChange: 5_00 }); GHO_AAVE_STEWARD = new GhoAaveSteward( OWNER, address(POOL_ADDRESSES_PROVIDER), address(POOL_DATA_PROVIDER), GHO_TOKEN, RISK_COUNCIL, defaultBorrowRateConfig ); IAccessControl(ACL_MANAGER).grantRole( IACLManager(ACL_MANAGER).RISK_ADMIN_ROLE(), address(GHO_AAVE_STEWARD) ); GHO_BUCKET_STEWARD = new GhoBucketSteward(OWNER, GHO_TOKEN, RISK_COUNCIL); GhoToken(GHO_TOKEN).grantRole( GhoToken(GHO_TOKEN).BUCKET_MANAGER_ROLE(), address(GHO_BUCKET_STEWARD) ); GHO_CCIP_STEWARD = new GhoCcipSteward(GHO_TOKEN, GHO_TOKEN_POOL, RISK_COUNCIL, true); IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).setRateLimitAdmin(address(GHO_CCIP_STEWARD)); IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).setBridgeLimitAdmin(address(GHO_CCIP_STEWARD)); FixedFeeStrategyFactory strategyFactory = new FixedFeeStrategyFactory(); GHO_GSM_STEWARD = new GhoGsmSteward(address(strategyFactory), RISK_COUNCIL); Gsm(GHO_GSM_USDC).grantRole(Gsm(GHO_GSM_USDC).CONFIGURATOR_ROLE(), address(GHO_GSM_STEWARD)); Gsm(GHO_GSM_USDT).grantRole(Gsm(GHO_GSM_USDT).CONFIGURATOR_ROLE(), address(GHO_GSM_STEWARD)); address[] memory controlledFacilitators = new address[](3); controlledFacilitators[0] = address(GHO_ATOKEN); controlledFacilitators[1] = address(GHO_GSM_USDC); controlledFacilitators[2] = address(GHO_GSM_USDT); changePrank(OWNER); GHO_BUCKET_STEWARD.setControlledFacilitator(controlledFacilitators, true); vm.stopPrank(); } function testStewardsPermissions() public { assertEq( IAccessControl(ACL_MANAGER).hasRole( IACLManager(ACL_MANAGER).RISK_ADMIN_ROLE(), address(GHO_AAVE_STEWARD) ), true ); assertEq( IAccessControl(GHO_TOKEN).hasRole( GhoToken(GHO_TOKEN).BUCKET_MANAGER_ROLE(), address(GHO_BUCKET_STEWARD) ), true ); assertEq( IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).getRateLimitAdmin(), address(GHO_CCIP_STEWARD) ); assertEq( IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).getBridgeLimitAdmin(), address(GHO_CCIP_STEWARD) ); assertEq( Gsm(GHO_GSM_USDC).hasRole(Gsm(GHO_GSM_USDC).CONFIGURATOR_ROLE(), address(GHO_GSM_STEWARD)), true ); assertEq( Gsm(GHO_GSM_USDT).hasRole(Gsm(GHO_GSM_USDT).CONFIGURATOR_ROLE(), address(GHO_GSM_STEWARD)), true ); } function testGhoAaveStewardUpdateGhoBorrowCap() public { uint256 currentBorrowCap = _getGhoBorrowCap(); uint256 newBorrowCap = currentBorrowCap + 1; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowCap(newBorrowCap); assertEq(_getGhoBorrowCap(), newBorrowCap); } function testGhoAaveStewardUpdateGhoSupplyCap() public { uint256 currentSupplyCap = _getGhoSupplyCap(); assertEq(currentSupplyCap, 0); uint256 newSupplyCap = currentSupplyCap + 1; // Can't update supply cap even by 1 since it's 0, and 100% of 0 is 0 vm.expectRevert('INVALID_SUPPLY_CAP_UPDATE'); vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoSupplyCap(newSupplyCap); } function testGhoAaveStewardUpdateGhoBorrowRate() public { IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( currentRates.optimalUsageRatio - 1, currentRates.baseVariableBorrowRate + 1, currentRates.variableRateSlope1 + 1, currentRates.variableRateSlope2 + 1 ); assertEq(_getOptimalUsageRatio(), currentRates.optimalUsageRatio - 1); assertEq(_getBaseVariableBorrowRate(), currentRates.baseVariableBorrowRate + 1); assertEq(_getVariableRateSlope1(), currentRates.variableRateSlope1 + 1); assertEq(_getVariableRateSlope2(), currentRates.variableRateSlope2 + 1); } function testGhoBucketStewardUpdateFacilitatorBucketCapacity() public { (uint256 currentBucketCapacity, ) = GhoToken(GHO_TOKEN).getFacilitatorBucket( address(GHO_ATOKEN) ); vm.prank(RISK_COUNCIL); uint128 newBucketCapacity = uint128(currentBucketCapacity) + 1; GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity(address(GHO_ATOKEN), newBucketCapacity); (uint256 capacity, ) = GhoToken(GHO_TOKEN).getFacilitatorBucket(address(GHO_ATOKEN)); assertEq(newBucketCapacity, capacity); } function testGhoBucketStewardSetControlledFacilitator() public { address[] memory newGsmList = new address[](1); address gho_gsm_4626 = makeAddr('gho_gsm_4626'); newGsmList[0] = gho_gsm_4626; vm.prank(OWNER); GHO_BUCKET_STEWARD.setControlledFacilitator(newGsmList, true); assertTrue(GHO_BUCKET_STEWARD.isControlledFacilitator(gho_gsm_4626)); vm.prank(OWNER); GHO_BUCKET_STEWARD.setControlledFacilitator(newGsmList, false); assertFalse(GHO_BUCKET_STEWARD.isControlledFacilitator(gho_gsm_4626)); } function testGhoCcipStewardUpdateBridgeLimit() public { uint256 oldBridgeLimit = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).getBridgeLimit(); uint256 newBridgeLimit = oldBridgeLimit + 1; vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateBridgeLimit(newBridgeLimit); uint256 currentBridgeLimit = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).getBridgeLimit(); assertEq(currentBridgeLimit, newBridgeLimit); } function testGhoCcipStewardUpdateRateLimit() public { RateLimiter.TokenBucket memory outboundConfig = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentOutboundRateLimiterState(remoteChainSelector); RateLimiter.TokenBucket memory inboundConfig = MockUpgradeableLockReleaseTokenPool( GHO_TOKEN_POOL ).getCurrentInboundRateLimiterState(remoteChainSelector); RateLimiter.Config memory newOutboundConfig = RateLimiter.Config({ isEnabled: outboundConfig.isEnabled, capacity: outboundConfig.capacity + 1, rate: outboundConfig.rate }); RateLimiter.Config memory newInboundConfig = RateLimiter.Config({ isEnabled: outboundConfig.isEnabled, capacity: inboundConfig.capacity, rate: inboundConfig.rate }); // Currently rate limit set to 0, so can't even change by 1 because 100% of 0 is 0 vm.expectRevert('INVALID_RATE_LIMIT_UPDATE'); vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, newOutboundConfig.isEnabled, newOutboundConfig.capacity, newOutboundConfig.rate, newInboundConfig.isEnabled, newInboundConfig.capacity, newInboundConfig.rate ); } function testGhoGsmStewardUpdateExposureCap() public { uint128 oldExposureCap = Gsm(GHO_GSM_USDC).getExposureCap(); uint128 newExposureCap = oldExposureCap + 1; vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmExposureCap(GHO_GSM_USDC, newExposureCap); uint128 currentExposureCap = Gsm(GHO_GSM_USDC).getExposureCap(); assertEq(currentExposureCap, newExposureCap); } function testGhoGsmStewardUpdateGsmBuySellFees() public { address feeStrategy = Gsm(GHO_GSM_USDC).getFeeStrategy(); uint256 buyFee = IGsmFeeStrategy(feeStrategy).getBuyFee(1e4); uint256 sellFee = IGsmFeeStrategy(feeStrategy).getSellFee(1e4); vm.prank(RISK_COUNCIL); GHO_GSM_STEWARD.updateGsmBuySellFees(GHO_GSM_USDC, buyFee + 1, sellFee); address newStrategy = Gsm(GHO_GSM_USDC).getFeeStrategy(); uint256 newBuyFee = IGsmFeeStrategy(newStrategy).getBuyFee(1e4); assertEq(newBuyFee, buyFee + 1); } function _getGhoBorrowCap() internal view returns (uint256) { DataTypes.ReserveConfigurationMap memory configuration = POOL.getConfiguration(GHO_TOKEN); return configuration.getBorrowCap(); } function _getGhoSupplyCap() internal view returns (uint256) { DataTypes.ReserveConfigurationMap memory configuration = POOL.getConfiguration( address(GHO_TOKEN) ); return configuration.getSupplyCap(); } function _getOptimalUsageRatio() internal view returns (uint16) { IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); return currentRates.optimalUsageRatio; } function _getBaseVariableBorrowRate() internal view returns (uint32) { IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); return currentRates.baseVariableBorrowRate; } function _getVariableRateSlope1() internal view returns (uint32) { IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); return currentRates.variableRateSlope1; } function _getVariableRateSlope2() internal view returns (uint32) { IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); return currentRates.variableRateSlope2; } function _getGhoBorrowRates() internal view returns (IDefaultInterestRateStrategyV2.InterestRateData memory) { address rateStrategyAddress = POOL_DATA_PROVIDER.getInterestRateStrategyAddress(GHO_TOKEN); return IDefaultInterestRateStrategyV2(rateStrategyAddress).getInterestRateDataBps(GHO_TOKEN); } } ================================================ FILE: src/test/TestGhoStewardsForkRemote.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import 'forge-std/Test.sol'; import {IAccessControl} from '@openzeppelin/contracts/access/IAccessControl.sol'; import {IACLManager} from '@aave/core-v3/contracts/interfaces/IACLManager.sol'; import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; import {AaveV3Arbitrum} from 'aave-address-book/AaveV3Arbitrum.sol'; import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; import {IPoolAddressesProvider, IPoolDataProvider} from 'aave-address-book/AaveV3.sol'; import {GhoToken} from '../contracts/gho/GhoToken.sol'; import {IGhoAaveSteward} from '../contracts/misc/interfaces/IGhoAaveSteward.sol'; import {GhoAaveSteward} from '../contracts/misc/GhoAaveSteward.sol'; import {GhoBucketSteward} from '../contracts/misc/GhoBucketSteward.sol'; import {GhoCcipSteward} from '../contracts/misc/GhoCcipSteward.sol'; import {RateLimiter, IUpgradeableLockReleaseTokenPool} from '../contracts/misc/dependencies/Ccip.sol'; import {IDefaultInterestRateStrategyV2} from '../contracts/misc/dependencies/AaveV3-1.sol'; import {MockUpgradeableBurnMintTokenPool} from './mocks/MockUpgradeableBurnMintTokenPool.sol'; contract TestGhoStewardsForkRemote is Test { address public OWNER = makeAddr('OWNER'); address public RISK_COUNCIL = makeAddr('RISK_COUNCIL'); IPoolDataProvider public POOL_DATA_PROVIDER = AaveV3Arbitrum.AAVE_PROTOCOL_DATA_PROVIDER; IPoolAddressesProvider public POOL_ADDRESSES_PROVIDER = AaveV3Arbitrum.POOL_ADDRESSES_PROVIDER; address public GHO_TOKEN = 0x7dfF72693f6A4149b17e7C6314655f6A9F7c8B33; address public ARM_PROXY = 0xC311a21e6fEf769344EB1515588B9d535662a145; address public ACL_ADMIN = AaveV3Arbitrum.ACL_ADMIN; address public GHO_TOKEN_POOL = MiscArbitrum.GHO_CCIP_TOKEN_POOL; address public PROXY_ADMIN = MiscArbitrum.PROXY_ADMIN; address public ACL_MANAGER; GhoAaveSteward public GHO_AAVE_STEWARD; GhoBucketSteward public GHO_BUCKET_STEWARD; GhoCcipSteward public GHO_CCIP_STEWARD; uint64 public remoteChainSelector = 5009297550715157269; event ChainConfigured( uint64 remoteChainSelector, RateLimiter.Config outboundRateLimiterConfig, RateLimiter.Config inboundRateLimiterConfig ); function setUp() public { vm.createSelectFork(vm.rpcUrl('arbitrum'), 247477524); vm.startPrank(ACL_ADMIN); ACL_MANAGER = POOL_ADDRESSES_PROVIDER.getACLManager(); IGhoAaveSteward.BorrowRateConfig memory defaultBorrowRateConfig = IGhoAaveSteward .BorrowRateConfig({ optimalUsageRatioMaxChange: 5_00, baseVariableBorrowRateMaxChange: 5_00, variableRateSlope1MaxChange: 5_00, variableRateSlope2MaxChange: 5_00 }); GHO_AAVE_STEWARD = new GhoAaveSteward( OWNER, address(POOL_ADDRESSES_PROVIDER), address(POOL_DATA_PROVIDER), GHO_TOKEN, RISK_COUNCIL, defaultBorrowRateConfig ); IAccessControl(ACL_MANAGER).grantRole( IACLManager(ACL_MANAGER).RISK_ADMIN_ROLE(), address(GHO_AAVE_STEWARD) ); GHO_BUCKET_STEWARD = new GhoBucketSteward(OWNER, GHO_TOKEN, RISK_COUNCIL); GhoToken(GHO_TOKEN).grantRole( GhoToken(GHO_TOKEN).BUCKET_MANAGER_ROLE(), address(GHO_BUCKET_STEWARD) ); GHO_CCIP_STEWARD = new GhoCcipSteward(GHO_TOKEN, GHO_TOKEN_POOL, RISK_COUNCIL, true); address[] memory controlledFacilitators = new address[](1); controlledFacilitators[0] = address(GHO_TOKEN_POOL); changePrank(OWNER); GHO_BUCKET_STEWARD.setControlledFacilitator(controlledFacilitators, true); vm.stopPrank(); } function testStewardsPermissions() public { assertEq( IAccessControl(ACL_MANAGER).hasRole( IACLManager(ACL_MANAGER).RISK_ADMIN_ROLE(), address(GHO_AAVE_STEWARD) ), true ); assertEq( IAccessControl(GHO_TOKEN).hasRole( GhoToken(GHO_TOKEN).BUCKET_MANAGER_ROLE(), address(GHO_BUCKET_STEWARD) ), true ); } function testGhoAaveStewardUpdateGhoBorrowRate() public { address rateStrategyAddress = POOL_DATA_PROVIDER.getInterestRateStrategyAddress(GHO_TOKEN); IDefaultInterestRateStrategyV2.InterestRateData memory mockResponse = IDefaultInterestRateStrategyV2.InterestRateData({ optimalUsageRatio: 100, baseVariableBorrowRate: 100, variableRateSlope1: 100, variableRateSlope2: 100 }); vm.mockCall( rateStrategyAddress, abi.encodeWithSelector( IDefaultInterestRateStrategyV2(rateStrategyAddress).getInterestRateDataBps.selector, GHO_TOKEN ), abi.encode(mockResponse) ); IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); uint16 newOptimalUsageRatio = currentRates.optimalUsageRatio + 1; uint32 newBaseVariableBorrowRate = currentRates.baseVariableBorrowRate + 1; uint32 newVariableRateSlope1 = currentRates.variableRateSlope1 - 1; uint32 newVariableRateSlope2 = currentRates.variableRateSlope2 - 1; vm.prank(RISK_COUNCIL); GHO_AAVE_STEWARD.updateGhoBorrowRate( newOptimalUsageRatio, newBaseVariableBorrowRate, newVariableRateSlope1, newVariableRateSlope2 ); vm.clearMockedCalls(); assertEq(_getOptimalUsageRatio(), newOptimalUsageRatio); assertEq(_getBaseVariableBorrowRate(), newBaseVariableBorrowRate); assertEq(_getVariableRateSlope1(), newVariableRateSlope1); assertEq(_getVariableRateSlope2(), newVariableRateSlope2); } function testGhoBucketStewardUpdateFacilitatorBucketCapacity() public { (uint256 currentBucketCapacity, ) = GhoToken(GHO_TOKEN).getFacilitatorBucket( address(GHO_TOKEN_POOL) ); vm.prank(RISK_COUNCIL); uint128 newBucketCapacity = uint128(currentBucketCapacity) + 1; GHO_BUCKET_STEWARD.updateFacilitatorBucketCapacity(address(GHO_TOKEN_POOL), newBucketCapacity); (uint256 bucketCapacity, ) = GhoToken(GHO_TOKEN).getFacilitatorBucket(address(GHO_TOKEN_POOL)); assertEq(bucketCapacity, newBucketCapacity); } function testGhoBucketStewardSetControlledFacilitator() public { address[] memory newGsmList = new address[](1); address gho_gsm_4626 = makeAddr('gho_gsm_4626'); newGsmList[0] = gho_gsm_4626; vm.prank(OWNER); GHO_BUCKET_STEWARD.setControlledFacilitator(newGsmList, true); assertTrue(GHO_BUCKET_STEWARD.isControlledFacilitator(gho_gsm_4626)); vm.prank(OWNER); GHO_BUCKET_STEWARD.setControlledFacilitator(newGsmList, false); assertFalse(GHO_BUCKET_STEWARD.isControlledFacilitator(gho_gsm_4626)); } function testGhoCcipStewardUpdateRateLimit() public { RateLimiter.TokenBucket memory outboundConfig = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) .getCurrentOutboundRateLimiterState(remoteChainSelector); RateLimiter.TokenBucket memory inboundConfig = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) .getCurrentInboundRateLimiterState(remoteChainSelector); RateLimiter.Config memory newOutboundConfig = RateLimiter.Config({ isEnabled: outboundConfig.isEnabled, capacity: outboundConfig.capacity + 1, rate: outboundConfig.rate }); RateLimiter.Config memory newInboundConfig = RateLimiter.Config({ isEnabled: outboundConfig.isEnabled, capacity: inboundConfig.capacity, rate: inboundConfig.rate }); // Currently rate limit set to 0, so can't even change by 1 because 100% of 0 is 0 vm.expectRevert('INVALID_RATE_LIMIT_UPDATE'); vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, newOutboundConfig.isEnabled, newOutboundConfig.capacity, newOutboundConfig.rate, newInboundConfig.isEnabled, newInboundConfig.capacity, newInboundConfig.rate ); } function testGhoCcipStewardRevertUpdateRateLimitUnauthorizedBeforeUpgrade() public { RateLimiter.TokenBucket memory mockConfig = RateLimiter.TokenBucket({ rate: 50, capacity: 50, tokens: 1, lastUpdated: 1, isEnabled: true }); // Mocking response due to rate limit currently being 0 vm.mockCall( GHO_TOKEN_POOL, abi.encodeWithSelector( IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) .getCurrentOutboundRateLimiterState .selector, remoteChainSelector ), abi.encode(mockConfig) ); RateLimiter.TokenBucket memory outboundConfig = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) .getCurrentOutboundRateLimiterState(remoteChainSelector); RateLimiter.TokenBucket memory inboundConfig = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) .getCurrentInboundRateLimiterState(remoteChainSelector); RateLimiter.Config memory newOutboundConfig = RateLimiter.Config({ isEnabled: outboundConfig.isEnabled, capacity: outboundConfig.capacity, rate: outboundConfig.rate + 1 }); RateLimiter.Config memory newInboundConfig = RateLimiter.Config({ isEnabled: outboundConfig.isEnabled, capacity: inboundConfig.capacity, rate: inboundConfig.rate }); vm.expectRevert('Only callable by owner'); vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, newOutboundConfig.isEnabled, newOutboundConfig.capacity, newOutboundConfig.rate, newInboundConfig.isEnabled, newInboundConfig.capacity, newInboundConfig.rate ); } function testGhoCcipStewardUpdateRateLimitAfterPoolUpgrade() public { MockUpgradeableBurnMintTokenPool tokenPoolImpl = new MockUpgradeableBurnMintTokenPool( address(GHO_TOKEN), address(ARM_PROXY), false, false ); vm.prank(PROXY_ADMIN); TransparentUpgradeableProxy(payable(address(GHO_TOKEN_POOL))).upgradeTo(address(tokenPoolImpl)); vm.prank(ACL_ADMIN); IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).setRateLimitAdmin(address(GHO_CCIP_STEWARD)); RateLimiter.TokenBucket memory mockConfig = RateLimiter.TokenBucket({ rate: 50, capacity: 50, tokens: 1, lastUpdated: 1, isEnabled: true }); // Mocking response due to rate limit currently being 0 vm.mockCall( GHO_TOKEN_POOL, abi.encodeWithSelector( IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) .getCurrentOutboundRateLimiterState .selector, remoteChainSelector ), abi.encode(mockConfig) ); vm.mockCall( GHO_TOKEN_POOL, abi.encodeWithSelector( IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL).getCurrentInboundRateLimiterState.selector, remoteChainSelector ), abi.encode(mockConfig) ); RateLimiter.TokenBucket memory outboundConfig = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) .getCurrentOutboundRateLimiterState(remoteChainSelector); RateLimiter.TokenBucket memory inboundConfig = IUpgradeableLockReleaseTokenPool(GHO_TOKEN_POOL) .getCurrentInboundRateLimiterState(remoteChainSelector); RateLimiter.Config memory newOutboundConfig = RateLimiter.Config({ isEnabled: outboundConfig.isEnabled, capacity: outboundConfig.capacity + 1, rate: outboundConfig.rate }); RateLimiter.Config memory newInboundConfig = RateLimiter.Config({ isEnabled: outboundConfig.isEnabled, capacity: inboundConfig.capacity + 1, rate: inboundConfig.rate }); vm.expectEmit(false, false, false, true); emit ChainConfigured(remoteChainSelector, newOutboundConfig, newInboundConfig); vm.prank(RISK_COUNCIL); GHO_CCIP_STEWARD.updateRateLimit( remoteChainSelector, newOutboundConfig.isEnabled, newOutboundConfig.capacity, newOutboundConfig.rate, newInboundConfig.isEnabled, newInboundConfig.capacity, newInboundConfig.rate ); } function _getOptimalUsageRatio() internal view returns (uint16) { IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); return currentRates.optimalUsageRatio; } function _getBaseVariableBorrowRate() internal view returns (uint32) { IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); return currentRates.baseVariableBorrowRate; } function _getVariableRateSlope1() internal view returns (uint32) { IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); return currentRates.variableRateSlope1; } function _getVariableRateSlope2() internal view returns (uint32) { IDefaultInterestRateStrategyV2.InterestRateData memory currentRates = _getGhoBorrowRates(); return currentRates.variableRateSlope2; } function _getGhoBorrowRates() internal view returns (IDefaultInterestRateStrategyV2.InterestRateData memory) { address rateStrategyAddress = POOL_DATA_PROVIDER.getInterestRateStrategyAddress(GHO_TOKEN); return IDefaultInterestRateStrategyV2(rateStrategyAddress).getInterestRateDataBps(GHO_TOKEN); } } ================================================ FILE: src/test/TestGhoToken.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGhoToken is TestGhoBase { function testConstructor() public { GhoToken ghoToken = new GhoToken(address(this)); vm.expectEmit(true, true, true, true, address(GHO_TOKEN)); emit RoleGranted(DEFAULT_ADMIN_ROLE, msg.sender, address(this)); GHO_TOKEN.grantRole(DEFAULT_ADMIN_ROLE, msg.sender); assertEq(ghoToken.name(), 'Gho Token', 'Wrong default ERC20 name'); assertEq(ghoToken.symbol(), 'GHO', 'Wrong default ERC20 symbol'); assertEq(ghoToken.decimals(), 18, 'Wrong default ERC20 decimals'); assertEq(ghoToken.getFacilitatorsList().length, 0, 'Facilitator list not empty'); } function testGetFacilitatorData() public { IGhoToken.Facilitator memory data = GHO_TOKEN.getFacilitator(address(GHO_ATOKEN)); assertEq(data.label, 'Aave V3 Pool', 'Unexpected facilitator label'); assertEq(data.bucketCapacity, DEFAULT_CAPACITY, 'Unexpected bucket capacity'); assertEq(data.bucketLevel, 0, 'Unexpected bucket level'); } function testGetNonFacilitatorData() public { IGhoToken.Facilitator memory data = GHO_TOKEN.getFacilitator(ALICE); assertEq(data.label, '', 'Unexpected facilitator label'); assertEq(data.bucketCapacity, 0, 'Unexpected bucket capacity'); assertEq(data.bucketLevel, 0, 'Unexpected bucket level'); } function testGetFacilitatorBucket() public { (uint256 capacity, uint256 level) = GHO_TOKEN.getFacilitatorBucket(address(GHO_ATOKEN)); assertEq(capacity, DEFAULT_CAPACITY, 'Unexpected bucket capacity'); assertEq(level, 0, 'Unexpected bucket level'); } function testGetNonFacilitatorBucket() public { (uint256 capacity, uint256 level) = GHO_TOKEN.getFacilitatorBucket(ALICE); assertEq(capacity, 0, 'Unexpected bucket capacity'); assertEq(level, 0, 'Unexpected bucket level'); } function testGetPopulatedFacilitatorsList() public { address[] memory facilitatorList = GHO_TOKEN.getFacilitatorsList(); assertEq(facilitatorList.length, 6, 'Unexpected number of facilitators'); assertEq(facilitatorList[0], address(GHO_ATOKEN), 'Unexpected address for mock facilitator 1'); assertEq( facilitatorList[1], address(GHO_FLASH_MINTER), 'Unexpected address for mock facilitator 2' ); assertEq( facilitatorList[2], address(FLASH_BORROWER), 'Unexpected address for mock facilitator 3' ); assertEq(facilitatorList[3], address(GHO_GSM), 'Unexpected address for mock facilitator 4'); assertEq( facilitatorList[4], address(GHO_GSM_4626), 'Unexpected address for mock facilitator 4' ); assertEq(facilitatorList[5], FAUCET, 'Unexpected address for mock facilitator 5'); } function testAddFacilitator() public { vm.expectEmit(true, true, false, true, address(GHO_TOKEN)); emit FacilitatorAdded(ALICE, keccak256(abi.encodePacked('Alice')), DEFAULT_CAPACITY); GHO_TOKEN.addFacilitator(ALICE, 'Alice', DEFAULT_CAPACITY); } function testAddFacilitatorWithRole() public { vm.expectEmit(true, true, true, true, address(GHO_TOKEN)); emit RoleGranted(GHO_TOKEN_FACILITATOR_MANAGER_ROLE, ALICE, address(this)); GHO_TOKEN.grantRole(GHO_TOKEN_FACILITATOR_MANAGER_ROLE, ALICE); vm.prank(ALICE); vm.expectEmit(true, true, false, true, address(GHO_TOKEN)); emit FacilitatorAdded(ALICE, keccak256(abi.encodePacked('Alice')), DEFAULT_CAPACITY); GHO_TOKEN.addFacilitator(ALICE, 'Alice', DEFAULT_CAPACITY); } function testRevertAddExistingFacilitator() public { vm.expectRevert('FACILITATOR_ALREADY_EXISTS'); GHO_TOKEN.addFacilitator(address(GHO_ATOKEN), 'Aave V3 Pool', DEFAULT_CAPACITY); } function testRevertAddFacilitatorNoLabel() public { vm.expectRevert('INVALID_LABEL'); GHO_TOKEN.addFacilitator(ALICE, '', DEFAULT_CAPACITY); } function testRevertAddFacilitatorNoRole() public { vm.expectRevert( AccessControlErrorsLib.MISSING_ROLE(GHO_TOKEN_FACILITATOR_MANAGER_ROLE, address(ALICE)) ); vm.prank(ALICE); GHO_TOKEN.addFacilitator(ALICE, 'Alice', DEFAULT_CAPACITY); } function testRevertSetBucketCapacityNonFacilitator() public { vm.expectRevert('FACILITATOR_DOES_NOT_EXIST'); GHO_TOKEN.setFacilitatorBucketCapacity(ALICE, DEFAULT_CAPACITY); } function testSetNewBucketCapacity() public { vm.expectEmit(true, false, false, true, address(GHO_TOKEN)); emit FacilitatorBucketCapacityUpdated(address(GHO_ATOKEN), DEFAULT_CAPACITY, 0); GHO_TOKEN.setFacilitatorBucketCapacity(address(GHO_ATOKEN), 0); } function testSetNewBucketCapacityAsManager() public { vm.expectEmit(true, true, true, true, address(GHO_TOKEN)); emit RoleGranted(GHO_TOKEN_BUCKET_MANAGER_ROLE, ALICE, address(this)); GHO_TOKEN.grantRole(GHO_TOKEN_BUCKET_MANAGER_ROLE, ALICE); vm.prank(ALICE); vm.expectEmit(true, false, false, true, address(GHO_TOKEN)); emit FacilitatorBucketCapacityUpdated(address(GHO_ATOKEN), DEFAULT_CAPACITY, 0); GHO_TOKEN.setFacilitatorBucketCapacity(address(GHO_ATOKEN), 0); } function testRevertSetNewBucketCapacityNoRole() public { vm.expectRevert( AccessControlErrorsLib.MISSING_ROLE(GHO_TOKEN_BUCKET_MANAGER_ROLE, address(ALICE)) ); vm.prank(ALICE); GHO_TOKEN.setFacilitatorBucketCapacity(address(GHO_ATOKEN), 0); } function testRevertRemoveNonFacilitator() public { vm.expectRevert('FACILITATOR_DOES_NOT_EXIST'); GHO_TOKEN.removeFacilitator(ALICE); } function testRevertRemoveFacilitatorNonZeroBucket() public { ghoFaucet(ALICE, 1); vm.expectRevert('FACILITATOR_BUCKET_LEVEL_NOT_ZERO'); GHO_TOKEN.removeFacilitator(FAUCET); } function testRemoveFacilitator() public { vm.expectEmit(true, false, false, true, address(GHO_TOKEN)); emit FacilitatorRemoved(address(GHO_ATOKEN)); GHO_TOKEN.removeFacilitator(address(GHO_ATOKEN)); } function testRemoveFacilitatorWithRole() public { vm.expectEmit(true, true, true, true, address(GHO_TOKEN)); emit RoleGranted(GHO_TOKEN_FACILITATOR_MANAGER_ROLE, ALICE, address(this)); GHO_TOKEN.grantRole(GHO_TOKEN_FACILITATOR_MANAGER_ROLE, ALICE); vm.prank(ALICE); vm.expectEmit(true, false, false, true, address(GHO_TOKEN)); emit FacilitatorRemoved(address(GHO_ATOKEN)); GHO_TOKEN.removeFacilitator(address(GHO_ATOKEN)); } function testRevertRemoveFacilitatorNoRole() public { vm.expectRevert( AccessControlErrorsLib.MISSING_ROLE(GHO_TOKEN_FACILITATOR_MANAGER_ROLE, address(ALICE)) ); vm.prank(ALICE); GHO_TOKEN.removeFacilitator(address(GHO_ATOKEN)); } function testRevertMintBadFacilitator() public { vm.prank(ALICE); vm.expectRevert('FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); GHO_TOKEN.mint(ALICE, DEFAULT_BORROW_AMOUNT); } function testRevertMintExceedCapacity() public { vm.prank(address(GHO_ATOKEN)); vm.expectRevert('FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); GHO_TOKEN.mint(ALICE, DEFAULT_CAPACITY + 1); } function testMint() public { vm.prank(address(GHO_ATOKEN)); vm.expectEmit(true, true, false, true, address(GHO_TOKEN)); emit Transfer(address(0), ALICE, DEFAULT_CAPACITY); vm.expectEmit(true, false, false, true, address(GHO_TOKEN)); emit FacilitatorBucketLevelUpdated(address(GHO_ATOKEN), 0, DEFAULT_CAPACITY); GHO_TOKEN.mint(ALICE, DEFAULT_CAPACITY); } function testRevertZeroMint() public { vm.prank(address(GHO_ATOKEN)); vm.expectRevert('INVALID_MINT_AMOUNT'); GHO_TOKEN.mint(ALICE, 0); } function testRevertZeroBurn() public { vm.prank(address(GHO_ATOKEN)); vm.expectRevert('INVALID_BURN_AMOUNT'); GHO_TOKEN.burn(0); } function testRevertBurnMoreThanMinted() public { vm.prank(address(GHO_ATOKEN)); vm.expectEmit(true, false, false, true, address(GHO_TOKEN)); emit FacilitatorBucketLevelUpdated(address(GHO_ATOKEN), 0, DEFAULT_CAPACITY); GHO_TOKEN.mint(address(GHO_ATOKEN), DEFAULT_CAPACITY); vm.prank(address(GHO_ATOKEN)); vm.expectRevert(stdError.arithmeticError); GHO_TOKEN.burn(DEFAULT_CAPACITY + 1); } function testRevertBurnOthersTokens() public { vm.prank(address(GHO_ATOKEN)); vm.expectEmit(true, true, false, true, address(GHO_TOKEN)); emit Transfer(address(0), ALICE, DEFAULT_CAPACITY); vm.expectEmit(true, false, false, true, address(GHO_TOKEN)); emit FacilitatorBucketLevelUpdated(address(GHO_ATOKEN), 0, DEFAULT_CAPACITY); GHO_TOKEN.mint(ALICE, DEFAULT_CAPACITY); vm.prank(address(GHO_ATOKEN)); vm.expectRevert(stdError.arithmeticError); GHO_TOKEN.burn(DEFAULT_CAPACITY); } function testBurn() public { vm.prank(address(GHO_ATOKEN)); vm.expectEmit(true, true, false, true, address(GHO_TOKEN)); emit Transfer(address(0), address(GHO_ATOKEN), DEFAULT_CAPACITY); vm.expectEmit(true, false, false, true, address(GHO_TOKEN)); emit FacilitatorBucketLevelUpdated(address(GHO_ATOKEN), 0, DEFAULT_CAPACITY); GHO_TOKEN.mint(address(GHO_ATOKEN), DEFAULT_CAPACITY); vm.prank(address(GHO_ATOKEN)); vm.expectEmit(true, false, false, true, address(GHO_TOKEN)); emit FacilitatorBucketLevelUpdated( address(GHO_ATOKEN), DEFAULT_CAPACITY, DEFAULT_CAPACITY - DEFAULT_BORROW_AMOUNT ); GHO_TOKEN.burn(DEFAULT_BORROW_AMOUNT); } function testOffboardFacilitator() public { // Onboard facilitator vm.expectEmit(true, true, false, true, address(GHO_TOKEN)); emit FacilitatorAdded(ALICE, keccak256(abi.encodePacked('Alice')), DEFAULT_CAPACITY); GHO_TOKEN.addFacilitator(ALICE, 'Alice', DEFAULT_CAPACITY); // Facilitator mints half of its capacity vm.prank(ALICE); GHO_TOKEN.mint(ALICE, DEFAULT_CAPACITY / 2); (uint256 bucketCapacity, uint256 bucketLevel) = GHO_TOKEN.getFacilitatorBucket(ALICE); assertEq(bucketCapacity, DEFAULT_CAPACITY, 'Unexpected bucket capacity of facilitator'); assertEq(bucketLevel, DEFAULT_CAPACITY / 2, 'Unexpected bucket level of facilitator'); // Facilitator cannot be removed vm.expectRevert('FACILITATOR_BUCKET_LEVEL_NOT_ZERO'); GHO_TOKEN.removeFacilitator(ALICE); // Facilitator Bucket Capacity set to 0 GHO_TOKEN.setFacilitatorBucketCapacity(ALICE, 0); // Facilitator cannot mint more and is expected to burn remaining level vm.prank(ALICE); vm.expectRevert('FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); GHO_TOKEN.mint(ALICE, 1); vm.prank(ALICE); GHO_TOKEN.burn(bucketLevel); // Facilitator can be removed with 0 bucket level vm.expectEmit(true, false, false, true, address(GHO_TOKEN)); emit FacilitatorRemoved(address(ALICE)); GHO_TOKEN.removeFacilitator(address(ALICE)); } function testDomainSeparator() public { bytes32 EIP712_DOMAIN = keccak256( 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' ); bytes memory EIP712_REVISION = bytes('1'); bytes32 expected = keccak256( abi.encode( EIP712_DOMAIN, keccak256(bytes(GHO_TOKEN.name())), keccak256(EIP712_REVISION), block.chainid, address(GHO_TOKEN) ) ); bytes32 result = GHO_TOKEN.DOMAIN_SEPARATOR(); assertEq(result, expected, 'Unexpected domain separator'); } function testDomainSeparatorNewChain() public { vm.chainId(31338); bytes32 EIP712_DOMAIN = keccak256( 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' ); bytes memory EIP712_REVISION = bytes('1'); bytes32 expected = keccak256( abi.encode( EIP712_DOMAIN, keccak256(bytes(GHO_TOKEN.name())), keccak256(EIP712_REVISION), block.chainid, address(GHO_TOKEN) ) ); bytes32 result = GHO_TOKEN.DOMAIN_SEPARATOR(); assertEq(result, expected, 'Unexpected domain separator'); } function testPermitAndVerifyNonce() public { (address david, uint256 davidKey) = makeAddrAndKey('david'); ghoFaucet(david, 1e18); bytes32 PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; bytes32 innerHash = keccak256(abi.encode(PERMIT_TYPEHASH, david, BOB, 1e18, 0, 1 hours)); bytes32 outerHash = keccak256( abi.encodePacked('\x19\x01', GHO_TOKEN.DOMAIN_SEPARATOR(), innerHash) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(davidKey, outerHash); GHO_TOKEN.permit(david, BOB, 1e18, 1 hours, v, r, s); assertEq(GHO_TOKEN.allowance(david, BOB), 1e18, 'Unexpected allowance'); assertEq(GHO_TOKEN.nonces(david), 1, 'Unexpected nonce'); } function testRevertPermitInvalidSignature() public { (, uint256 davidKey) = makeAddrAndKey('david'); bytes32 PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; bytes32 innerHash = keccak256(abi.encode(PERMIT_TYPEHASH, ALICE, BOB, 1e18, 0, 1 hours)); bytes32 outerHash = keccak256( abi.encodePacked('\x19\x01', GHO_TOKEN.DOMAIN_SEPARATOR(), innerHash) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(davidKey, outerHash); vm.expectRevert(bytes('INVALID_SIGNER')); GHO_TOKEN.permit(ALICE, BOB, 1e18, 1 hours, v, r, s); } function testRevertPermitInvalidDeadline() public { vm.expectRevert(bytes('PERMIT_DEADLINE_EXPIRED')); GHO_TOKEN.permit(ALICE, BOB, 1e18, block.timestamp - 1, 0, 0, 0); } } ================================================ FILE: src/test/TestGhoVariableDebtToken.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGhoVariableDebtToken is TestGhoBase { function setUp() public { mintAndStakeDiscountToken(BOB, 10_000e18); } function testConstructor() public { GhoVariableDebtToken debtToken = new GhoVariableDebtToken(IPool(address(POOL))); assertEq(debtToken.name(), 'GHO_VARIABLE_DEBT_TOKEN_IMPL', 'Wrong default ERC20 name'); assertEq(debtToken.symbol(), 'GHO_VARIABLE_DEBT_TOKEN_IMPL', 'Wrong default ERC20 symbol'); assertEq(debtToken.decimals(), 0, 'Wrong default ERC20 decimals'); } function testInitialize() public { GhoVariableDebtToken debtToken = new GhoVariableDebtToken(IPool(address(POOL))); string memory tokenName = 'Aave Variable Debt GHO'; string memory tokenSymbol = 'variableDebtGHO'; bytes memory empty; debtToken.initialize( IPool(address(POOL)), address(GHO_TOKEN), IAaveIncentivesController(address(0)), 18, tokenName, tokenSymbol, empty ); assertEq(debtToken.name(), tokenName, 'Wrong initialized name'); assertEq(debtToken.symbol(), tokenSymbol, 'Wrong initialized symbol'); assertEq(debtToken.decimals(), 18, 'Wrong ERC20 decimals'); } function testInitializePoolRevert() public { string memory tokenName = 'Aave Variable Debt GHO'; string memory tokenSymbol = 'variableDebtGHO'; bytes memory empty; GhoVariableDebtToken debtToken = new GhoVariableDebtToken(IPool(address(POOL))); vm.expectRevert(bytes(Errors.POOL_ADDRESSES_DO_NOT_MATCH)); debtToken.initialize( IPool(address(0)), address(GHO_TOKEN), IAaveIncentivesController(address(0)), 18, tokenName, tokenSymbol, empty ); } function testReInitRevert() public { string memory tokenName = 'Aave Variable Debt GHO'; string memory tokenSymbol = 'variableDebtGHO'; bytes memory empty; vm.expectRevert(bytes('Contract instance has already been initialized')); GHO_DEBT_TOKEN.initialize( IPool(address(POOL)), address(GHO_TOKEN), IAaveIncentivesController(address(0)), 18, tokenName, tokenSymbol, empty ); } function testBorrowFixed() public { borrowAction(ALICE, DEFAULT_BORROW_AMOUNT); } function testBorrowOnBehalf() public { vm.prank(BOB); GHO_DEBT_TOKEN.approveDelegation(ALICE, DEFAULT_BORROW_AMOUNT); borrowActionOnBehalf(ALICE, BOB, DEFAULT_BORROW_AMOUNT); } function testBorrowFuzz(uint256 fuzzAmount) public { vm.assume(fuzzAmount < 100000000000000000000000001); vm.assume(fuzzAmount > 0); borrowAction(ALICE, fuzzAmount); assertEq( GHO_DEBT_TOKEN.getBalanceFromInterest(ALICE), 0, 'Accumulated interest should be zero' ); } function testBorrowFixedWithDiscount() public { borrowAction(BOB, DEFAULT_BORROW_AMOUNT); } function testMultipleBorrowFixedWithDiscount() public { borrowAction(BOB, DEFAULT_BORROW_AMOUNT); vm.warp(block.timestamp + 100000000); borrowAction(BOB, 1e16); } function testBorrowMultiple() public { for (uint x; x < 100; ++x) { borrowAction(ALICE, DEFAULT_BORROW_AMOUNT); vm.warp(block.timestamp + 2628000); } } function testBorrowMultipleWithDiscount() public { for (uint x; x < 100; ++x) { borrowAction(BOB, DEFAULT_BORROW_AMOUNT); vm.warp(block.timestamp + 2628000); } } function testBorrowMultipleFuzz(uint256 fuzzAmount) public { vm.assume(fuzzAmount < 1000000000000000000000000); vm.assume(fuzzAmount > 0); for (uint x; x < 10; ++x) { borrowAction(ALICE, fuzzAmount); vm.warp(block.timestamp + 2628000); } } function testPartialMinorRepay() public { uint256 partialRepayAmount = 1e7; // Perform borrow borrowAction(ALICE, DEFAULT_BORROW_AMOUNT); vm.warp(block.timestamp + 2628000); // Perform repayment repayAction(ALICE, partialRepayAmount); } function testPartialRepay() public { uint256 partialRepayAmount = 50e18; // Perform borrow borrowAction(ALICE, DEFAULT_BORROW_AMOUNT); vm.warp(block.timestamp + 2628000); // Perform repayment repayAction(ALICE, partialRepayAmount); } function testPartialRepayDiscount() public { uint256 partialRepayAmount = 50e18; // Perform borrow borrowAction(ALICE, DEFAULT_BORROW_AMOUNT); vm.warp(block.timestamp + 2628000); repayAction(ALICE, partialRepayAmount); mintAndStakeDiscountToken(ALICE, 10_000e18); vm.warp(block.timestamp + 2628000); repayAction(ALICE, partialRepayAmount); } function testFullRepay() public { vm.prank(ALICE); // Perform borrow borrowAction(ALICE, DEFAULT_BORROW_AMOUNT); vm.warp(block.timestamp + 2628000); uint256 allDebt = GHO_DEBT_TOKEN.balanceOf(ALICE); ghoFaucet(ALICE, 1e18); repayAction(ALICE, allDebt); } function testMultipleMinorRepay() public { uint256 partialRepayAmount = 1e7; // Perform borrow borrowAction(ALICE, DEFAULT_BORROW_AMOUNT); vm.warp(block.timestamp + 2628000); for (uint x; x < 100; ++x) { repayAction(ALICE, partialRepayAmount); vm.warp(block.timestamp + 2628000); } } function testMultipleRepay() public { uint256 partialRepayAmount = 50e18; // Perform borrow borrowAction(ALICE, DEFAULT_BORROW_AMOUNT); vm.warp(block.timestamp + 2628000); for (uint x; x < 4; ++x) { repayAction(ALICE, partialRepayAmount); vm.warp(block.timestamp + 2628000); } } function testDiscountRebalance() public { mintAndStakeDiscountToken(ALICE, 10e18); borrowAction(ALICE, 1000e18); vm.warp(block.timestamp + 10000000000); rebalanceDiscountAction(ALICE); } function testUnderlying() public { assertEq( GHO_DEBT_TOKEN.UNDERLYING_ASSET_ADDRESS(), address(GHO_TOKEN), 'Underlying should match token' ); } function testGetAToken() public { assertEq( GHO_DEBT_TOKEN.getAToken(), address(GHO_ATOKEN), 'AToken getter should match Gho AToken' ); } function testBalanceOfSameIndex() public { borrowAction(ALICE, DEFAULT_BORROW_AMOUNT); uint256 balanceOne = GHO_DEBT_TOKEN.balanceOf(ALICE); uint256 balanceTwo = GHO_DEBT_TOKEN.balanceOf(ALICE); assertEq(balanceOne, balanceTwo, 'Balance should be equal if index does not increase'); } function testTransferRevert() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); GHO_DEBT_TOKEN.transfer(CHARLES, 1); } function testTransferFromRevert() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); GHO_DEBT_TOKEN.transferFrom(ALICE, CHARLES, 1); } function testApproveRevert() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); GHO_DEBT_TOKEN.approve(CHARLES, 1); } function testIncreaseAllowanceRevert() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); GHO_DEBT_TOKEN.increaseAllowance(CHARLES, 1); } function testDecreaseAllowanceRevert() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); GHO_DEBT_TOKEN.decreaseAllowance(CHARLES, 1); } function testAllowanceRevert() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.OPERATION_NOT_SUPPORTED)); GHO_DEBT_TOKEN.allowance(CHARLES, ALICE); } function testUnauthorizedUpdateDiscount() public { vm.startPrank(ALICE); vm.expectRevert(bytes('CALLER_NOT_DISCOUNT_TOKEN')); GHO_DEBT_TOKEN.updateDiscountDistribution(ALICE, ALICE, 0, 0, 0); } function testUpdateDiscount() public { borrowAction(ALICE, DEFAULT_BORROW_AMOUNT); borrowAction(BOB, DEFAULT_BORROW_AMOUNT); vm.warp(block.timestamp + 1000); vm.prank(address(STK_TOKEN)); GHO_DEBT_TOKEN.updateDiscountDistribution(ALICE, BOB, 0, 0, 0); } function testUpdateDiscountSkipComputation() public { vm.record(); vm.prank(address(STK_TOKEN)); GHO_DEBT_TOKEN.updateDiscountDistribution(ALICE, BOB, 0, 0, 0); (bytes32[] memory reads, ) = vm.accesses(address(GHO_DEBT_TOKEN.POOL())); assertEq(reads.length, 0, 'Unexpected read of index from Pool'); } function testUpdateDiscountSelfTransfer() public { // Top up Alice with discount tokens mintAndStakeDiscountToken(ALICE, 1e18); // Alice mints some GHO borrowAction(ALICE, DEFAULT_BORROW_AMOUNT); uint256 discountPercentBefore = GHO_DEBT_TOKEN.getDiscountPercent(ALICE); address discountRateStrategyBefore = GHO_DEBT_TOKEN.getDiscountRateStrategy(); assertTrue(discountPercentBefore > 0); assertTrue(discountPercentBefore < GHO_DISCOUNT_STRATEGY.DISCOUNT_RATE()); // Alice self-transfers discount tokens vm.record(); vm.prank(ALICE); IERC20(address(STK_TOKEN)).transfer(ALICE, 1e18); (, bytes32[] memory writes) = vm.accesses(address(GHO_DEBT_TOKEN)); assertEq(writes.length, 0); assertEq(discountPercentBefore, GHO_DEBT_TOKEN.getDiscountPercent(ALICE)); assertEq(discountRateStrategyBefore, GHO_DEBT_TOKEN.getDiscountRateStrategy()); } function testUpdateDiscountSelfTransferZeroAmount() public { // Top up Alice with discount tokens mintAndStakeDiscountToken(ALICE, 1e18); // Alice mints some GHO borrowAction(ALICE, DEFAULT_BORROW_AMOUNT); uint256 discountPercentBefore = GHO_DEBT_TOKEN.getDiscountPercent(ALICE); address discountRateStrategyBefore = GHO_DEBT_TOKEN.getDiscountRateStrategy(); assertTrue(discountPercentBefore > 0); assertTrue(discountPercentBefore < GHO_DISCOUNT_STRATEGY.DISCOUNT_RATE()); // Alice self-transfers 0 discount tokens vm.record(); vm.prank(ALICE); IERC20(address(STK_TOKEN)).transfer(ALICE, 0); (, bytes32[] memory writes) = vm.accesses(address(GHO_DEBT_TOKEN)); assertEq(writes.length, 0); assertEq(discountPercentBefore, GHO_DEBT_TOKEN.getDiscountPercent(ALICE)); assertEq(discountRateStrategyBefore, GHO_DEBT_TOKEN.getDiscountRateStrategy()); } function testUpdateDiscountTransferZeroAmount() public { // Top up Alice with discount tokens mintAndStakeDiscountToken(ALICE, 1e18); // Alice mints some GHO borrowAction(ALICE, DEFAULT_BORROW_AMOUNT); uint256 discountPercentBefore = GHO_DEBT_TOKEN.getDiscountPercent(ALICE); address discountRateStrategyBefore = GHO_DEBT_TOKEN.getDiscountRateStrategy(); assertTrue(discountPercentBefore > 0); assertTrue(discountPercentBefore < GHO_DISCOUNT_STRATEGY.DISCOUNT_RATE()); // Alice transfers 0 discount tokens to BOB vm.prank(ALICE); IERC20(address(STK_TOKEN)).transfer(BOB, 0); assertEq(discountPercentBefore, GHO_DEBT_TOKEN.getDiscountPercent(ALICE)); assertEq(discountRateStrategyBefore, GHO_DEBT_TOKEN.getDiscountRateStrategy()); } function testUpdateDiscountSelfTransferFuzz( uint256 debtBalance, uint256 discountTokenBalance, uint256 amount ) public { discountTokenBalance = bound(discountTokenBalance, 0, type(uint128).max); debtBalance = bound(debtBalance, 1, DEFAULT_CAPACITY); vm.assume(amount < discountTokenBalance); // Top up Alice with discount tokens mintAndStakeDiscountToken(ALICE, discountTokenBalance); // Alice mints some GHO borrowAction(ALICE, debtBalance); uint256 discountPercentBefore = GHO_DEBT_TOKEN.getDiscountPercent(ALICE); address discountRateStrategyBefore = GHO_DEBT_TOKEN.getDiscountRateStrategy(); assertTrue(discountPercentBefore <= GHO_DISCOUNT_STRATEGY.DISCOUNT_RATE()); // Alice self-transfers discount tokens vm.record(); vm.prank(ALICE); IERC20(address(STK_TOKEN)).transfer(ALICE, discountTokenBalance); (, bytes32[] memory writes) = vm.accesses(address(GHO_DEBT_TOKEN)); assertEq(writes.length, 0); assertEq(discountPercentBefore, GHO_DEBT_TOKEN.getDiscountPercent(ALICE)); assertEq( discountPercentBefore, GHO_DISCOUNT_STRATEGY.calculateDiscountRate(debtBalance, discountTokenBalance) ); assertEq(discountRateStrategyBefore, GHO_DEBT_TOKEN.getDiscountRateStrategy()); assertTrue(GHO_DEBT_TOKEN.getDiscountPercent(ALICE) <= GHO_DISCOUNT_STRATEGY.DISCOUNT_RATE()); } function testUpdateDiscountTransferBackAndForthFuzz( uint256 aliceDebtBalance, uint256 charlesDebtBalance, uint256 aliceDiscountTokenBalance, uint256 charlesDiscountTokenBalance, uint256 transferAmount ) public { aliceDebtBalance = bound(aliceDebtBalance, 0, DEFAULT_CAPACITY); charlesDebtBalance = bound(charlesDebtBalance, 0, DEFAULT_CAPACITY - aliceDebtBalance); aliceDiscountTokenBalance = bound(aliceDiscountTokenBalance, 0, type(uint128).max); charlesDiscountTokenBalance = bound( charlesDiscountTokenBalance, 0, type(uint128).max - aliceDiscountTokenBalance ); vm.assume(transferAmount < aliceDiscountTokenBalance); // Top up with discount tokens if (aliceDiscountTokenBalance > 0) { mintAndStakeDiscountToken(ALICE, aliceDiscountTokenBalance); } if (charlesDiscountTokenBalance > 0) { mintAndStakeDiscountToken(CHARLES, charlesDiscountTokenBalance); } // Users borrow GHO if (aliceDebtBalance > 0) { borrowAction(ALICE, aliceDebtBalance); } if (charlesDebtBalance > 0) { borrowAction(CHARLES, charlesDebtBalance); } uint256 aliceDiscountPercentBefore = GHO_DEBT_TOKEN.getDiscountPercent(ALICE); uint256 charlesDiscountPercentBefore = GHO_DEBT_TOKEN.getDiscountPercent(CHARLES); console2.log( 'balance', GHO_DEBT_TOKEN.balanceOf(CHARLES), IERC20(address(STK_TOKEN)).balanceOf(CHARLES), GHO_DISCOUNT_STRATEGY.calculateDiscountRate( GHO_DEBT_TOKEN.balanceOf(CHARLES), IERC20(address(STK_TOKEN)).balanceOf(CHARLES) ) ); assertTrue(aliceDiscountPercentBefore <= GHO_DISCOUNT_STRATEGY.DISCOUNT_RATE()); assertTrue(charlesDiscountPercentBefore <= GHO_DISCOUNT_STRATEGY.DISCOUNT_RATE()); // Transfer from Alice to Charles vm.prank(ALICE); IERC20(address(STK_TOKEN)).transfer(CHARLES, transferAmount); assertEq( GHO_DEBT_TOKEN.getDiscountPercent(ALICE), GHO_DISCOUNT_STRATEGY.calculateDiscountRate( aliceDebtBalance, aliceDiscountTokenBalance - transferAmount ) ); assertEq( GHO_DEBT_TOKEN.getDiscountPercent(CHARLES), GHO_DISCOUNT_STRATEGY.calculateDiscountRate( charlesDebtBalance, charlesDiscountTokenBalance + transferAmount ) ); // Transfer from Charles to Alice vm.prank(CHARLES); IERC20(address(STK_TOKEN)).transfer(ALICE, transferAmount); assertEq( GHO_DEBT_TOKEN.getDiscountPercent(ALICE), GHO_DISCOUNT_STRATEGY.calculateDiscountRate(aliceDebtBalance, aliceDiscountTokenBalance) ); assertEq( GHO_DEBT_TOKEN.getDiscountPercent(CHARLES), GHO_DISCOUNT_STRATEGY.calculateDiscountRate(charlesDebtBalance, charlesDiscountTokenBalance) ); assertEq(GHO_DEBT_TOKEN.getDiscountPercent(ALICE), aliceDiscountPercentBefore); assertEq(GHO_DEBT_TOKEN.getDiscountPercent(CHARLES), charlesDiscountPercentBefore); } function testUnauthorizedDecreaseBalance() public { vm.startPrank(ALICE); vm.expectRevert(bytes('CALLER_NOT_A_TOKEN')); GHO_DEBT_TOKEN.decreaseBalanceFromInterest(ALICE, 1); } function testUnauthorizedMint() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.CALLER_MUST_BE_POOL)); GHO_DEBT_TOKEN.mint(ALICE, ALICE, 0, 0); } function testUnauthorizedBurn() public { vm.startPrank(ALICE); vm.expectRevert(bytes(Errors.CALLER_MUST_BE_POOL)); GHO_DEBT_TOKEN.burn(ALICE, 0, 0); } function testRevertMintZero() public { vm.prank(address(POOL)); vm.expectRevert(bytes(Errors.INVALID_MINT_AMOUNT)); GHO_DEBT_TOKEN.mint(ALICE, ALICE, 0, 1); } function testRevertBurnZero() public { vm.prank(address(POOL)); vm.expectRevert(bytes(Errors.INVALID_BURN_AMOUNT)); GHO_DEBT_TOKEN.burn(ALICE, 0, 1); } function testUnauthorizedSetAToken() public { GhoVariableDebtToken debtToken = new GhoVariableDebtToken(IPool(address(POOL))); vm.startPrank(ALICE); ACL_MANAGER.setState(false); vm.expectRevert(bytes(Errors.CALLER_NOT_POOL_ADMIN)); debtToken.setAToken(ALICE); } function testSetAToken() public { GhoVariableDebtToken debtToken = new GhoVariableDebtToken(IPool(address(POOL))); vm.expectEmit(true, true, true, true, address(debtToken)); emit ATokenSet(address(GHO_ATOKEN)); debtToken.setAToken(address(GHO_ATOKEN)); } function testUpdateAToken() public { vm.startPrank(ALICE); vm.expectRevert(bytes('ATOKEN_ALREADY_SET')); GHO_DEBT_TOKEN.setAToken(ALICE); } function testZeroAToken() public { GhoVariableDebtToken debtToken = new GhoVariableDebtToken(IPool(address(POOL))); vm.startPrank(ALICE); vm.expectRevert(bytes('ZERO_ADDRESS_NOT_VALID')); debtToken.setAToken(address(0)); } function testUnauthorizedUpdateDiscountRateStrategy() public { vm.startPrank(ALICE); ACL_MANAGER.setState(false); vm.expectRevert(bytes(Errors.CALLER_NOT_POOL_ADMIN)); GHO_DEBT_TOKEN.updateDiscountRateStrategy(ALICE); } function testUnauthorizedUpdateDiscountToken() public { vm.startPrank(ALICE); ACL_MANAGER.setState(false); vm.expectRevert(bytes(Errors.CALLER_NOT_POOL_ADMIN)); GHO_DEBT_TOKEN.updateDiscountToken(ALICE); } function testUpdateDiscountTokenToZero() public { vm.startPrank(ALICE); vm.expectRevert(bytes('ZERO_ADDRESS_NOT_VALID')); GHO_DEBT_TOKEN.updateDiscountToken(address(0)); } function testUpdateDiscountStrategy() public { vm.startPrank(ALICE); GHO_DEBT_TOKEN.updateDiscountRateStrategy(CHARLES); assertEq( GHO_DEBT_TOKEN.getDiscountRateStrategy(), CHARLES, 'Discount Rate Strategy should be updated' ); } function testRevertUpdateDiscountStrategyZero() public { vm.startPrank(address(POOL)); vm.expectRevert(bytes('ZERO_ADDRESS_NOT_VALID')); GHO_DEBT_TOKEN.updateDiscountRateStrategy(address(0)); } function testUpdateDiscountToken() public { vm.startPrank(ALICE); GHO_DEBT_TOKEN.updateDiscountToken(CHARLES); assertEq(GHO_DEBT_TOKEN.getDiscountToken(), CHARLES, 'Discount token should be updated'); } function testUpdateDiscountTokenWithBorrow() public { borrowAction(BOB, DEFAULT_BORROW_AMOUNT); vm.warp(block.timestamp + 10000); vm.startPrank(ALICE); GHO_DEBT_TOKEN.updateDiscountToken(BOB); assertEq(GHO_DEBT_TOKEN.getDiscountToken(), BOB, 'Discount token should be updated'); } function testScaledUserBalanceAndSupply() public { borrowAction(ALICE, DEFAULT_BORROW_AMOUNT); borrowAction(BOB, DEFAULT_BORROW_AMOUNT); (uint256 userScaledBalance, uint256 totalScaledSupply) = GHO_DEBT_TOKEN .getScaledUserBalanceAndSupply(ALICE); assertEq(userScaledBalance, DEFAULT_BORROW_AMOUNT, 'Unexpected user balance'); assertEq(totalScaledSupply, DEFAULT_BORROW_AMOUNT * 2, 'Unexpected total supply'); } } ================================================ FILE: src/test/TestGhoVariableDebtTokenForked.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {InitializableImmutableAdminUpgradeabilityProxy} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/InitializableImmutableAdminUpgradeabilityProxy.sol'; import './TestGhoBase.t.sol'; contract TestGhoVariableDebtTokenForked is TestGhoBase { IGhoToken gho = IGhoToken(0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f); address usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address aave = 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9; address stkAave = 0x4da27a545c0c5B758a6BA100e3a049001de870f5; InitializableImmutableAdminUpgradeabilityProxy debtToken = InitializableImmutableAdminUpgradeabilityProxy( payable(0x786dBff3f1292ae8F92ea68Cf93c30b34B1ed04B) ); IPool pool = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2); address admin = 0x64b761D848206f447Fe2dd461b0c635Ec39EbB27; uint256 usdcSupplyAmount = 100_000e6; uint256 ghoBorrowAmount = 71_000e18; uint256 stkAaveAmount = 100_000e18; function setUp() public { vm.createSelectFork(vm.envString('ETH_RPC_URL'), 17987863); } function testBorrowAndRepayFullUnexpectedScaledBalance() public { uint256 timeSkip = 86545113; // Stake AAVE deal(aave, ALICE, stkAaveAmount); vm.startPrank(ALICE); IERC20(aave).approve(stkAave, stkAaveAmount); IStakedAaveV3(stkAave).stake(ALICE, stkAaveAmount); vm.stopPrank(); // Supply USDC, borrow GHO deal(usdc, ALICE, usdcSupplyAmount); vm.startPrank(ALICE); IERC20(usdc).approve(address(pool), usdcSupplyAmount); pool.supply(usdc, usdcSupplyAmount, ALICE, 0); pool.borrow(address(gho), ghoBorrowAmount, 2, 0, ALICE); vm.stopPrank(); vm.warp(block.timestamp + timeSkip); // Ensure Alice has the correct GHO balance uint256 allDebt = IERC20(address(debtToken)).balanceOf(ALICE); deal(address(gho), ALICE, allDebt); // Repay in full vm.startPrank(ALICE); gho.approve(address(pool), type(uint256).max); pool.repay(address(gho), type(uint256).max, 2, ALICE); vm.stopPrank(); DataTypes.UserConfigurationMap memory userConfig = pool.getUserConfiguration(ALICE); bool isBorrowing = ((userConfig.data >> (20 << 1)) & 1 != 0); // Verify isBorrowing is false, but there is a non-zero scaledBalance assertEq(isBorrowing, false, 'Unexpected borrow state'); assertEq(GhoAToken(address(debtToken)).scaledBalanceOf(ALICE), 1, 'Unexpected scaled balance'); } function testBorrowAndRepayFullAmountUpgradeVerifyNoDust(uint256 timeSkip) public { timeSkip = bound(timeSkip, 1, 31_560_000); address newDebtToken = address(new GhoVariableDebtToken(pool)); // Stake AAVE deal(aave, ALICE, stkAaveAmount); vm.startPrank(ALICE); IERC20(aave).approve(stkAave, stkAaveAmount); IStakedAaveV3(stkAave).stake(ALICE, stkAaveAmount); vm.stopPrank(); // Supply USDC, borrow GHO deal(usdc, ALICE, usdcSupplyAmount); vm.startPrank(ALICE); IERC20(usdc).approve(address(pool), usdcSupplyAmount); pool.supply(usdc, usdcSupplyAmount, ALICE, 0); pool.borrow(address(gho), ghoBorrowAmount, 2, 0, ALICE); vm.stopPrank(); // Upgrade GhoVariableDebtToken vm.prank(admin); debtToken.upgradeTo(newDebtToken); vm.warp(block.timestamp + timeSkip); // Ensure Alice has the correct GHO balance uint256 allDebt = IERC20(address(debtToken)).balanceOf(ALICE); deal(address(gho), ALICE, allDebt); // Repay in full vm.startPrank(ALICE); gho.approve(address(pool), type(uint256).max); pool.repay(address(gho), type(uint256).max, 2, ALICE); vm.stopPrank(); DataTypes.UserConfigurationMap memory userConfig = pool.getUserConfiguration(ALICE); bool isBorrowing = ((userConfig.data >> (20 << 1)) & 1 != 0); // Ensure isBorrowing is false and the scaledBalance never exceeds zero assertEq(isBorrowing, false, 'Unexpected borrow state'); assertEq(GhoAToken(address(debtToken)).scaledBalanceOf(ALICE), 0, 'Unexpected scaled balance'); } } ================================================ FILE: src/test/TestGsm.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGsm is TestGhoBase { using PercentageMath for uint256; using PercentageMath for uint128; address internal gsmSignerAddr; uint256 internal gsmSignerKey; function setUp() public { (gsmSignerAddr, gsmSignerKey) = makeAddrAndKey('gsmSigner'); } function testConstructor() public { Gsm gsm = new Gsm( address(GHO_TOKEN), address(USDC_TOKEN), address(GHO_GSM_FIXED_PRICE_STRATEGY) ); assertEq(gsm.GHO_TOKEN(), address(GHO_TOKEN), 'Unexpected GHO token address'); assertEq(gsm.UNDERLYING_ASSET(), address(USDC_TOKEN), 'Unexpected underlying asset address'); assertEq( gsm.PRICE_STRATEGY(), address(GHO_GSM_FIXED_PRICE_STRATEGY), 'Unexpected price strategy' ); assertEq(gsm.getExposureCap(), 0, 'Unexpected exposure capacity'); } function testRevertConstructorInvalidPriceStrategy() public { FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy(1e18, address(GHO_TOKEN), 18); vm.expectRevert('INVALID_PRICE_STRATEGY'); new Gsm(address(GHO_TOKEN), address(USDC_TOKEN), address(newPriceStrategy)); } function testRevertConstructorZeroAddressParams() public { vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); new Gsm(address(0), address(USDC_TOKEN), address(GHO_GSM_FIXED_PRICE_STRATEGY)); vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); new Gsm(address(GHO_TOKEN), address(0), address(GHO_GSM_FIXED_PRICE_STRATEGY)); } function testInitialize() public { Gsm gsm = new Gsm( address(GHO_TOKEN), address(USDC_TOKEN), address(GHO_GSM_FIXED_PRICE_STRATEGY) ); vm.expectEmit(true, true, true, true); emit RoleGranted(DEFAULT_ADMIN_ROLE, address(this), address(this)); vm.expectEmit(true, true, false, true); emit GhoTreasuryUpdated(address(0), address(TREASURY)); vm.expectEmit(true, true, false, true); emit ExposureCapUpdated(0, DEFAULT_GSM_USDC_EXPOSURE); gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); assertEq(gsm.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); } function testRevertInitializeZeroAdmin() public { Gsm gsm = new Gsm( address(GHO_TOKEN), address(USDC_TOKEN), address(GHO_GSM_FIXED_PRICE_STRATEGY) ); vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); gsm.initialize(address(0), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); } function testRevertInitializeTwice() public { Gsm gsm = new Gsm( address(GHO_TOKEN), address(USDC_TOKEN), address(GHO_GSM_FIXED_PRICE_STRATEGY) ); gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); vm.expectRevert('Contract instance has already been initialized'); gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); } function testSellAssetZeroFee() public { vm.expectEmit(true, true, false, true, address(GHO_GSM)); emit FeeStrategyUpdated(address(GHO_GSM_FIXED_FEE_STRATEGY), address(0)); GHO_GSM.updateFeeStrategy(address(0)); vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, 0); (uint256 assetAmount, uint256 ghoBought) = GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); assertEq(ghoBought, DEFAULT_GSM_GHO_AMOUNT, 'Unexpected GHO amount bought'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected asset amount sold'); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), DEFAULT_GSM_GHO_AMOUNT, 'Unexpected final GHO balance'); assertEq(GHO_GSM.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); } function testSellAsset() public { uint256 fee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 ghoOut = DEFAULT_GSM_GHO_AMOUNT - fee; vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, fee); (uint256 assetAmount, uint256 ghoBought) = GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); assertEq(ghoBought, DEFAULT_GSM_GHO_AMOUNT - fee, 'Unexpected GHO amount bought'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected asset amount sold'); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoOut, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), fee, 'Unexpected GSM GHO balance'); assertEq( GHO_GSM.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available underlying exposure' ); assertEq( GHO_GSM.getAvailableLiquidity(), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available liquidity' ); assertEq(GHO_GSM.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); } function testSellAssetSendToOther() public { uint256 fee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 ghoOut = DEFAULT_GSM_GHO_AMOUNT - fee; vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit SellAsset(ALICE, BOB, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, fee); (uint256 assetAmount, uint256 ghoBought) = GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, BOB); vm.stopPrank(); assertEq(ghoBought, DEFAULT_GSM_GHO_AMOUNT - fee, 'Unexpected GHO amount bought'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected asset amount sold'); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), 0, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(BOB), ghoOut, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), fee, 'Unexpected GSM GHO balance'); assertEq(GHO_GSM.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); } function testSellAssetWithSig() public { uint256 deadline = block.timestamp + 1 hours; uint256 fee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 ghoOut = DEFAULT_GSM_GHO_AMOUNT - fee; vm.prank(FAUCET); USDC_TOKEN.mint(gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT); vm.prank(gsmSignerAddr); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); assertEq(GHO_GSM.nonces(gsmSignerAddr), 0, 'Unexpected before gsmSignerAddr nonce'); bytes32 digest = keccak256( abi.encode( '\x19\x01', GHO_GSM.DOMAIN_SEPARATOR(), GSM_SELL_ASSET_WITH_SIG_TYPEHASH, abi.encode( gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, gsmSignerAddr, GHO_GSM.nonces(gsmSignerAddr), deadline ) ) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmSignerKey, digest); bytes memory signature = abi.encodePacked(r, s, v); assertTrue(gsmSignerAddr != ALICE, 'Signer is the same as Alice'); // Send the signature via another user vm.prank(ALICE); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit SellAsset( gsmSignerAddr, gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, fee ); GHO_GSM.sellAssetWithSig( gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, gsmSignerAddr, deadline, signature ); assertEq(GHO_GSM.nonces(gsmSignerAddr), 1, 'Unexpected final gsmSignerAddr nonce'); assertEq(USDC_TOKEN.balanceOf(gsmSignerAddr), 0, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(gsmSignerAddr), ghoOut, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), fee, 'Unexpected GSM GHO balance'); assertEq(GHO_GSM.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); } function testSellAssetWithSigExactDeadline() public { // EIP-2612 states the execution must be allowed in case deadline is equal to block.timestamp uint256 deadline = block.timestamp; uint256 fee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 ghoOut = DEFAULT_GSM_GHO_AMOUNT - fee; vm.prank(FAUCET); USDC_TOKEN.mint(gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT); vm.prank(gsmSignerAddr); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); assertEq(GHO_GSM.nonces(gsmSignerAddr), 0, 'Unexpected before gsmSignerAddr nonce'); bytes32 digest = keccak256( abi.encode( '\x19\x01', GHO_GSM.DOMAIN_SEPARATOR(), GSM_SELL_ASSET_WITH_SIG_TYPEHASH, abi.encode( gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, gsmSignerAddr, GHO_GSM.nonces(gsmSignerAddr), deadline ) ) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmSignerKey, digest); bytes memory signature = abi.encodePacked(r, s, v); assertTrue(gsmSignerAddr != ALICE, 'Signer is the same as Alice'); // Send the signature via another user vm.prank(ALICE); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit SellAsset( gsmSignerAddr, gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, fee ); GHO_GSM.sellAssetWithSig( gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, gsmSignerAddr, deadline, signature ); assertEq(GHO_GSM.nonces(gsmSignerAddr), 1, 'Unexpected final gsmSignerAddr nonce'); assertEq(USDC_TOKEN.balanceOf(gsmSignerAddr), 0, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(gsmSignerAddr), ghoOut, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), fee, 'Unexpected GSM GHO balance'); assertEq(GHO_GSM.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); } function testRevertSellAssetWithSigExpiredSignature() public { uint256 deadline = block.timestamp - 1; bytes32 digest = keccak256( abi.encode( '\x19\x01', GHO_GSM.DOMAIN_SEPARATOR(), GSM_SELL_ASSET_WITH_SIG_TYPEHASH, abi.encode( gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, gsmSignerAddr, GHO_GSM.nonces(gsmSignerAddr), deadline ) ) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmSignerKey, digest); bytes memory signature = abi.encodePacked(r, s, v); assertTrue(gsmSignerAddr != ALICE, 'Signer is the same as Alice'); // Send the signature via another user vm.prank(ALICE); vm.expectRevert('SIGNATURE_DEADLINE_EXPIRED'); GHO_GSM.sellAssetWithSig( gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, gsmSignerAddr, deadline, signature ); } function testRevertSellAssetWithSigInvalidSignature() public { uint256 deadline = block.timestamp + 1 hours; bytes32 digest = keccak256( abi.encode( '\x19\x01', GHO_GSM.DOMAIN_SEPARATOR(), GSM_SELL_ASSET_WITH_SIG_TYPEHASH, abi.encode( gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, gsmSignerAddr, GHO_GSM.nonces(gsmSignerAddr), deadline ) ) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmSignerKey, digest); bytes memory signature = abi.encodePacked(r, s, v); assertTrue(gsmSignerAddr != ALICE, 'Signer is the same as Alice'); // Send the signature via another user vm.prank(ALICE); vm.expectRevert('SIGNATURE_INVALID'); GHO_GSM.sellAssetWithSig(ALICE, DEFAULT_GSM_USDC_AMOUNT, ALICE, deadline, signature); } function testRevertSellAssetZeroAmount() public { vm.prank(ALICE); vm.expectRevert('INVALID_AMOUNT'); GHO_GSM.sellAsset(0, ALICE); } function testRevertSellAssetNoAsset() public { vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); vm.expectRevert('ERC20: transfer amount exceeds balance'); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); } function testRevertSellAssetNoAllowance() public { vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.prank(ALICE); vm.expectRevert('ERC20: transfer amount exceeds allowance'); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); } function testRevertSellAssetNoBucketCap() public { Gsm gsm = new Gsm( address(GHO_TOKEN), address(USDC_TOKEN), address(GHO_GSM_FIXED_PRICE_STRATEGY) ); gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); GHO_TOKEN.addFacilitator(address(gsm), 'GSM Modified Bucket Cap', DEFAULT_CAPACITY - 1); uint256 defaultCapInUsdc = DEFAULT_CAPACITY / (10 ** (18 - USDC_TOKEN.decimals())); vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, defaultCapInUsdc); vm.startPrank(ALICE); USDC_TOKEN.approve(address(gsm), defaultCapInUsdc); vm.expectRevert('FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); gsm.sellAsset(defaultCapInUsdc, ALICE); vm.stopPrank(); } function testRevertSellAssetTooMuchUnderlyingExposure() public { Gsm gsm = new Gsm( address(GHO_TOKEN), address(USDC_TOKEN), address(GHO_GSM_FIXED_PRICE_STRATEGY) ); gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE - 1); GHO_TOKEN.addFacilitator(address(gsm), 'GSM Modified Exposure Cap', DEFAULT_CAPACITY); vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_EXPOSURE); vm.startPrank(ALICE); USDC_TOKEN.approve(address(gsm), DEFAULT_GSM_USDC_EXPOSURE); vm.expectRevert('EXOGENOUS_ASSET_EXPOSURE_TOO_HIGH'); gsm.sellAsset(DEFAULT_GSM_USDC_EXPOSURE, ALICE); vm.stopPrank(); } function testGetGhoAmountForSellAsset() public { (uint256 exactAssetAmount, uint256 ghoBought, uint256 grossAmount, uint256 fee) = GHO_GSM .getGhoAmountForSellAsset(DEFAULT_GSM_USDC_AMOUNT); _sellAsset(GHO_GSM, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); assertEq( DEFAULT_GSM_USDC_AMOUNT - USDC_TOKEN.balanceOf(ALICE), exactAssetAmount, 'Unexpected asset amount sold' ); assertEq(ghoBought + fee, grossAmount, 'Unexpected GHO gross amount'); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoBought, 'Unexpected GHO bought amount'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), fee, 'Unexpected GHO fee amount'); (uint256 assetAmount, uint256 exactGhoBought, uint256 grossAmount2, uint256 fee2) = GHO_GSM .getAssetAmountForSellAsset(ghoBought); assertEq(GHO_TOKEN.balanceOf(ALICE), exactGhoBought, 'Unexpected GHO bought amount'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected estimation of sold assets'); assertEq(grossAmount, grossAmount2, 'Unexpected GHO gross amount'); assertEq(fee, fee2, 'Unexpected GHO fee amount'); } function testGetGhoAmountForSellAssetWithZeroFee() public { GHO_GSM.updateFeeStrategy(address(0)); (uint256 exactAssetAmount, uint256 ghoBought, uint256 grossAmount, uint256 fee) = GHO_GSM .getGhoAmountForSellAsset(DEFAULT_GSM_USDC_AMOUNT); assertEq(fee, 0, 'Unexpected GHO fee amount'); _sellAsset(GHO_GSM, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); assertEq( DEFAULT_GSM_USDC_AMOUNT - USDC_TOKEN.balanceOf(ALICE), exactAssetAmount, 'Unexpected asset amount sold' ); assertEq(ghoBought, grossAmount, 'Unexpected GHO gross amount'); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoBought, 'Unexpected GHO bought amount'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), 0, 'Unexpected GHO fee amount'); (uint256 assetAmount, uint256 exactGhoBought, uint256 grossAmount2, uint256 fee2) = GHO_GSM .getAssetAmountForSellAsset(ghoBought); assertEq(GHO_TOKEN.balanceOf(ALICE), exactGhoBought, 'Unexpected GHO bought amount'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected estimation of sold assets'); assertEq(grossAmount, grossAmount2, 'Unexpected GHO gross amount'); assertEq(fee, fee2, 'Unexpected GHO fee amount'); } function testGetGhoAmountForSellAssetWithZeroAmount() public { (uint256 exactAssetAmount, uint256 ghoBought, uint256 grossAmount, uint256 fee) = GHO_GSM .getGhoAmountForSellAsset(0); assertEq(exactAssetAmount, 0, 'Unexpected exact asset amount'); assertEq(ghoBought, 0, 'Unexpected GHO bought amount'); assertEq(grossAmount, 0, 'Unexpected GHO gross amount'); assertEq(fee, 0, 'Unexpected GHO fee amount'); (uint256 assetAmount, uint256 exactGhoBought, uint256 grossAmount2, uint256 fee2) = GHO_GSM .getAssetAmountForSellAsset(ghoBought); assertEq(exactGhoBought, 0, 'Unexpected exact gho bought'); assertEq(assetAmount, 0, 'Unexpected estimation of sold assets'); assertEq(grossAmount, grossAmount2, 'Unexpected GHO gross amount'); assertEq(fee, fee2, 'Unexpected GHO fee amount'); } function testBuyAssetZeroFee() public { vm.expectEmit(true, true, false, true, address(GHO_GSM)); emit FeeStrategyUpdated(address(GHO_GSM_FIXED_FEE_STRATEGY), address(0)); GHO_GSM.updateFeeStrategy(address(0)); // Supply assets to the GSM first vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, 0); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); // Buy assets as another user ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT); vm.startPrank(BOB); GHO_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_GHO_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit BuyAsset(BOB, BOB, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, 0); (uint256 assetAmount, uint256 ghoSold) = GHO_GSM.buyAsset(DEFAULT_GSM_USDC_AMOUNT, BOB); vm.stopPrank(); assertEq(ghoSold, DEFAULT_GSM_GHO_AMOUNT, 'Unexpected GHO amount sold'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected asset amount bought'); assertEq(USDC_TOKEN.balanceOf(BOB), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), DEFAULT_GSM_GHO_AMOUNT, 'Unexpected final GHO balance'); assertEq(GHO_GSM.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); } function testBuyAsset() public { uint256 sellFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 buyFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_BUY_FEE); uint256 ghoOut = DEFAULT_GSM_GHO_AMOUNT - sellFee; // Supply assets to the GSM first vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, sellFee); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); // Buy assets as another user ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.startPrank(BOB); GHO_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit BuyAsset(BOB, BOB, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT + buyFee, buyFee); (uint256 assetAmount, uint256 ghoSold) = GHO_GSM.buyAsset(DEFAULT_GSM_USDC_AMOUNT, BOB); vm.stopPrank(); assertEq(ghoSold, DEFAULT_GSM_GHO_AMOUNT + buyFee, 'Unexpected GHO amount sold'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected asset amount bought'); assertEq(USDC_TOKEN.balanceOf(BOB), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoOut, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), sellFee + buyFee, 'Unexpected GSM GHO balance'); assertEq( GHO_GSM.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected available underlying exposure' ); assertEq(GHO_GSM.getAvailableLiquidity(), 0, 'Unexpected available liquidity'); assertEq(GHO_GSM.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); } function testBuyAssetSendToOther() public { uint256 sellFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 buyFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_BUY_FEE); uint256 ghoOut = DEFAULT_GSM_GHO_AMOUNT - sellFee; // Supply assets to the GSM first vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, sellFee); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); // Buy assets as another user ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.startPrank(BOB); GHO_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit BuyAsset(BOB, CHARLES, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT + buyFee, buyFee); (uint256 assetAmount, uint256 ghoSold) = GHO_GSM.buyAsset(DEFAULT_GSM_USDC_AMOUNT, CHARLES); vm.stopPrank(); assertEq(ghoSold, DEFAULT_GSM_GHO_AMOUNT + buyFee, 'Unexpected GHO amount sold'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected asset amount bought'); assertEq(USDC_TOKEN.balanceOf(BOB), 0, 'Unexpected final USDC balance'); assertEq( USDC_TOKEN.balanceOf(CHARLES), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected final USDC balance' ); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoOut, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), sellFee + buyFee, 'Unexpected GSM GHO balance'); assertEq(GHO_GSM.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); } function testBuyAssetWithSig() public { uint256 deadline = block.timestamp + 1 hours; uint256 sellFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 buyFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_BUY_FEE); uint256 ghoOut = DEFAULT_GSM_GHO_AMOUNT - sellFee; // Supply assets to the GSM first vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, sellFee); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); assertTrue(gsmSignerAddr != ALICE, 'Signer is the same as Alice'); // Buy assets as another user ghoFaucet(gsmSignerAddr, DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.prank(gsmSignerAddr); GHO_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_GHO_AMOUNT + buyFee); assertEq(GHO_GSM.nonces(gsmSignerAddr), 0, 'Unexpected before gsmSignerAddr nonce'); bytes32 digest = keccak256( abi.encode( '\x19\x01', GHO_GSM.DOMAIN_SEPARATOR(), GSM_BUY_ASSET_WITH_SIG_TYPEHASH, abi.encode( gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, gsmSignerAddr, GHO_GSM.nonces(gsmSignerAddr), deadline ) ) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmSignerKey, digest); bytes memory signature = abi.encodePacked(r, s, v); assertTrue(gsmSignerAddr != BOB, 'Signer is the same as Bob'); vm.prank(BOB); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit BuyAsset( gsmSignerAddr, gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT + buyFee, buyFee ); GHO_GSM.buyAssetWithSig( gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, gsmSignerAddr, deadline, signature ); assertEq(GHO_GSM.nonces(gsmSignerAddr), 1, 'Unexpected final gsmSignerAddr nonce'); assertEq( USDC_TOKEN.balanceOf(gsmSignerAddr), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected final USDC balance' ); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoOut, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), sellFee + buyFee, 'Unexpected GSM GHO balance'); assertEq(GHO_GSM.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); } function testBuyAssetWithSigExactDeadline() public { // EIP-2612 states the execution must be allowed in case deadline is equal to block.timestamp uint256 deadline = block.timestamp; uint256 sellFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 buyFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_BUY_FEE); uint256 ghoOut = DEFAULT_GSM_GHO_AMOUNT - sellFee; // Supply assets to the GSM first vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, sellFee); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); assertTrue(gsmSignerAddr != ALICE, 'Signer is the same as Alice'); // Buy assets as another user ghoFaucet(gsmSignerAddr, DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.prank(gsmSignerAddr); GHO_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_GHO_AMOUNT + buyFee); assertEq(GHO_GSM.nonces(gsmSignerAddr), 0, 'Unexpected before gsmSignerAddr nonce'); bytes32 digest = keccak256( abi.encode( '\x19\x01', GHO_GSM.DOMAIN_SEPARATOR(), GSM_BUY_ASSET_WITH_SIG_TYPEHASH, abi.encode( gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, gsmSignerAddr, GHO_GSM.nonces(gsmSignerAddr), deadline ) ) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmSignerKey, digest); bytes memory signature = abi.encodePacked(r, s, v); assertTrue(gsmSignerAddr != BOB, 'Signer is the same as Bob'); vm.prank(BOB); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit BuyAsset( gsmSignerAddr, gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT + buyFee, buyFee ); GHO_GSM.buyAssetWithSig( gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, gsmSignerAddr, deadline, signature ); assertEq(GHO_GSM.nonces(gsmSignerAddr), 1, 'Unexpected final gsmSignerAddr nonce'); assertEq( USDC_TOKEN.balanceOf(gsmSignerAddr), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected final USDC balance' ); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoOut, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), sellFee + buyFee, 'Unexpected GSM GHO balance'); assertEq(GHO_GSM.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); } function testBuyThenSellAtMaximumBucketCapacity() public { // Use zero fees to simplify amount calculations vm.expectEmit(true, true, false, true, address(GHO_GSM)); emit FeeStrategyUpdated(address(GHO_GSM_FIXED_FEE_STRATEGY), address(0)); GHO_GSM.updateFeeStrategy(address(0)); // Supply assets to the GSM first vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_EXPOSURE); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_EXPOSURE); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_EXPOSURE, DEFAULT_CAPACITY, 0); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_EXPOSURE, ALICE); (uint256 ghoCapacity, uint256 ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM)); assertEq(ghoLevel, ghoCapacity, 'Unexpected GHO bucket level after initial sell'); assertEq( GHO_TOKEN.balanceOf(ALICE), DEFAULT_CAPACITY, 'Unexpected Alice GHO balance after sell' ); // Buy 1 of the underlying GHO_TOKEN.approve(address(GHO_GSM), 1e18); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit BuyAsset(ALICE, ALICE, 1e6, 1e18, 0); GHO_GSM.buyAsset(1e6, ALICE); (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM)); assertEq(ghoLevel, DEFAULT_CAPACITY - 1e18, 'Unexpected GHO bucket level after buy'); assertEq( GHO_TOKEN.balanceOf(ALICE), DEFAULT_CAPACITY - 1e18, 'Unexpected Alice GHO balance after buy' ); assertEq(USDC_TOKEN.balanceOf(ALICE), 1e6, 'Unexpected Alice USDC balance after buy'); // Sell 1 of the underlying USDC_TOKEN.approve(address(GHO_GSM), 1e6); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit SellAsset(ALICE, ALICE, 1e6, 1e18, 0); GHO_GSM.sellAsset(1e6, ALICE); vm.stopPrank(); (ghoCapacity, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM)); assertEq(ghoLevel, ghoCapacity, 'Unexpected GHO bucket level after second sell'); assertEq( GHO_TOKEN.balanceOf(ALICE), DEFAULT_CAPACITY, 'Unexpected Alice GHO balance after second sell' ); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected Alice USDC balance after second sell'); assertEq(GHO_GSM.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); } function testRevertBuyAssetWithSigExpiredSignature() public { uint256 deadline = block.timestamp - 1; bytes32 digest = keccak256( abi.encode( '\x19\x01', GHO_GSM.DOMAIN_SEPARATOR(), GSM_BUY_ASSET_WITH_SIG_TYPEHASH, abi.encode( gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, gsmSignerAddr, GHO_GSM.nonces(gsmSignerAddr), deadline ) ) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmSignerKey, digest); bytes memory signature = abi.encodePacked(r, s, v); assertTrue(gsmSignerAddr != BOB, 'Signer is the same as Bob'); vm.prank(BOB); vm.expectRevert('SIGNATURE_DEADLINE_EXPIRED'); GHO_GSM.buyAssetWithSig( gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, gsmSignerAddr, deadline, signature ); } function testRevertBuyAssetWithSigInvalidSignature() public { uint256 deadline = block.timestamp + 1 hours; bytes32 digest = keccak256( abi.encode( '\x19\x01', GHO_GSM.DOMAIN_SEPARATOR(), GSM_BUY_ASSET_WITH_SIG_TYPEHASH, abi.encode( gsmSignerAddr, DEFAULT_GSM_USDC_AMOUNT, gsmSignerAddr, GHO_GSM.nonces(gsmSignerAddr), deadline ) ) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(gsmSignerKey, digest); bytes memory signature = abi.encodePacked(r, s, v); assertTrue(gsmSignerAddr != BOB, 'Signer is the same as Bob'); vm.prank(BOB); vm.expectRevert('SIGNATURE_INVALID'); GHO_GSM.buyAssetWithSig(BOB, DEFAULT_GSM_USDC_AMOUNT, gsmSignerAddr, deadline, signature); } function testRevertBuyAssetZeroAmount() public { vm.prank(ALICE); vm.expectRevert('INVALID_AMOUNT'); GHO_GSM.buyAsset(0, ALICE); } function testRevertBuyAssetNoGHO() public { uint256 sellFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 buyFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_BUY_FEE); // Supply assets to the GSM first vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, sellFee); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); vm.startPrank(BOB); GHO_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.expectRevert(stdError.arithmeticError); GHO_GSM.buyAsset(DEFAULT_GSM_USDC_AMOUNT, BOB); vm.stopPrank(); } function testRevertBuyAssetNoAllowance() public { uint256 sellFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 buyFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_BUY_FEE); // Supply assets to the GSM first vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, sellFee); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.startPrank(BOB); vm.expectRevert(stdError.arithmeticError); GHO_GSM.buyAsset(DEFAULT_GSM_USDC_AMOUNT, BOB); vm.stopPrank(); } function testGetGhoAmountForBuyAsset() public { (uint256 exactAssetAmount, uint256 ghoSold, uint256 grossAmount, uint256 fee) = GHO_GSM .getGhoAmountForBuyAsset(DEFAULT_GSM_USDC_AMOUNT); uint256 topUpAmount = 1_000_000e18; ghoFaucet(ALICE, topUpAmount); _sellAsset(GHO_GSM, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); uint256 ghoBalanceBefore = GHO_TOKEN.balanceOf(ALICE); uint256 ghoFeesBefore = GHO_TOKEN.balanceOf(address(GHO_GSM)); vm.startPrank(ALICE); GHO_TOKEN.approve(address(GHO_GSM), type(uint256).max); GHO_GSM.buyAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); assertEq(DEFAULT_GSM_USDC_AMOUNT, exactAssetAmount, 'Unexpected asset amount bought'); assertEq(ghoSold - fee, grossAmount, 'Unexpected GHO gross sold amount'); assertEq(ghoBalanceBefore - GHO_TOKEN.balanceOf(ALICE), ghoSold, 'Unexpected GHO sold amount'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM)) - ghoFeesBefore, fee, 'Unexpected GHO fee amount' ); (uint256 assetAmount, uint256 exactGhoSold, uint256 grossAmount2, uint256 fee2) = GHO_GSM .getAssetAmountForBuyAsset(ghoSold); assertEq( ghoBalanceBefore - GHO_TOKEN.balanceOf(ALICE), exactGhoSold, 'Unexpected GHO sold exact amount' ); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected estimation of bought assets'); assertEq(grossAmount, grossAmount2, 'Unexpected GHO gross amount'); assertEq(fee, fee2, 'Unexpected GHO fee amount'); } function testGetGhoAmountForBuyAssetWithZeroFee() public { GHO_GSM.updateFeeStrategy(address(0)); (uint256 exactAssetAmount, uint256 ghoSold, uint256 grossAmount, uint256 fee) = GHO_GSM .getGhoAmountForBuyAsset(DEFAULT_GSM_USDC_AMOUNT); assertEq(fee, 0, 'Unexpected GHO fee amount'); uint256 topUpAmount = 1_000_000e18; ghoFaucet(ALICE, topUpAmount); _sellAsset(GHO_GSM, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); uint256 ghoBalanceBefore = GHO_TOKEN.balanceOf(ALICE); uint256 ghoFeesBefore = GHO_TOKEN.balanceOf(address(GHO_GSM)); vm.startPrank(ALICE); GHO_TOKEN.approve(address(GHO_GSM), type(uint256).max); GHO_GSM.buyAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); assertEq(DEFAULT_GSM_USDC_AMOUNT, exactAssetAmount, 'Unexpected asset amount bought'); assertEq(ghoSold, grossAmount, 'Unexpected GHO gross sold amount'); assertEq(ghoBalanceBefore - GHO_TOKEN.balanceOf(ALICE), ghoSold, 'Unexpected GHO sold amount'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), ghoFeesBefore, 'Unexpected GHO fee amount'); (uint256 assetAmount, uint256 exactGhoSold, uint256 grossAmount2, uint256 fee2) = GHO_GSM .getAssetAmountForBuyAsset(ghoSold); assertEq( ghoBalanceBefore - GHO_TOKEN.balanceOf(ALICE), exactGhoSold, 'Unexpected GHO sold exact amount' ); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected estimation of bought assets'); assertEq(grossAmount, grossAmount2, 'Unexpected GHO gross amount'); assertEq(fee, fee2, 'Unexpected GHO fee amount'); } function testGetGhoAmountForBuyAssetWithZeroAmount() public { (uint256 exactAssetAmount, uint256 ghoSold, uint256 grossAmount, uint256 fee) = GHO_GSM .getGhoAmountForBuyAsset(0); assertEq(exactAssetAmount, 0, 'Unexpected exact asset amount'); assertEq(ghoSold, 0, 'Unexpected GHO sold amount'); assertEq(grossAmount, 0, 'Unexpected GHO gross amount'); assertEq(fee, 0, 'Unexpected GHO fee amount'); (uint256 assetAmount, uint256 exactGhoSold, uint256 grossAmount2, uint256 fee2) = GHO_GSM .getAssetAmountForBuyAsset(ghoSold); assertEq(exactGhoSold, 0, 'Unexpected exact gho bought'); assertEq(assetAmount, 0, 'Unexpected estimation of bought assets'); assertEq(grossAmount, grossAmount2, 'Unexpected GHO gross amount'); assertEq(fee, fee2, 'Unexpected GHO fee amount'); } function testSwapFreeze() public { assertEq(GHO_GSM.getIsFrozen(), false, 'Unexpected freeze status before'); vm.prank(address(GHO_GSM_SWAP_FREEZER)); vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit SwapFreeze(address(GHO_GSM_SWAP_FREEZER), true); GHO_GSM.setSwapFreeze(true); assertEq(GHO_GSM.getIsFrozen(), true, 'Unexpected freeze status after'); } function testRevertFreezeNotAuthorized() public { vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(GSM_SWAP_FREEZER_ROLE, ALICE)); vm.prank(ALICE); GHO_GSM.setSwapFreeze(true); } function testRevertSwapFreezeAlreadyFrozen() public { vm.startPrank(address(GHO_GSM_SWAP_FREEZER)); GHO_GSM.setSwapFreeze(true); vm.expectRevert('GSM_ALREADY_FROZEN'); GHO_GSM.setSwapFreeze(true); vm.stopPrank(); } function testSwapUnfreeze() public { vm.startPrank(address(GHO_GSM_SWAP_FREEZER)); GHO_GSM.setSwapFreeze(true); vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit SwapFreeze(address(GHO_GSM_SWAP_FREEZER), false); GHO_GSM.setSwapFreeze(false); vm.stopPrank(); } function testRevertUnfreezeNotAuthorized() public { vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(GSM_SWAP_FREEZER_ROLE, ALICE)); vm.prank(ALICE); GHO_GSM.setSwapFreeze(false); } function testRevertUnfreezeNotFrozen() public { vm.prank(address(GHO_GSM_SWAP_FREEZER)); vm.expectRevert('GSM_ALREADY_UNFROZEN'); GHO_GSM.setSwapFreeze(false); } function testRevertBuyAndSellWhenSwapFrozen() public { vm.prank(address(GHO_GSM_SWAP_FREEZER)); GHO_GSM.setSwapFreeze(true); vm.expectRevert('GSM_FROZEN'); GHO_GSM.buyAsset(0, ALICE); vm.expectRevert('GSM_FROZEN'); GHO_GSM.sellAsset(0, ALICE); } function testUpdateConfigurator() public { vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit RoleGranted(GSM_CONFIGURATOR_ROLE, ALICE, address(this)); GHO_GSM.grantRole(GSM_CONFIGURATOR_ROLE, ALICE); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit RoleRevoked(GSM_CONFIGURATOR_ROLE, address(this), address(this)); GHO_GSM.revokeRole(GSM_CONFIGURATOR_ROLE, address(this)); } function testRevertUpdateConfiguratorNotAuthorized() public { vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(DEFAULT_ADMIN_ROLE, ALICE)); vm.prank(ALICE); GHO_GSM.grantRole(GSM_CONFIGURATOR_ROLE, ALICE); } function testConfiguratorUpdateMethods() public { // Alice as configurator vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit RoleGranted(GSM_CONFIGURATOR_ROLE, ALICE, address(this)); GHO_GSM.grantRole(GSM_CONFIGURATOR_ROLE, ALICE); vm.startPrank(address(ALICE)); assertEq( GHO_GSM.getFeeStrategy(), address(GHO_GSM_FIXED_FEE_STRATEGY), 'Unexpected fee strategy' ); FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy( DEFAULT_GSM_BUY_FEE, DEFAULT_GSM_SELL_FEE ); vm.expectEmit(true, true, false, true, address(GHO_GSM)); emit FeeStrategyUpdated(address(GHO_GSM_FIXED_FEE_STRATEGY), address(newFeeStrategy)); GHO_GSM.updateFeeStrategy(address(newFeeStrategy)); assertEq(GHO_GSM.getFeeStrategy(), address(newFeeStrategy), 'Unexpected fee strategy'); address newGhoTreasury = address(GHO_GSM); vm.expectEmit(true, true, true, true, address(newGhoTreasury)); emit GhoTreasuryUpdated(TREASURY, newGhoTreasury); GHO_GSM.updateGhoTreasury(newGhoTreasury); assertEq(GHO_GSM.getGhoTreasury(), newGhoTreasury); vm.expectEmit(true, true, false, true, address(GHO_GSM)); emit ExposureCapUpdated(DEFAULT_GSM_USDC_EXPOSURE, 0); GHO_GSM.updateExposureCap(0); assertEq(GHO_GSM.getExposureCap(), 0, 'Unexpected exposure capacity'); vm.expectEmit(true, true, false, true, address(GHO_GSM)); emit ExposureCapUpdated(0, 1000); GHO_GSM.updateExposureCap(1000); assertEq(GHO_GSM.getExposureCap(), 1000, 'Unexpected exposure capacity'); vm.stopPrank(); } function testRevertConfiguratorUpdateMethodsNotAuthorized() public { vm.startPrank(ALICE); vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(DEFAULT_ADMIN_ROLE, ALICE)); GHO_GSM.grantRole(GSM_LIQUIDATOR_ROLE, ALICE); vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(DEFAULT_ADMIN_ROLE, ALICE)); GHO_GSM.grantRole(GSM_SWAP_FREEZER_ROLE, ALICE); vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(GSM_CONFIGURATOR_ROLE, ALICE)); GHO_GSM.updateExposureCap(0); vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(GSM_CONFIGURATOR_ROLE, ALICE)); GHO_GSM.updateGhoTreasury(ALICE); vm.stopPrank(); } function testRevertInitializeTreasuryZeroAddress() public { Gsm gsm = new Gsm( address(GHO_TOKEN), address(USDC_TOKEN), address(GHO_GSM_FIXED_PRICE_STRATEGY) ); vm.expectRevert(bytes('ZERO_ADDRESS_NOT_VALID')); gsm.initialize(address(this), address(0), DEFAULT_GSM_USDC_EXPOSURE); } function testUpdateGhoTreasuryRevertIfZero() public { vm.expectRevert(bytes('ZERO_ADDRESS_NOT_VALID')); GHO_GSM.updateGhoTreasury(address(0)); } function testUpdateGhoTreasury() public { vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit GhoTreasuryUpdated(TREASURY, ALICE); GHO_GSM.updateGhoTreasury(ALICE); assertEq(GHO_GSM.getGhoTreasury(), ALICE); } function testUnauthorizedUpdateGhoTreasuryRevert() public { vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(GSM_CONFIGURATOR_ROLE, ALICE)); vm.prank(ALICE); GHO_GSM.updateGhoTreasury(ALICE); } function testRescueTokens() public { GHO_GSM.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); vm.prank(FAUCET); WETH.mint(address(GHO_GSM), 100e18); assertEq(WETH.balanceOf(address(GHO_GSM)), 100e18, 'Unexpected GSM WETH before balance'); assertEq(WETH.balanceOf(ALICE), 0, 'Unexpected target WETH before balance'); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit TokensRescued(address(WETH), ALICE, 100e18); GHO_GSM.rescueTokens(address(WETH), ALICE, 100e18); assertEq(WETH.balanceOf(address(GHO_GSM)), 0, 'Unexpected GSM WETH after balance'); assertEq(WETH.balanceOf(ALICE), 100e18, 'Unexpected target WETH after balance'); } function testRevertRescueTokensZeroAmount() public { GHO_GSM.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); vm.expectRevert('INVALID_AMOUNT'); GHO_GSM.rescueTokens(address(WETH), ALICE, 0); } function testRescueGhoTokens() public { GHO_GSM.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); ghoFaucet(address(GHO_GSM), 100e18); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), 100e18, 'Unexpected GSM GHO before balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), 0, 'Unexpected target GHO before balance'); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit TokensRescued(address(GHO_TOKEN), ALICE, 100e18); GHO_GSM.rescueTokens(address(GHO_TOKEN), ALICE, 100e18); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), 0, 'Unexpected GSM GHO after balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), 100e18, 'Unexpected target GHO after balance'); } function testRescueGhoTokensWithAccruedFees() public { GHO_GSM.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); uint256 fee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); assertGt(fee, 0, 'Fee not greater than zero'); vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, fee); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), fee, 'Unexpected GSM GHO balance'); ghoFaucet(address(GHO_GSM), 1); assertEq(GHO_TOKEN.balanceOf(BOB), 0, 'Unexpected target GHO balance before'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), fee + 1, 'Unexpected GSM GHO balance before'); vm.expectRevert('INSUFFICIENT_GHO_TO_RESCUE'); GHO_GSM.rescueTokens(address(GHO_TOKEN), BOB, fee); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit TokensRescued(address(GHO_TOKEN), BOB, 1); GHO_GSM.rescueTokens(address(GHO_TOKEN), BOB, 1); assertEq(GHO_TOKEN.balanceOf(BOB), 1, 'Unexpected target GHO balance after'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), fee, 'Unexpected GSM GHO balance after'); } function testRevertRescueGhoTokens() public { GHO_GSM.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); vm.expectRevert('INSUFFICIENT_GHO_TO_RESCUE'); GHO_GSM.rescueTokens(address(GHO_TOKEN), ALICE, 1); } function testRescueUnderlyingTokens() public { GHO_GSM.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); vm.prank(FAUCET); USDC_TOKEN.mint(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected USDC balance before'); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit TokensRescued(address(USDC_TOKEN), ALICE, DEFAULT_GSM_USDC_AMOUNT); GHO_GSM.rescueTokens(address(USDC_TOKEN), ALICE, DEFAULT_GSM_USDC_AMOUNT); assertEq(USDC_TOKEN.balanceOf(ALICE), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected USDC balance after'); } function testRescueUnderlyingTokensWithAccruedFees() public { GHO_GSM.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); uint256 currentGSMBalance = DEFAULT_GSM_USDC_AMOUNT; assertEq( USDC_TOKEN.balanceOf(address(GHO_GSM)), currentGSMBalance, 'Unexpected GSM USDC balance before' ); vm.prank(FAUCET); USDC_TOKEN.mint(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); assertEq( USDC_TOKEN.balanceOf(address(GHO_GSM)), currentGSMBalance + DEFAULT_GSM_USDC_AMOUNT, 'Unexpected GSM USDC balance before, post-mint' ); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected target USDC balance before'); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit TokensRescued(address(USDC_TOKEN), ALICE, DEFAULT_GSM_USDC_AMOUNT); GHO_GSM.rescueTokens(address(USDC_TOKEN), ALICE, DEFAULT_GSM_USDC_AMOUNT); assertEq( USDC_TOKEN.balanceOf(address(GHO_GSM)), currentGSMBalance, 'Unexpected GSM USDC balance after' ); assertEq( USDC_TOKEN.balanceOf(ALICE), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected target USDC balance after' ); } function testRevertRescueUnderlyingTokens() public { GHO_GSM.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); vm.expectRevert('INSUFFICIENT_EXOGENOUS_ASSET_TO_RESCUE'); GHO_GSM.rescueTokens(address(USDC_TOKEN), ALICE, 1); } function testSeize() public { assertEq(GHO_GSM.getIsSeized(), false, 'Unexpected seize status before'); vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); assertEq(USDC_TOKEN.balanceOf(TREASURY), 0, 'Unexpected USDC before token balance'); vm.prank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit Seized( address(GHO_GSM_LAST_RESORT_LIQUIDATOR), BOB, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT ); uint256 seizedAmount = GHO_GSM.seize(); assertEq(GHO_GSM.getIsSeized(), true, 'Unexpected seize status after'); assertEq(seizedAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected seized amount'); assertEq( USDC_TOKEN.balanceOf(TREASURY), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected USDC after token balance' ); assertEq(GHO_GSM.getAvailableLiquidity(), 0, 'Unexpected available liquidity'); assertEq( GHO_GSM.getAvailableUnderlyingExposure(), 0, 'Unexpected underlying exposure available' ); assertEq(GHO_GSM.getExposureCap(), 0, 'Unexpected exposure capacity'); } function testRevertSeizeWithoutAuthorization() public { vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(GSM_LIQUIDATOR_ROLE, address(this))); GHO_GSM.seize(); } function testRevertMethodsAfterSeizure() public { vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); vm.prank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); uint256 seizedAmount = GHO_GSM.seize(); assertEq(seizedAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected seized amount'); vm.expectRevert('GSM_SEIZED'); GHO_GSM.buyAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.expectRevert('GSM_SEIZED'); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.expectRevert('GSM_SEIZED'); GHO_GSM.seize(); } function testBurnAfterSeize() public { vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); vm.prank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); uint256 seizedAmount = GHO_GSM.seize(); assertEq(seizedAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected seized amount'); vm.expectRevert('FACILITATOR_BUCKET_LEVEL_NOT_ZERO'); GHO_TOKEN.removeFacilitator(address(GHO_GSM)); ghoFaucet(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), DEFAULT_GSM_GHO_AMOUNT); vm.startPrank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); GHO_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_GHO_AMOUNT); vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit BurnAfterSeize(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), DEFAULT_GSM_GHO_AMOUNT, 0); uint256 burnedAmount = GHO_GSM.burnAfterSeize(DEFAULT_GSM_GHO_AMOUNT); vm.stopPrank(); assertEq(burnedAmount, DEFAULT_GSM_GHO_AMOUNT, 'Unexpected burned amount of GHO'); vm.expectEmit(true, false, false, true, address(GHO_TOKEN)); emit FacilitatorRemoved(address(GHO_GSM)); GHO_TOKEN.removeFacilitator(address(GHO_GSM)); } function testBurnAfterSeizeGreaterAmount() public { vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); vm.prank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); uint256 seizedAmount = GHO_GSM.seize(); assertEq(seizedAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected seized amount'); ghoFaucet(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), DEFAULT_GSM_GHO_AMOUNT + 1); vm.startPrank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); GHO_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_GHO_AMOUNT + 1); vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit BurnAfterSeize(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), DEFAULT_GSM_GHO_AMOUNT, 0); uint256 burnedAmount = GHO_GSM.burnAfterSeize(DEFAULT_GSM_GHO_AMOUNT + 1); vm.stopPrank(); assertEq(burnedAmount, DEFAULT_GSM_GHO_AMOUNT, 'Unexpected burned amount of GHO'); } function testRevertBurnAfterSeizeNotSeized() public { vm.expectRevert('GSM_NOT_SEIZED'); vm.prank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); GHO_GSM.burnAfterSeize(1); } function testRevertBurnAfterInvalidAmount() public { vm.startPrank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); GHO_GSM_4626.seize(); vm.expectRevert('INVALID_AMOUNT'); GHO_GSM_4626.burnAfterSeize(0); vm.stopPrank(); } function testRevertBurnAfterSeizeUnauthorized() public { vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(GSM_LIQUIDATOR_ROLE, address(this))); GHO_GSM.burnAfterSeize(1); } function testDistributeFeesToTreasury() public { uint256 fee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, fee); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), fee, 'Unexpected GSM GHO balance'); assertEq(GHO_GSM.getAccruedFees(), fee, 'Unexpected GSM accrued fees'); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit FeesDistributedToTreasury( TREASURY, address(GHO_TOKEN), GHO_TOKEN.balanceOf(address(GHO_GSM)) ); GHO_GSM.distributeFeesToTreasury(); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM)), 0, 'Unexpected GSM GHO balance post-distribution' ); assertEq(GHO_TOKEN.balanceOf(TREASURY), fee, 'Unexpected GHO balance in treasury'); assertEq(GHO_GSM.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); } function testDistributeYieldToTreasuryDoNothing() public { uint256 gsmBalanceBefore = GHO_TOKEN.balanceOf(address(GHO_GSM)); uint256 treasuryBalanceBefore = GHO_TOKEN.balanceOf(address(TREASURY)); assertEq(GHO_GSM.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); vm.record(); GHO_GSM.distributeFeesToTreasury(); (, bytes32[] memory writes) = vm.accesses(address(GHO_GSM)); assertEq(writes.length, 0, 'Unexpected update of accrued fees'); assertEq(GHO_GSM.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM)), gsmBalanceBefore, 'Unexpected GSM GHO balance post-distribution' ); assertEq( GHO_TOKEN.balanceOf(TREASURY), treasuryBalanceBefore, 'Unexpected GHO balance in treasury' ); } function testGetAccruedFees() public { assertEq(GHO_GSM.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); uint256 sellFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 buyFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_BUY_FEE); _sellAsset(GHO_GSM, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), sellFee, 'Unexpected GSM GHO balance'); assertEq(GHO_GSM.getAccruedFees(), sellFee, 'Unexpected GSM accrued fees'); ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.startPrank(BOB); GHO_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit BuyAsset(BOB, BOB, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT + buyFee, buyFee); GHO_GSM.buyAsset(DEFAULT_GSM_USDC_AMOUNT, BOB); vm.stopPrank(); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM)), sellFee + buyFee, 'Unexpected GSM GHO balance'); assertEq(GHO_GSM.getAccruedFees(), sellFee + buyFee, 'Unexpected GSM accrued fees'); } function testGetAccruedFeesWithZeroFee() public { vm.expectEmit(true, true, false, true, address(GHO_GSM)); emit FeeStrategyUpdated(address(GHO_GSM_FIXED_FEE_STRATEGY), address(0)); GHO_GSM.updateFeeStrategy(address(0)); assertEq(GHO_GSM.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); for (uint256 i = 0; i < 10; i++) { _sellAsset(GHO_GSM, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); assertEq(GHO_GSM.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT); vm.startPrank(BOB); GHO_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_GHO_AMOUNT); GHO_GSM.buyAsset(DEFAULT_GSM_USDC_AMOUNT, BOB); vm.stopPrank(); assertEq(GHO_GSM.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); } } function testCanSwap() public { assertEq(GHO_GSM.canSwap(), true, 'Unexpected initial swap state'); // Freeze the GSM vm.startPrank(address(GHO_GSM_SWAP_FREEZER)); GHO_GSM.setSwapFreeze(true); assertEq(GHO_GSM.canSwap(), false, 'Unexpected swap state post-freeze'); // Unfreeze the GSM GHO_GSM.setSwapFreeze(false); assertEq(GHO_GSM.canSwap(), true, 'Unexpected swap state post-unfreeze'); vm.stopPrank(); // Seize the GSM vm.prank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); GHO_GSM.seize(); assertEq(GHO_GSM.canSwap(), false, 'Unexpected swap state post-seize'); } function testUpdateExposureCapBelowCurrentExposure() public { assertEq(GHO_GSM.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure cap'); vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, 2 * DEFAULT_GSM_USDC_AMOUNT); // Alice as configurator GHO_GSM.grantRole(GSM_CONFIGURATOR_ROLE, ALICE); vm.startPrank(address(ALICE)); GHO_GSM.updateFeeStrategy(address(0)); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); assertEq( GHO_GSM.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available underlying exposure' ); assertEq(GHO_GSM.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure cap'); // Update exposure cap to smaller value than current exposure uint256 currentExposure = GHO_GSM.getAvailableLiquidity(); uint256 newExposureCap = currentExposure - 1; GHO_GSM.updateExposureCap(uint128(newExposureCap)); assertEq(GHO_GSM.getExposureCap(), newExposureCap, 'Unexpected exposure cap'); assertEq(GHO_GSM.getAvailableLiquidity(), currentExposure, 'Unexpected current exposure'); // Reducing exposure to 0 GHO_GSM.updateExposureCap(0); // Sell cannot be executed vm.expectRevert('EXOGENOUS_ASSET_EXPOSURE_TOO_HIGH'); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); // Buy some asset to reduce current exposure vm.stopPrank(); ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT / 2); vm.startPrank(BOB); GHO_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_GHO_AMOUNT / 2); GHO_GSM.buyAsset(DEFAULT_GSM_USDC_AMOUNT / 2, BOB); assertEq(GHO_GSM.getExposureCap(), 0, 'Unexpected exposure capacity'); } } ================================================ FILE: src/test/TestGsm4626.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGsm4626 is TestGhoBase { using PercentageMath for uint256; using PercentageMath for uint128; function testConstructor() public { Gsm4626 gsm = new Gsm4626( address(GHO_TOKEN), address(USDC_4626_TOKEN), address(GHO_GSM_4626_FIXED_PRICE_STRATEGY) ); assertEq(gsm.GHO_TOKEN(), address(GHO_TOKEN), 'Unexpected GHO token address'); assertEq( gsm.UNDERLYING_ASSET(), address(USDC_4626_TOKEN), 'Unexpected underlying asset address' ); assertEq( gsm.PRICE_STRATEGY(), address(GHO_GSM_4626_FIXED_PRICE_STRATEGY), 'Unexpected price strategy' ); } function testRevertConstructorInvalidPriceStrategy() public { FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy(1e18, address(GHO_TOKEN), 18); vm.expectRevert('INVALID_PRICE_STRATEGY'); new Gsm4626(address(GHO_TOKEN), address(USDC_4626_TOKEN), address(newPriceStrategy)); } function testRevertConstructorZeroAddressParams() public { vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); new Gsm4626(address(0), address(USDC_4626_TOKEN), address(GHO_GSM_4626_FIXED_PRICE_STRATEGY)); vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); new Gsm4626(address(GHO_TOKEN), address(0), address(GHO_GSM_4626_FIXED_PRICE_STRATEGY)); } function testInitialize() public { Gsm4626 gsm = new Gsm4626( address(GHO_TOKEN), address(USDC_4626_TOKEN), address(GHO_GSM_4626_FIXED_PRICE_STRATEGY) ); vm.expectEmit(true, true, true, true); emit RoleGranted(DEFAULT_ADMIN_ROLE, address(this), address(this)); vm.expectEmit(true, true, false, true); emit ExposureCapUpdated(0, DEFAULT_GSM_USDC_EXPOSURE); gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); assertEq(gsm.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); } function testRevertInitializeTwice() public { Gsm4626 gsm = new Gsm4626( address(GHO_TOKEN), address(USDC_4626_TOKEN), address(GHO_GSM_4626_FIXED_PRICE_STRATEGY) ); gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); vm.expectRevert('Contract instance has already been initialized'); gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); } function testSellAssetZeroFee() public { vm.expectEmit(true, true, false, true, address(GHO_GSM_4626)); emit FeeStrategyUpdated(address(GHO_GSM_FIXED_FEE_STRATEGY), address(0)); GHO_GSM_4626.updateFeeStrategy(address(0)); _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, 0); (uint256 assetAmount, uint256 ghoBought) = GHO_GSM_4626.sellAsset( DEFAULT_GSM_USDC_AMOUNT, ALICE ); vm.stopPrank(); assertEq(ghoBought, DEFAULT_GSM_GHO_AMOUNT, 'Unexpected GHO amount bought'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected asset amount sold'); assertEq(USDC_4626_TOKEN.balanceOf(ALICE), 0, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), DEFAULT_GSM_GHO_AMOUNT, 'Unexpected final GHO balance'); assertEq( GHO_GSM_4626.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity' ); } function testSellAsset() public { uint256 fee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 ghoOut = DEFAULT_GSM_GHO_AMOUNT - fee; _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); assertEq( USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(ALICE)), DEFAULT_GSM_USDC_AMOUNT ); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, fee); (uint256 assetAmount, uint256 ghoBought) = GHO_GSM_4626.sellAsset( DEFAULT_GSM_USDC_AMOUNT, ALICE ); vm.stopPrank(); assertEq(ghoBought, DEFAULT_GSM_GHO_AMOUNT - fee, 'Unexpected GHO amount bought'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected asset amount sold'); assertEq(USDC_4626_TOKEN.balanceOf(ALICE), 0, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoOut, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), fee, 'Unexpected GSM GHO balance'); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available underlying exposure' ); assertEq( GHO_GSM_4626.getAvailableLiquidity(), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available liquidity' ); assertEq( GHO_GSM_4626.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity' ); } function testSellAssetSendToOther() public { uint256 fee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 ghoOut = DEFAULT_GSM_GHO_AMOUNT - fee; _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit SellAsset(ALICE, BOB, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, fee); (uint256 assetAmount, uint256 ghoBought) = GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, BOB); vm.stopPrank(); assertEq(ghoBought, DEFAULT_GSM_GHO_AMOUNT - fee, 'Unexpected GHO amount bought'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected asset amount sold'); assertEq(USDC_4626_TOKEN.balanceOf(ALICE), 0, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), 0, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(BOB), ghoOut, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), fee, 'Unexpected GSM GHO balance'); assertEq( GHO_GSM_4626.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity' ); } function testRevertSellAssetTooMuchUnderlyingExposure() public { Gsm4626 gsm = new Gsm4626( address(GHO_TOKEN), address(USDC_4626_TOKEN), address(GHO_GSM_4626_FIXED_PRICE_STRATEGY) ); gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE - 1); GHO_TOKEN.addFacilitator(address(gsm), 'GSM Modified Exposure Cap', DEFAULT_CAPACITY); _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_EXPOSURE); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(gsm), DEFAULT_GSM_USDC_EXPOSURE); vm.expectRevert('EXOGENOUS_ASSET_EXPOSURE_TOO_HIGH'); gsm.sellAsset(DEFAULT_GSM_USDC_EXPOSURE, ALICE); vm.stopPrank(); } function testGetGhoAmountForSellAsset() public { (uint256 exactAssetAmount, uint256 ghoBought, uint256 grossAmount, uint256 fee) = GHO_GSM_4626 .getGhoAmountForSellAsset(DEFAULT_GSM_USDC_AMOUNT); _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); assertEq( DEFAULT_GSM_USDC_AMOUNT - USDC_4626_TOKEN.balanceOf(ALICE), exactAssetAmount, 'Unexpected asset amount sold' ); assertEq(ghoBought + fee, grossAmount, 'Unexpected GHO gross amount'); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoBought, 'Unexpected GHO bought amount'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), fee, 'Unexpected GHO fee amount'); (uint256 assetAmount, uint256 exactGhoBought, uint256 grossAmount2, uint256 fee2) = GHO_GSM_4626 .getAssetAmountForSellAsset(ghoBought); assertEq(GHO_TOKEN.balanceOf(ALICE), exactGhoBought, 'Unexpected GHO bought amount'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected estimation of sold assets'); assertEq(grossAmount, grossAmount2, 'Unexpected GHO gross amount'); assertEq(fee, fee2, 'Unexpected GHO fee amount'); } function testGetGhoAmountForSellAssetWithZeroFee() public { GHO_GSM_4626.updateFeeStrategy(address(0)); (uint256 exactAssetAmount, uint256 ghoBought, uint256 grossAmount, uint256 fee) = GHO_GSM_4626 .getGhoAmountForSellAsset(DEFAULT_GSM_USDC_AMOUNT); assertEq(fee, 0, 'Unexpected GHO fee amount'); _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); assertEq( DEFAULT_GSM_USDC_AMOUNT - USDC_4626_TOKEN.balanceOf(ALICE), exactAssetAmount, 'Unexpected asset amount sold' ); assertEq(ghoBought, grossAmount, 'Unexpected GHO gross amount'); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoBought, 'Unexpected GHO bought amount'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), 0, 'Unexpected GHO fee amount'); (uint256 assetAmount, uint256 exactGhoBought, uint256 grossAmount2, uint256 fee2) = GHO_GSM_4626 .getAssetAmountForSellAsset(ghoBought); assertEq(GHO_TOKEN.balanceOf(ALICE), exactGhoBought, 'Unexpected GHO bought amount'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected estimation of sold assets'); assertEq(grossAmount, grossAmount2, 'Unexpected GHO gross amount'); assertEq(fee, fee2, 'Unexpected GHO fee amount'); } function testGetGhoAmountForSellAssetWithZeroAmount() public { (uint256 exactAssetAmount, uint256 ghoBought, uint256 grossAmount, uint256 fee) = GHO_GSM_4626 .getGhoAmountForSellAsset(0); assertEq(exactAssetAmount, 0, 'Unexpected exact asset amount'); assertEq(ghoBought, 0, 'Unexpected GHO bought amount'); assertEq(grossAmount, 0, 'Unexpected GHO gross amount'); assertEq(fee, 0, 'Unexpected GHO fee amount'); (uint256 assetAmount, uint256 exactGhoBought, uint256 grossAmount2, uint256 fee2) = GHO_GSM_4626 .getAssetAmountForSellAsset(ghoBought); assertEq(exactGhoBought, 0, 'Unexpected exact gho bought'); assertEq(assetAmount, 0, 'Unexpected estimation of sold assets'); assertEq(grossAmount, grossAmount2, 'Unexpected GHO gross amount'); assertEq(fee, fee2, 'Unexpected GHO fee amount'); } function testBuyAssetZeroFee() public { vm.expectEmit(true, true, false, true, address(GHO_GSM_4626)); emit FeeStrategyUpdated(address(GHO_GSM_FIXED_FEE_STRATEGY), address(0)); GHO_GSM_4626.updateFeeStrategy(address(0)); // Supply assets to the GSM first _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, 0); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); // Buy assets as another user ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT); vm.startPrank(BOB); GHO_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_GHO_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit BuyAsset(BOB, BOB, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, 0); (uint256 assetAmount, uint256 ghoSold) = GHO_GSM_4626.buyAsset(DEFAULT_GSM_USDC_AMOUNT, BOB); vm.stopPrank(); assertEq(ghoSold, DEFAULT_GSM_GHO_AMOUNT, 'Unexpected GHO amount sold'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected asset amount bought'); assertEq( USDC_4626_TOKEN.balanceOf(BOB), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected final USDC balance' ); assertEq(GHO_TOKEN.balanceOf(ALICE), DEFAULT_GSM_GHO_AMOUNT, 'Unexpected final GHO balance'); assertEq( GHO_GSM_4626.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity' ); } function testBuyAsset() public { uint256 sellFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 buyFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_BUY_FEE); uint256 ghoOut = DEFAULT_GSM_GHO_AMOUNT - sellFee; // Supply assets to the GSM first _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, sellFee); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); // Buy assets as another user ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.startPrank(BOB); GHO_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit BuyAsset(BOB, BOB, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT + buyFee, buyFee); (uint256 assetAmount, uint256 ghoSold) = GHO_GSM_4626.buyAsset(DEFAULT_GSM_USDC_AMOUNT, BOB); vm.stopPrank(); assertEq(ghoSold, DEFAULT_GSM_GHO_AMOUNT + buyFee, 'Unexpected GHO amount sold'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected asset amount bought'); assertEq( USDC_4626_TOKEN.balanceOf(BOB), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected final USDC balance' ); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoOut, 'Unexpected final GHO balance'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), sellFee + buyFee, 'Unexpected GSM GHO balance' ); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected available underlying exposure' ); assertEq(GHO_GSM_4626.getAvailableLiquidity(), 0, 'Unexpected available liquidity'); assertEq( GHO_GSM_4626.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity' ); } function testBuyAssetSendToOther() public { uint256 sellFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 buyFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_BUY_FEE); uint256 ghoOut = DEFAULT_GSM_GHO_AMOUNT - sellFee; // Supply assets to the GSM first _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, sellFee); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); // Buy assets as another user ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.startPrank(BOB); GHO_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit BuyAsset(BOB, CHARLES, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT + buyFee, buyFee); (uint256 assetAmount, uint256 ghoSold) = GHO_GSM_4626.buyAsset( DEFAULT_GSM_USDC_AMOUNT, CHARLES ); vm.stopPrank(); assertEq(ghoSold, DEFAULT_GSM_GHO_AMOUNT + buyFee, 'Unexpected GHO amount sold'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected asset amount bought'); assertEq(USDC_4626_TOKEN.balanceOf(BOB), 0, 'Unexpected final USDC balance'); assertEq( USDC_4626_TOKEN.balanceOf(CHARLES), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected final USDC balance' ); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoOut, 'Unexpected final GHO balance'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), sellFee + buyFee, 'Unexpected GSM GHO balance' ); assertEq( GHO_GSM_4626.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity' ); } function testBuyThenSellAtMaximumBucketCapacity() public { // Use zero fees to simplify amount calculations vm.expectEmit(true, true, false, true, address(GHO_GSM_4626)); emit FeeStrategyUpdated(address(GHO_GSM_FIXED_FEE_STRATEGY), address(0)); GHO_GSM_4626.updateFeeStrategy(address(0)); // Supply assets to the GSM first _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_EXPOSURE); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_EXPOSURE); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_EXPOSURE, DEFAULT_CAPACITY, 0); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_EXPOSURE, ALICE); (uint256 ghoCapacity, uint256 ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(ghoLevel, ghoCapacity, 'Unexpected GHO bucket level after initial sell'); assertEq( GHO_TOKEN.balanceOf(ALICE), DEFAULT_CAPACITY, 'Unexpected Alice GHO balance after sell' ); // Buy 1 of the underlying GHO_TOKEN.approve(address(GHO_GSM_4626), 1e18); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit BuyAsset(ALICE, ALICE, 1e6, 1e18, 0); GHO_GSM_4626.buyAsset(1e6, ALICE); (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(ghoLevel, DEFAULT_CAPACITY - 1e18, 'Unexpected GHO bucket level after buy'); assertEq( GHO_TOKEN.balanceOf(ALICE), DEFAULT_CAPACITY - 1e18, 'Unexpected Alice GHO balance after buy' ); assertEq(USDC_4626_TOKEN.balanceOf(ALICE), 1e6, 'Unexpected Alice USDC balance after buy'); // Sell 1 of the underlying USDC_4626_TOKEN.approve(address(GHO_GSM_4626), 1e6); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit SellAsset(ALICE, ALICE, 1e6, 1e18, 0); GHO_GSM_4626.sellAsset(1e6, ALICE); vm.stopPrank(); (ghoCapacity, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(ghoLevel, ghoCapacity, 'Unexpected GHO bucket level after second sell'); assertEq( GHO_TOKEN.balanceOf(ALICE), DEFAULT_CAPACITY, 'Unexpected Alice GHO balance after second sell' ); assertEq( USDC_4626_TOKEN.balanceOf(ALICE), 0, 'Unexpected Alice USDC balance after second sell' ); assertEq( GHO_GSM_4626.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity' ); } function testRevertBuyAssetZeroAmount() public { vm.prank(ALICE); vm.expectRevert('INVALID_AMOUNT'); GHO_GSM_4626.buyAsset(0, ALICE); } function testRevertBuyAssetNoGHO() public { uint256 sellFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 buyFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_BUY_FEE); // Supply assets to the GSM first _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, sellFee); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); vm.startPrank(BOB); GHO_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.expectRevert(stdError.arithmeticError); GHO_GSM_4626.buyAsset(DEFAULT_GSM_USDC_AMOUNT, BOB); vm.stopPrank(); } function testRevertBuyAssetNoAllowance() public { uint256 sellFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 buyFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_BUY_FEE); // Supply assets to the GSM first _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, sellFee); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.startPrank(BOB); vm.expectRevert(stdError.arithmeticError); GHO_GSM_4626.buyAsset(DEFAULT_GSM_USDC_AMOUNT, BOB); vm.stopPrank(); } function testGetGhoAmountForBuyAsset() public { (uint256 exactAssetAmount, uint256 ghoSold, uint256 grossAmount, uint256 fee) = GHO_GSM_4626 .getGhoAmountForBuyAsset(DEFAULT_GSM_USDC_AMOUNT); uint256 topUpAmount = 1_000_000e18; ghoFaucet(ALICE, topUpAmount); _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); uint256 ghoBalanceBefore = GHO_TOKEN.balanceOf(ALICE); uint256 ghoFeesBefore = GHO_TOKEN.balanceOf(address(GHO_GSM_4626)); vm.startPrank(ALICE); GHO_TOKEN.approve(address(GHO_GSM_4626), type(uint256).max); GHO_GSM_4626.buyAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); assertEq(DEFAULT_GSM_USDC_AMOUNT, exactAssetAmount, 'Unexpected asset amount bought'); assertEq(ghoSold - fee, grossAmount, 'Unexpected GHO gross sold amount'); assertEq(ghoBalanceBefore - GHO_TOKEN.balanceOf(ALICE), ghoSold, 'Unexpected GHO sold amount'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)) - ghoFeesBefore, fee, 'Unexpected GHO fee amount' ); (uint256 assetAmount, uint256 exactGhoSold, uint256 grossAmount2, uint256 fee2) = GHO_GSM_4626 .getAssetAmountForBuyAsset(ghoSold); assertEq( ghoBalanceBefore - GHO_TOKEN.balanceOf(ALICE), exactGhoSold, 'Unexpected GHO sold exact amount' ); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected estimation of bought assets'); assertEq(grossAmount, grossAmount2, 'Unexpected GHO gross amount'); assertEq(fee, fee2, 'Unexpected GHO fee amount'); } function testGetGhoAmountForBuyAssetWithZeroFee() public { GHO_GSM_4626.updateFeeStrategy(address(0)); (uint256 exactAssetAmount, uint256 ghoSold, uint256 grossAmount, uint256 fee) = GHO_GSM_4626 .getGhoAmountForBuyAsset(DEFAULT_GSM_USDC_AMOUNT); assertEq(fee, 0, 'Unexpected GHO fee amount'); uint256 topUpAmount = 1_000_000e18; ghoFaucet(ALICE, topUpAmount); _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); uint256 ghoBalanceBefore = GHO_TOKEN.balanceOf(ALICE); uint256 ghoFeesBefore = GHO_TOKEN.balanceOf(address(GHO_GSM_4626)); vm.startPrank(ALICE); GHO_TOKEN.approve(address(GHO_GSM_4626), type(uint256).max); GHO_GSM_4626.buyAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); assertEq(DEFAULT_GSM_USDC_AMOUNT, exactAssetAmount, 'Unexpected asset amount bought'); assertEq(ghoSold, grossAmount, 'Unexpected GHO gross sold amount'); assertEq(ghoBalanceBefore - GHO_TOKEN.balanceOf(ALICE), ghoSold, 'Unexpected GHO sold amount'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), ghoFeesBefore, 'Unexpected GHO fee amount' ); (uint256 assetAmount, uint256 exactGhoSold, uint256 grossAmount2, uint256 fee2) = GHO_GSM_4626 .getAssetAmountForBuyAsset(ghoSold); assertEq( ghoBalanceBefore - GHO_TOKEN.balanceOf(ALICE), exactGhoSold, 'Unexpected GHO sold exact amount' ); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected estimation of bought assets'); assertEq(grossAmount, grossAmount2, 'Unexpected GHO gross amount'); assertEq(fee, fee2, 'Unexpected GHO fee amount'); } function testGetGhoAmountForBuyAssetWithZeroAmount() public { (uint256 exactAssetAmount, uint256 ghoSold, uint256 grossAmount, uint256 fee) = GHO_GSM_4626 .getGhoAmountForBuyAsset(0); assertEq(exactAssetAmount, 0, 'Unexpected exact asset amount'); assertEq(ghoSold, 0, 'Unexpected GHO sold amount'); assertEq(grossAmount, 0, 'Unexpected GHO gross amount'); assertEq(fee, 0, 'Unexpected GHO fee amount'); (uint256 assetAmount, uint256 exactGhoSold, uint256 grossAmount2, uint256 fee2) = GHO_GSM_4626 .getAssetAmountForBuyAsset(ghoSold); assertEq(exactGhoSold, 0, 'Unexpected exact gho bought'); assertEq(assetAmount, 0, 'Unexpected estimation of bought assets'); assertEq(grossAmount, grossAmount2, 'Unexpected GHO gross amount'); assertEq(fee, fee2, 'Unexpected GHO fee amount'); } function testSwapFreeze() public { assertEq(GHO_GSM_4626.getIsFrozen(), false, 'Unexpected freeze status before'); vm.prank(address(GHO_GSM_SWAP_FREEZER)); vm.expectEmit(true, false, false, true, address(GHO_GSM_4626)); emit SwapFreeze(address(GHO_GSM_SWAP_FREEZER), true); GHO_GSM_4626.setSwapFreeze(true); assertEq(GHO_GSM_4626.getIsFrozen(), true, 'Unexpected freeze status after'); } function testRevertFreezeNotAuthorized() public { vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(GSM_SWAP_FREEZER_ROLE, ALICE)); vm.prank(ALICE); GHO_GSM_4626.setSwapFreeze(true); } function testRevertSwapFreezeAlreadyFrozen() public { vm.startPrank(address(GHO_GSM_SWAP_FREEZER)); GHO_GSM_4626.setSwapFreeze(true); vm.expectRevert('GSM_ALREADY_FROZEN'); GHO_GSM_4626.setSwapFreeze(true); vm.stopPrank(); } function testSwapUnfreeze() public { vm.startPrank(address(GHO_GSM_SWAP_FREEZER)); GHO_GSM_4626.setSwapFreeze(true); vm.expectEmit(true, false, false, true, address(GHO_GSM_4626)); emit SwapFreeze(address(GHO_GSM_SWAP_FREEZER), false); GHO_GSM_4626.setSwapFreeze(false); vm.stopPrank(); } function testRevertUnfreezeNotAuthorized() public { vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(GSM_SWAP_FREEZER_ROLE, ALICE)); vm.prank(ALICE); GHO_GSM_4626.setSwapFreeze(false); } function testRevertUnfreezeNotFrozen() public { vm.prank(address(GHO_GSM_SWAP_FREEZER)); vm.expectRevert('GSM_ALREADY_UNFROZEN'); GHO_GSM_4626.setSwapFreeze(false); } function testRevertBuyAndSellWhenSwapFrozen() public { vm.prank(address(GHO_GSM_SWAP_FREEZER)); GHO_GSM_4626.setSwapFreeze(true); vm.expectRevert('GSM_FROZEN'); GHO_GSM_4626.buyAsset(0, ALICE); vm.expectRevert('GSM_FROZEN'); GHO_GSM_4626.sellAsset(0, ALICE); } function testUpdateConfigurator() public { vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit RoleGranted(GSM_CONFIGURATOR_ROLE, ALICE, address(this)); GHO_GSM_4626.grantRole(GSM_CONFIGURATOR_ROLE, ALICE); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit RoleRevoked(GSM_CONFIGURATOR_ROLE, address(this), address(this)); GHO_GSM_4626.revokeRole(GSM_CONFIGURATOR_ROLE, address(this)); } function testRevertUpdateConfiguratorNotAuthorized() public { vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(DEFAULT_ADMIN_ROLE, ALICE)); vm.prank(ALICE); GHO_GSM_4626.grantRole(GSM_CONFIGURATOR_ROLE, ALICE); } function testConfiguratorUpdateMethods() public { // Alice as configurator vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit RoleGranted(GSM_CONFIGURATOR_ROLE, ALICE, address(this)); GHO_GSM_4626.grantRole(GSM_CONFIGURATOR_ROLE, ALICE); vm.startPrank(address(ALICE)); assertEq( GHO_GSM_4626.getFeeStrategy(), address(GHO_GSM_FIXED_FEE_STRATEGY), 'Unexpected fee strategy' ); FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy( DEFAULT_GSM_BUY_FEE, DEFAULT_GSM_SELL_FEE ); vm.expectEmit(true, true, false, true, address(GHO_GSM_4626)); emit FeeStrategyUpdated(address(GHO_GSM_FIXED_FEE_STRATEGY), address(newFeeStrategy)); GHO_GSM_4626.updateFeeStrategy(address(newFeeStrategy)); assertEq(GHO_GSM_4626.getFeeStrategy(), address(newFeeStrategy), 'Unexpected fee strategy'); vm.expectEmit(true, true, false, true, address(GHO_GSM_4626)); emit ExposureCapUpdated(DEFAULT_GSM_USDC_EXPOSURE, 0); GHO_GSM_4626.updateExposureCap(0); assertEq(GHO_GSM_4626.getExposureCap(), 0, 'Unexpected exposure capacity'); vm.expectEmit(true, true, false, true, address(GHO_GSM_4626)); emit ExposureCapUpdated(0, 1000); GHO_GSM_4626.updateExposureCap(1000); assertEq(GHO_GSM_4626.getExposureCap(), 1000, 'Unexpected exposure capacity'); vm.stopPrank(); } function testRevertConfiguratorUpdateMethodsNotAuthorized() public { vm.startPrank(ALICE); vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(DEFAULT_ADMIN_ROLE, ALICE)); GHO_GSM_4626.grantRole(GSM_LIQUIDATOR_ROLE, ALICE); vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(DEFAULT_ADMIN_ROLE, ALICE)); GHO_GSM_4626.grantRole(GSM_SWAP_FREEZER_ROLE, ALICE); vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(GSM_CONFIGURATOR_ROLE, ALICE)); GHO_GSM_4626.updateExposureCap(0); vm.stopPrank(); } function testUpdateGhoTreasuryRevertIfZero() public { vm.expectRevert(bytes('ZERO_ADDRESS_NOT_VALID')); GHO_GSM_4626.updateGhoTreasury(address(0)); } function testUpdateGhoTreasury() public { vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit GhoTreasuryUpdated(TREASURY, ALICE); GHO_GSM_4626.updateGhoTreasury(ALICE); assertEq(GHO_GSM_4626.getGhoTreasury(), ALICE); } function testUnauthorizedUpdateGhoTreasuryRevert() public { vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(GSM_CONFIGURATOR_ROLE, ALICE)); vm.prank(ALICE); GHO_GSM_4626.updateGhoTreasury(ALICE); } function testRescueTokens() public { GHO_GSM_4626.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); vm.prank(FAUCET); WETH.mint(address(GHO_GSM_4626), 100e18); assertEq(WETH.balanceOf(address(GHO_GSM_4626)), 100e18, 'Unexpected GSM WETH before balance'); assertEq(WETH.balanceOf(ALICE), 0, 'Unexpected target WETH before balance'); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit TokensRescued(address(WETH), ALICE, 100e18); GHO_GSM_4626.rescueTokens(address(WETH), ALICE, 100e18); assertEq(WETH.balanceOf(address(GHO_GSM_4626)), 0, 'Unexpected GSM WETH after balance'); assertEq(WETH.balanceOf(ALICE), 100e18, 'Unexpected target WETH after balance'); } function testRescueGhoTokens() public { GHO_GSM_4626.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); ghoFaucet(address(GHO_GSM_4626), 100e18); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), 100e18, 'Unexpected GSM GHO before balance' ); assertEq(GHO_TOKEN.balanceOf(ALICE), 0, 'Unexpected target GHO before balance'); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit TokensRescued(address(GHO_TOKEN), ALICE, 100e18); GHO_GSM_4626.rescueTokens(address(GHO_TOKEN), ALICE, 100e18); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), 0, 'Unexpected GSM GHO after balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), 100e18, 'Unexpected target GHO after balance'); } function testRescueGhoTokensWithAccruedFees() public { GHO_GSM_4626.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); uint256 fee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); assertGt(fee, 0, 'Fee not greater than zero'); _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, fee); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), fee, 'Unexpected GSM GHO balance'); ghoFaucet(address(GHO_GSM_4626), 1); assertEq(GHO_TOKEN.balanceOf(BOB), 0, 'Unexpected target GHO balance before'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), fee + 1, 'Unexpected GSM GHO balance before' ); vm.expectRevert('INSUFFICIENT_GHO_TO_RESCUE'); GHO_GSM_4626.rescueTokens(address(GHO_TOKEN), BOB, fee); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit TokensRescued(address(GHO_TOKEN), BOB, 1); GHO_GSM_4626.rescueTokens(address(GHO_TOKEN), BOB, 1); assertEq(GHO_TOKEN.balanceOf(BOB), 1, 'Unexpected target GHO balance after'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), fee, 'Unexpected GSM GHO balance after'); } function testRevertRescueGhoTokens() public { GHO_GSM_4626.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); vm.expectRevert('INSUFFICIENT_GHO_TO_RESCUE'); GHO_GSM_4626.rescueTokens(address(GHO_TOKEN), ALICE, 1); } function testRescueUnderlyingTokens() public { GHO_GSM_4626.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); assertEq(USDC_4626_TOKEN.balanceOf(ALICE), 0, 'Unexpected USDC balance before'); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit TokensRescued(address(USDC_4626_TOKEN), ALICE, DEFAULT_GSM_USDC_AMOUNT); GHO_GSM_4626.rescueTokens(address(USDC_4626_TOKEN), ALICE, DEFAULT_GSM_USDC_AMOUNT); assertEq( USDC_4626_TOKEN.balanceOf(ALICE), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected USDC balance after' ); } function testRescueUnderlyingTokensWithAccruedFees() public { GHO_GSM_4626.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); uint256 currentGSMBalance = DEFAULT_GSM_USDC_AMOUNT; assertEq( USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), currentGSMBalance, 'Unexpected GSM USDC balance before' ); _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); assertEq( USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), currentGSMBalance + DEFAULT_GSM_USDC_AMOUNT, 'Unexpected GSM USDC balance before, post-mint' ); assertEq(USDC_4626_TOKEN.balanceOf(ALICE), 0, 'Unexpected target USDC balance before'); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit TokensRescued(address(USDC_4626_TOKEN), ALICE, DEFAULT_GSM_USDC_AMOUNT); GHO_GSM_4626.rescueTokens(address(USDC_4626_TOKEN), ALICE, DEFAULT_GSM_USDC_AMOUNT); assertEq( USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), currentGSMBalance, 'Unexpected GSM USDC balance after' ); assertEq( USDC_4626_TOKEN.balanceOf(ALICE), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected target USDC balance after' ); } function testRevertRescueUnderlyingTokens() public { GHO_GSM_4626.grantRole(GSM_TOKEN_RESCUER_ROLE, address(this)); vm.expectRevert('INSUFFICIENT_EXOGENOUS_ASSET_TO_RESCUE'); GHO_GSM_4626.rescueTokens(address(USDC_4626_TOKEN), ALICE, 1); } function testSeize() public { assertEq(GHO_GSM_4626.getIsSeized(), false, 'Unexpected seize status before'); _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); assertEq(USDC_4626_TOKEN.balanceOf(TREASURY), 0, 'Unexpected USDC before token balance'); vm.prank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); vm.expectEmit(true, false, false, true, address(GHO_GSM_4626)); emit Seized( address(GHO_GSM_LAST_RESORT_LIQUIDATOR), BOB, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT ); uint256 seizedAmount = GHO_GSM_4626.seize(); assertEq(seizedAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected seized amount'); assertEq(GHO_GSM_4626.getIsSeized(), true, 'Unexpected seize status after'); assertEq( USDC_4626_TOKEN.balanceOf(TREASURY), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected USDC after token balance' ); assertEq(GHO_GSM_4626.getExposureCap(), 0, 'Unexpected exposure capacity'); } function testRevertSeizeWithoutAuthorization() public { vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(GSM_LIQUIDATOR_ROLE, address(this))); GHO_GSM_4626.seize(); } function testRevertMethodsAfterSeizure() public { _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); vm.prank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); uint256 seizedAmount = GHO_GSM_4626.seize(); assertEq(seizedAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected seized amount'); vm.expectRevert('GSM_SEIZED'); GHO_GSM_4626.buyAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.expectRevert('GSM_SEIZED'); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.expectRevert('GSM_SEIZED'); GHO_GSM_4626.seize(); GHO_GSM_4626.grantRole(GSM_CONFIGURATOR_ROLE, BOB); vm.startPrank(BOB); vm.expectRevert('GSM_SEIZED'); GHO_GSM_4626.backWithGho(1); vm.expectRevert('GSM_SEIZED'); GHO_GSM_4626.backWithUnderlying(1); vm.stopPrank(); } function testBurnAfterSeize() public { _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); vm.prank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); uint256 seizedAmount = GHO_GSM_4626.seize(); assertEq(seizedAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected seized amount'); vm.expectRevert('FACILITATOR_BUCKET_LEVEL_NOT_ZERO'); GHO_TOKEN.removeFacilitator(address(GHO_GSM_4626)); ghoFaucet(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), DEFAULT_GSM_GHO_AMOUNT); vm.startPrank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); GHO_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_GHO_AMOUNT); vm.expectEmit(true, false, false, true, address(GHO_GSM_4626)); emit BurnAfterSeize(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), DEFAULT_GSM_GHO_AMOUNT, 0); uint256 burnedAmount = GHO_GSM_4626.burnAfterSeize(DEFAULT_GSM_GHO_AMOUNT); vm.stopPrank(); assertEq(burnedAmount, DEFAULT_GSM_GHO_AMOUNT, 'Unexpected burned amount of GHO'); vm.expectEmit(true, false, false, true, address(GHO_TOKEN)); emit FacilitatorRemoved(address(GHO_GSM_4626)); GHO_TOKEN.removeFacilitator(address(GHO_GSM_4626)); } function testBurnAfterSeizeGreaterAmount() public { _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); vm.prank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); uint256 seizedAmount = GHO_GSM_4626.seize(); assertEq(seizedAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected seized amount'); ghoFaucet(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), DEFAULT_GSM_GHO_AMOUNT + 1); vm.startPrank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); GHO_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_GHO_AMOUNT + 1); vm.expectEmit(true, false, false, true, address(GHO_GSM_4626)); emit BurnAfterSeize(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), DEFAULT_GSM_GHO_AMOUNT, 0); uint256 burnedAmount = GHO_GSM_4626.burnAfterSeize(DEFAULT_GSM_GHO_AMOUNT + 1); vm.stopPrank(); assertEq(burnedAmount, DEFAULT_GSM_GHO_AMOUNT, 'Unexpected burned amount of GHO'); } function testRevertBurnAfterInvalidAmount() public { vm.startPrank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); GHO_GSM_4626.seize(); vm.expectRevert('INVALID_AMOUNT'); GHO_GSM_4626.burnAfterSeize(0); vm.stopPrank(); } function testRevertBurnAfterSeizeNotSeized() public { vm.expectRevert('GSM_NOT_SEIZED'); vm.prank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); GHO_GSM_4626.burnAfterSeize(1); } function testRevertBurnAfterSeizeUnauthorized() public { vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(GSM_LIQUIDATOR_ROLE, address(this))); GHO_GSM_4626.burnAfterSeize(1); } function testInjectGho() public { _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); (uint256 excess, uint256 deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, 0, 'Unexpected excess value of GHO'); assertEq(deficit, 0, 'Unexpected deficit of GHO'); _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_AMOUNT / 2, false); GHO_GSM_4626.grantRole(GSM_CONFIGURATOR_ROLE, BOB); (excess, deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, 0, 'Unexpected excess value of GHO'); assertEq(deficit, DEFAULT_GSM_GHO_AMOUNT / 2, 'Unexpected deficit of GHO'); ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT / 2); vm.startPrank(BOB); GHO_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_GHO_AMOUNT / 2); vm.expectEmit(true, true, false, true, address(GHO_GSM_4626)); emit BackingProvided( BOB, address(GHO_TOKEN), DEFAULT_GSM_GHO_AMOUNT / 2, DEFAULT_GSM_GHO_AMOUNT / 2, 0 ); uint256 ghoUsedForBacking = GHO_GSM_4626.backWithGho(DEFAULT_GSM_GHO_AMOUNT / 2); assertEq(DEFAULT_GSM_GHO_AMOUNT / 2, ghoUsedForBacking); vm.stopPrank(); (excess, deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, 0, 'Unexpected excess value of GHO'); assertEq(deficit, 0, 'Unexpected deficit of GHO'); } function testInjectGhoMoreThanNeeded() public { _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); (uint256 excess, uint256 deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, 0, 'Unexpected excess value of GHO'); assertEq(deficit, 0, 'Unexpected deficit of GHO'); GHO_GSM_4626.grantRole(GSM_CONFIGURATOR_ROLE, ALICE); _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_AMOUNT / 2, false); ghoFaucet(address(this), (DEFAULT_GSM_GHO_AMOUNT / 2) + 1); GHO_TOKEN.approve(address(GHO_GSM_4626), type(uint256).max); uint256 balanceBefore = GHO_TOKEN.balanceOf(address(this)); (, uint256 ghoLevelBefore) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(GHO_GSM_4626)); uint256 ghoUsedForBacking = GHO_GSM_4626.backWithGho((DEFAULT_GSM_GHO_AMOUNT / 2) + 1); uint256 balanceAfter = GHO_TOKEN.balanceOf(address(this)); (, uint256 ghoLevelAfter) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(DEFAULT_GSM_GHO_AMOUNT / 2, ghoUsedForBacking); assertEq(balanceBefore - balanceAfter, ghoUsedForBacking); assertEq(ghoLevelBefore - ghoLevelAfter, ghoUsedForBacking); (excess, deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, 0, 'Unexpected excess value of GHO'); assertEq(deficit, 0, 'Unexpected deficit of GHO'); } function testInjectUnderlying() public { _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); (uint256 excess, uint256 deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, 0, 'Unexpected excess value of GHO'); assertEq(deficit, 0, 'Unexpected deficit of GHO'); _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_AMOUNT / 2, false); (excess, deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, 0, 'Unexpected excess value of GHO'); assertEq(deficit, DEFAULT_GSM_GHO_AMOUNT / 2, 'Unexpected deficit of GHO'); GHO_GSM_4626.grantRole(GSM_CONFIGURATOR_ROLE, BOB); _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, BOB, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(BOB); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, false, true, address(GHO_GSM_4626)); emit BackingProvided( BOB, address(USDC_4626_TOKEN), DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT / 2, 0 ); uint256 usdcUsedForBacking = GHO_GSM_4626.backWithUnderlying(DEFAULT_GSM_USDC_AMOUNT); assertEq(DEFAULT_GSM_USDC_AMOUNT, usdcUsedForBacking); vm.stopPrank(); (excess, deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, 0, 'Unexpected excess value of GHO'); assertEq(deficit, 0, 'Unexpected deficit of GHO'); } function testInjectUnderlyingMoreThanNeeded() public { _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); (uint256 excess, uint256 deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, 0, 'Unexpected excess value of GHO'); assertEq(deficit, 0, 'Unexpected deficit of GHO'); _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_AMOUNT / 2, false); (excess, deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, 0, 'Unexpected excess value of GHO'); assertEq(deficit, DEFAULT_GSM_GHO_AMOUNT / 2, 'Unexpected deficit of GHO'); GHO_GSM_4626.grantRole(GSM_CONFIGURATOR_ROLE, BOB); _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, BOB, DEFAULT_GSM_USDC_AMOUNT + 1); vm.startPrank(BOB); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT + 1); vm.expectEmit(true, true, false, true, address(GHO_GSM_4626)); emit BackingProvided( BOB, address(USDC_4626_TOKEN), DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT / 2, 0 ); uint256 usdcUsedForBacking = GHO_GSM_4626.backWithUnderlying(DEFAULT_GSM_USDC_AMOUNT + 1); assertEq(DEFAULT_GSM_USDC_AMOUNT, usdcUsedForBacking); vm.stopPrank(); (excess, deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, 0, 'Unexpected excess value of GHO'); assertEq(deficit, 0, 'Unexpected deficit of GHO'); } function testRevertBackWithNotAuthorized() public { vm.startPrank(ALICE); vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(GSM_CONFIGURATOR_ROLE, ALICE)); GHO_GSM_4626.backWithGho(0); vm.expectRevert(AccessControlErrorsLib.MISSING_ROLE(GSM_CONFIGURATOR_ROLE, ALICE)); GHO_GSM_4626.backWithUnderlying(0); vm.stopPrank(); } function testRevertBackWithZeroAmount() public { vm.expectRevert('INVALID_AMOUNT'); GHO_GSM_4626.backWithGho(0); vm.expectRevert('INVALID_AMOUNT'); GHO_GSM_4626.backWithUnderlying(0); } function testRevertBackWithNoDeficit() public { (uint256 excess, uint256 deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, 0, 'Unexpected excess value of GHO'); assertEq(deficit, 0, 'Unexpected deficit of GHO'); vm.expectRevert('NO_CURRENT_DEFICIT_BACKING'); GHO_GSM_4626.backWithGho(1); vm.expectRevert('NO_CURRENT_DEFICIT_BACKING'); GHO_GSM_4626.backWithUnderlying(1); } function testDistributeFeesToTreasury() public { uint256 fee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, fee); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), fee, 'Unexpected GSM GHO balance'); assertEq(GHO_GSM_4626.getAccruedFees(), fee, 'Unexpected GSM accrued fees'); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit FeesDistributedToTreasury( TREASURY, address(GHO_TOKEN), GHO_TOKEN.balanceOf(address(GHO_GSM_4626)) ); GHO_GSM_4626.distributeFeesToTreasury(); assertEq(GHO_GSM_4626.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), 0, 'Unexpected GSM GHO balance post-distribution' ); assertEq(GHO_TOKEN.balanceOf(TREASURY), fee, 'Unexpected GHO balance in treasury'); } function testDistributeYieldToTreasuryDoNothing() public { uint256 gsmBalanceBefore = GHO_TOKEN.balanceOf(address(GHO_GSM_4626)); uint256 treasuryBalanceBefore = GHO_TOKEN.balanceOf(address(TREASURY)); assertEq(GHO_GSM_4626.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); vm.record(); GHO_GSM_4626.distributeFeesToTreasury(); (, bytes32[] memory writes) = vm.accesses(address(GHO_GSM_4626)); assertEq(writes.length, 0, 'Unexpected update of accrued fees'); assertEq(GHO_GSM_4626.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), gsmBalanceBefore, 'Unexpected GSM GHO balance post-distribution' ); assertEq( GHO_TOKEN.balanceOf(TREASURY), treasuryBalanceBefore, 'Unexpected GHO balance in treasury' ); } function testGetAccruedFees() public { assertEq(GHO_GSM_4626.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); uint256 sellFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 buyFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_BUY_FEE); _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), sellFee, 'Unexpected GSM GHO balance'); assertEq(GHO_GSM_4626.getAccruedFees(), sellFee, 'Unexpected GSM accrued fees'); ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.startPrank(BOB); GHO_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_GHO_AMOUNT + buyFee); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit BuyAsset(BOB, BOB, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT + buyFee, buyFee); GHO_GSM_4626.buyAsset(DEFAULT_GSM_USDC_AMOUNT, BOB); vm.stopPrank(); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), sellFee + buyFee, 'Unexpected GSM GHO balance' ); assertEq(GHO_GSM_4626.getAccruedFees(), sellFee + buyFee, 'Unexpected GSM accrued fees'); } function testGetAccruedFeesWithZeroFee() public { vm.expectEmit(true, true, false, true, address(GHO_GSM_4626)); emit FeeStrategyUpdated(address(GHO_GSM_FIXED_FEE_STRATEGY), address(0)); GHO_GSM_4626.updateFeeStrategy(address(0)); assertEq(GHO_GSM_4626.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); for (uint256 i = 0; i < 10; i++) { _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); assertEq(GHO_GSM_4626.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT); vm.startPrank(BOB); GHO_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_GHO_AMOUNT); GHO_GSM_4626.buyAsset(DEFAULT_GSM_USDC_AMOUNT, BOB); vm.stopPrank(); assertEq(GHO_GSM_4626.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); } } } ================================================ FILE: src/test/TestGsm4626Edge.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGsm4626Edge is TestGhoBase { using PercentageMath for uint256; using PercentageMath for uint128; function testOngoingExposureSellAsset() public { (, uint256 ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(ghoLevel, 0); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), 0); assertEq(USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), 0); assertEq(GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE); assertEq(GHO_GSM_4626.getAvailableLiquidity(), 0); uint128 sellAssetAmount = DEFAULT_GSM_USDC_AMOUNT; uint256 sellFee = DEFAULT_GSM_GHO_AMOUNT.percentMul(DEFAULT_GSM_SELL_FEE); uint256 calcGhoMinted = DEFAULT_GSM_GHO_AMOUNT; uint256 calcExposure = DEFAULT_GSM_USDC_AMOUNT; uint256 ghoBought = _sellAsset( GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, sellAssetAmount ); assertEq(ghoBought, calcGhoMinted - sellFee, 'Unexpected GHO amount bought'); (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(ghoLevel, calcGhoMinted); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), sellAssetAmount); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - calcExposure ); assertEq(GHO_GSM_4626.getAvailableLiquidity(), calcExposure); assertEq( USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), sellAssetAmount ); // Inflate exchange rate _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, sellAssetAmount, true); // same exposure assertEq(ghoLevel, calcGhoMinted); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), calcExposure); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - calcExposure ); assertEq(GHO_GSM_4626.getAvailableLiquidity(), calcExposure); assertEq( USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), sellAssetAmount * 2 ); // more GHO minted with same amount sold uint256 ghoAmountBefore = GHO_TOKEN.balanceOf(ALICE); uint256 ghoReceived = ghoAmountBefore; sellFee = (DEFAULT_GSM_GHO_AMOUNT * 2).percentMul(DEFAULT_GSM_SELL_FEE); calcGhoMinted += DEFAULT_GSM_GHO_AMOUNT * 2; calcExposure += DEFAULT_GSM_USDC_AMOUNT; ghoBought = _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, sellAssetAmount); assertEq(ghoBought, (DEFAULT_GSM_GHO_AMOUNT * 2) - sellFee, 'Unexpected GHO amount bought'); uint256 ghoAmountAfter = GHO_TOKEN.balanceOf(ALICE) - ghoAmountBefore; assertEq(ghoAmountAfter, ghoReceived * 2); (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(ghoLevel, calcGhoMinted); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), calcExposure); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - calcExposure ); assertEq(GHO_GSM_4626.getAvailableLiquidity(), calcExposure); assertEq( USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), sellAssetAmount * 2 * 2 ); // Deflate exchange rate _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, sellAssetAmount * 3, false); // same exposure assertEq(ghoLevel, calcGhoMinted); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), calcExposure); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - calcExposure ); assertEq(GHO_GSM_4626.getAvailableLiquidity(), calcExposure); assertEq( USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), sellAssetAmount ); // less GHO minted with same amount sold ghoAmountBefore = GHO_TOKEN.balanceOf(ALICE); sellFee = (DEFAULT_GSM_GHO_AMOUNT / 2).percentMul(DEFAULT_GSM_SELL_FEE); calcGhoMinted += DEFAULT_GSM_GHO_AMOUNT / 2; calcExposure += DEFAULT_GSM_USDC_AMOUNT; ghoBought = _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, sellAssetAmount); assertEq(ghoBought, (DEFAULT_GSM_GHO_AMOUNT / 2) - sellFee, 'Unexpected GHO amount bought'); ghoAmountAfter = GHO_TOKEN.balanceOf(ALICE) - ghoAmountBefore; assertEq(ghoAmountAfter, ghoReceived / 2); } function testSellAssetWithHighExchangeRate() public { uint256 resultingAssets = DEFAULT_GSM_USDC_AMOUNT * 2; uint256 grossAmount = DEFAULT_GSM_GHO_AMOUNT * 2; uint256 fee = grossAmount.percentMul(DEFAULT_GSM_SELL_FEE); uint256 ghoOut = grossAmount - fee; _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); // Inflate exchange rate _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_AMOUNT, true); assertEq(USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(ALICE)), resultingAssets); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, grossAmount, fee); (uint256 assetAmount, uint256 ghoBought) = GHO_GSM_4626.sellAsset( DEFAULT_GSM_USDC_AMOUNT, ALICE ); vm.stopPrank(); assertEq(ghoBought, ghoOut, 'Unexpected GHO amount bought'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected asset amount sold'); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoOut, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), fee, 'Unexpected GSM GHO balance'); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available underlying exposure' ); assertEq( GHO_GSM_4626.getAvailableLiquidity(), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available liquidity' ); } function testSellAssetWithLowExchangeRate() public { uint256 resultingAssets = DEFAULT_GSM_USDC_AMOUNT / 2; uint256 grossAmount = DEFAULT_GSM_GHO_AMOUNT / 2; uint256 fee = grossAmount.percentMul(DEFAULT_GSM_SELL_FEE); uint256 ghoOut = grossAmount - fee; _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); // Deflate exchange rate _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_AMOUNT / 2, false); assertEq(USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(ALICE)), resultingAssets); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, grossAmount, fee); (uint256 assetAmount, uint256 ghoBought) = GHO_GSM_4626.sellAsset( DEFAULT_GSM_USDC_AMOUNT, ALICE ); vm.stopPrank(); assertEq(ghoBought, ghoOut, 'Unexpected GHO amount bought'); assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected asset amount sold'); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoOut, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), fee, 'Unexpected GSM GHO balance'); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available underlying exposure' ); assertEq( GHO_GSM_4626.getAvailableLiquidity(), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available liquidity' ); } function testExposureLimitWithSharpExchangeRate() public { Gsm4626 gsm = new Gsm4626( address(GHO_TOKEN), address(USDC_4626_TOKEN), address(GHO_GSM_4626_FIXED_PRICE_STRATEGY) ); gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE - 1); GHO_TOKEN.addFacilitator(address(gsm), 'GSM Modified Exposure Cap', DEFAULT_CAPACITY); uint128 depositAmount = DEFAULT_GSM_USDC_EXPOSURE / 2; _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, depositAmount); // Inflate exchange rate _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, depositAmount, true); assertEq( USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(ALICE)), DEFAULT_GSM_USDC_EXPOSURE ); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(gsm), depositAmount); gsm.sellAsset(depositAmount, ALICE); assertEq(gsm.getAvailableLiquidity(), depositAmount); assertEq(gsm.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - 1 - depositAmount); vm.stopPrank(); } function testRevertExposureWithSharpExchangeRate() public { Gsm4626 gsm = new Gsm4626( address(GHO_TOKEN), address(USDC_4626_TOKEN), address(GHO_GSM_4626_FIXED_PRICE_STRATEGY) ); gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE - 1); GHO_TOKEN.addFacilitator(address(gsm), 'GSM Modified Exposure Cap', DEFAULT_CAPACITY); uint128 depositAmount = DEFAULT_GSM_USDC_EXPOSURE * 2; _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, depositAmount); // Deflate exchange rate _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_EXPOSURE, false); assertEq( USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(ALICE)), DEFAULT_GSM_USDC_EXPOSURE ); vm.prank(ALICE); vm.expectRevert('EXOGENOUS_ASSET_EXPOSURE_TOO_HIGH'); gsm.sellAsset(depositAmount, ALICE); } function testDistributeYieldToTreasury() public { /** * 1. Alice sellAsset with 1:1 exchangeRate * 2. ExchangeRate increases, so there is an excess of backing * 3. Distribute GHO fees to treasury, which redirect excess yield in form of GHO too */ uint256 grossAmount = DEFAULT_GSM_GHO_AMOUNT; uint256 fee = grossAmount.percentMul(DEFAULT_GSM_SELL_FEE); uint256 ghoOut = grossAmount - fee; _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); assertEq(GHO_GSM_4626.getAccruedFees(), fee, 'Unexpected GSM accrued fees'); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoOut, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), fee, 'Unexpected GSM GHO balance'); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available underlying exposure' ); assertEq( GHO_GSM_4626.getAvailableLiquidity(), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available liquidity' ); uint256 backingBefore = USDC_4626_TOKEN.previewRedeem( USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)) ); // Inflate exchange rate _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_AMOUNT, true); // Accrued dees does not change assertEq(GHO_GSM_4626.getAccruedFees(), fee, 'Unexpected GSM accrued fees'); // Same underlying exposure assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available underlying exposure' ); assertEq( GHO_GSM_4626.getAvailableLiquidity(), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available liquidity' ); // More backing than before uint256 backingAfter = USDC_4626_TOKEN.previewRedeem( USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)) ); assertEq(backingAfter, backingBefore * 2); // Distribute fees and yield in form of GHO to the treasury uint256 totalBackedGho = GHO_GSM_4626_FIXED_PRICE_STRATEGY.getAssetPriceInGho( GHO_GSM_4626.getAvailableLiquidity(), true ); (, uint256 totalMintedGho) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(totalBackedGho, totalMintedGho + DEFAULT_GSM_GHO_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit FeesDistributedToTreasury( TREASURY, address(GHO_TOKEN), GHO_TOKEN.balanceOf(address(GHO_GSM_4626)) + DEFAULT_GSM_GHO_AMOUNT ); // Accrued fees does not change, only upon swap action or distribution of fees assertEq(GHO_GSM_4626.getAccruedFees(), fee, 'Unexpected GSM accrued fees'); GHO_GSM_4626.distributeFeesToTreasury(); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), 0, 'Unexpected GSM GHO balance post-distribution' ); assertEq(GHO_GSM_4626.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); assertEq( GHO_TOKEN.balanceOf(TREASURY), fee + DEFAULT_GSM_GHO_AMOUNT, 'Unexpected GHO balance in treasury' ); (, totalMintedGho) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq( totalBackedGho, GHO_GSM_4626_FIXED_PRICE_STRATEGY.getAssetPriceInGho( GHO_GSM_4626.getAvailableLiquidity(), false ) ); assertEq(totalBackedGho, totalMintedGho); } function testDistributeYieldToTreasuryDoNothing() public { uint256 gsmBalanceBefore = GHO_TOKEN.balanceOf(address(GHO_GSM_4626)); uint256 treasuryBalanceBefore = GHO_TOKEN.balanceOf(address(TREASURY)); assertEq(GHO_GSM_4626.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); GHO_GSM_4626.distributeFeesToTreasury(); assertEq(GHO_GSM_4626.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), gsmBalanceBefore, 'Unexpected GSM GHO balance post-distribution' ); assertEq( GHO_TOKEN.balanceOf(TREASURY), treasuryBalanceBefore, 'Unexpected GHO balance in treasury' ); } function testDistributeYieldToTreasuryWithNoExcess() public { /** * 1. Alice sellAsset with 1:1 exchangeRate * 2. Distribute GHO fees to treasury, but there is no yield from excess backing */ uint256 grossAmount = DEFAULT_GSM_GHO_AMOUNT; uint256 fee = grossAmount.percentMul(DEFAULT_GSM_SELL_FEE); uint256 ghoOut = grossAmount - fee; _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); assertEq(GHO_GSM_4626.getAccruedFees(), fee, 'Unexpected GSM accrued fees'); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoOut, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), fee, 'Unexpected GSM GHO balance'); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available underlying exposure' ); assertEq( GHO_GSM_4626.getAvailableLiquidity(), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available liquidity' ); // Distribute fees, with no yield in GHO to redirect vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit FeesDistributedToTreasury( TREASURY, address(GHO_TOKEN), GHO_TOKEN.balanceOf(address(GHO_GSM_4626)) ); uint256 totalBackedGho = GHO_GSM_4626_FIXED_PRICE_STRATEGY.getAssetPriceInGho( GHO_GSM_4626.getAvailableLiquidity(), false ); (, uint256 totalMintedGho) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(totalBackedGho, totalMintedGho); GHO_GSM_4626.distributeFeesToTreasury(); assertEq(GHO_GSM_4626.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), 0, 'Unexpected GSM GHO balance post-distribution' ); assertEq(GHO_TOKEN.balanceOf(TREASURY), fee, 'Unexpected GHO balance in treasury'); (, totalMintedGho) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(totalBackedGho, totalMintedGho); } function testDistributeYieldToTreasuryWithLosses() public { /** * 1. Alice sellAsset with 1:1 exchangeRate * 2. ExchangeRate decreases, so there is a loss * 3. Distribute of GHO fees only * 4. Portion of minted GHO unbacked */ uint256 grossAmount = DEFAULT_GSM_GHO_AMOUNT; uint256 fee = grossAmount.percentMul(DEFAULT_GSM_SELL_FEE); uint256 ghoOut = grossAmount - fee; _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); assertEq(GHO_GSM_4626.getAccruedFees(), fee, 'Unexpected GSM accrued fees'); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoOut, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), fee, 'Unexpected GSM GHO balance'); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available underlying exposure' ); assertEq( GHO_GSM_4626.getAvailableLiquidity(), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available liquidity' ); uint256 backingBefore = USDC_4626_TOKEN.previewRedeem( USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)) ); // Deflate exchange rate _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_AMOUNT / 2, false); assertEq( USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), DEFAULT_GSM_USDC_AMOUNT / 2 ); // Accrued fees does not change assertEq(GHO_GSM_4626.getAccruedFees(), fee, 'Unexpected GSM accrued fees'); // Same underlying exposure assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available underlying exposure' ); assertEq( GHO_GSM_4626.getAvailableLiquidity(), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available liquidity' ); // Less backing than before uint256 backingAfter = USDC_4626_TOKEN.previewRedeem( USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)) ); assertEq(backingAfter, backingBefore / 2); // Distribute fees uint256 totalBackedGho = GHO_GSM_4626_FIXED_PRICE_STRATEGY.getAssetPriceInGho( GHO_GSM_4626.getAvailableLiquidity(), false ); (, uint256 totalMintedGho) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(totalBackedGho, totalMintedGho / 2); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit FeesDistributedToTreasury( TREASURY, address(GHO_TOKEN), GHO_TOKEN.balanceOf(address(GHO_GSM_4626)) ); GHO_GSM_4626.distributeFeesToTreasury(); assertEq(GHO_GSM_4626.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), 0, 'Unexpected GSM GHO balance post-distribution' ); assertEq(GHO_TOKEN.balanceOf(TREASURY), fee, 'Unexpected GHO balance in treasury'); assertEq(totalBackedGho, totalMintedGho / 2); } function testDistributeYieldToTreasuryWithExcessExceedingCapacity() public { /** * 1. Alice sellAsset with 1:1 exchangeRate * 2. Facilitator capacity set to an amount less than the accrued fees * 3. ExchangeRate increases, so there is an excess of backing * 4. The distribution fees should mint up to the remaining capacity */ uint256 ongoingAccruedFees = 0; uint256 grossAmount = DEFAULT_GSM_GHO_AMOUNT; uint256 fee = grossAmount.percentMul(DEFAULT_GSM_SELL_FEE); uint256 ghoOut = grossAmount - fee; _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); ongoingAccruedFees += fee; assertEq(GHO_GSM_4626.getAccruedFees(), ongoingAccruedFees, 'Unexpected GSM accrued fees'); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoOut, 'Unexpected final GHO balance'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), fee, 'Unexpected GSM GHO balance'); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available underlying exposure' ); assertEq( GHO_GSM_4626.getAvailableLiquidity(), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available liquidity' ); // set the capacity to be less than the amount of fees accrued uint128 feePercentToMint = 0.3e4; // 30% uint128 margin = uint128(fee.percentMul(feePercentToMint)); uint128 capacity = DEFAULT_GSM_GHO_AMOUNT + margin; GHO_TOKEN.setFacilitatorBucketCapacity(address(GHO_GSM_4626), capacity); // Inflate exchange rate _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_AMOUNT, true); (uint256 excessBeforeDistribution, uint256 deficitBeforeDistribution) = GHO_GSM_4626 .getCurrentBacking(); assertEq(excessBeforeDistribution, (DEFAULT_GSM_USDC_AMOUNT) * 1e12, 'Unexpected excess'); assertEq(deficitBeforeDistribution, 0, 'Unexpected non-zero deficit'); (uint256 ghoCapacity, uint256 ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); uint256 ghoAvailableToMint = ghoCapacity - ghoLevel; assertEq(ghoAvailableToMint, margin, 'Unexpected GHO amount available to mint'); // Fee distribution vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit FeesDistributedToTreasury(TREASURY, address(GHO_TOKEN), ongoingAccruedFees + margin); GHO_GSM_4626.distributeFeesToTreasury(); (ghoCapacity, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); ghoAvailableToMint = ghoCapacity - ghoLevel; assertEq(ghoAvailableToMint, 0); assertEq(GHO_GSM_4626.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), 0, 'Unexpected GSM GHO balance'); assertEq( GHO_TOKEN.balanceOf(address(TREASURY)), ongoingAccruedFees + margin, 'Unexpected Treasury GHO balance' ); (uint256 excessAfterDistribution, uint256 deficitAfterDistribution) = GHO_GSM_4626 .getCurrentBacking(); assertEq( excessAfterDistribution, excessBeforeDistribution - fee.percentMul(feePercentToMint), 'Unexpected excess' ); assertEq(deficitAfterDistribution, 0, 'Unexpected non-zero deficit'); } function testGetAccruedFeesWithHighExchangeRate() public { /** * 1. Alice sellAsset with 1:1 exchangeRate * 2. ExchangeRate increases, so there is an excess of backing * 3. Accrued fees does not factor in new yield in form of GHO * 4. A new sellAsset does not accrue fees from yield (only the swap fee) * 5. A new buyAsset accrues fees from the swap fee and yield * 6. The distribution of fees does not add new fees */ uint256 ongoingAccruedFees = 0; uint256 grossAmount = DEFAULT_GSM_GHO_AMOUNT; uint256 sellFee = grossAmount.percentMul(DEFAULT_GSM_SELL_FEE); uint256 ghoOut = grossAmount - sellFee; _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); ongoingAccruedFees += sellFee; assertEq(GHO_GSM_4626.getAccruedFees(), ongoingAccruedFees, 'Unexpected GSM accrued fees'); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoOut, 'Unexpected final GHO balance'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), ongoingAccruedFees, 'Unexpected GSM GHO balance' ); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available underlying exposure' ); assertEq( GHO_GSM_4626.getAvailableLiquidity(), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available liquidity' ); // Inflate exchange rate _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_AMOUNT, true); // Accrued dees does not change assertEq(GHO_GSM_4626.getAccruedFees(), ongoingAccruedFees, 'Unexpected GSM accrued fees'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), ongoingAccruedFees, 'Unexpected GSM GHO balance' ); // Yield in form of GHO, not accrued yet uint256 totalBackedGho = GHO_GSM_4626_FIXED_PRICE_STRATEGY.getAssetPriceInGho( GHO_GSM_4626.getAvailableLiquidity(), false ); (, uint256 totalMintedGho) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); uint256 yieldInGho = totalBackedGho - totalMintedGho; assertEq(yieldInGho, DEFAULT_GSM_GHO_AMOUNT); // Sell asset accrues only the swap fee grossAmount = DEFAULT_GSM_GHO_AMOUNT * 2; // taking exchange rate into account sellFee = grossAmount.percentMul(DEFAULT_GSM_SELL_FEE); _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); ongoingAccruedFees += sellFee; assertEq(GHO_GSM_4626.getAccruedFees(), ongoingAccruedFees, 'Unexpected GSM accrued fees'); // Buy asset accrues only the swap fee grossAmount = DEFAULT_GSM_GHO_AMOUNT * 2; // taking exchange rate into account uint256 buyFee = grossAmount.percentMul(DEFAULT_GSM_BUY_FEE); ghoFaucet(BOB, grossAmount + buyFee); vm.startPrank(BOB); GHO_TOKEN.approve(address(GHO_GSM_4626), grossAmount + buyFee); GHO_GSM_4626.buyAsset(DEFAULT_GSM_USDC_AMOUNT, BOB); vm.stopPrank(); ongoingAccruedFees += buyFee + yieldInGho; assertEq(GHO_GSM_4626.getAccruedFees(), ongoingAccruedFees, 'Unexpected GSM accrued fees'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), ongoingAccruedFees, 'Unexpected GSM GHO balance' ); // Fee distribution uint256 treasuryBalanceBefore = GHO_TOKEN.balanceOf(address(TREASURY)); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit FeesDistributedToTreasury(TREASURY, address(GHO_TOKEN), ongoingAccruedFees); GHO_GSM_4626.distributeFeesToTreasury(); assertEq(GHO_GSM_4626.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), 0, 'Unexpected GSM GHO balance'); assertEq( GHO_TOKEN.balanceOf(address(TREASURY)) - treasuryBalanceBefore, ongoingAccruedFees, 'Unexpected Treasury GHO balance' ); } function testGetAccruedFeesWithHighExchangeRateAndMaxedOutCapacity() public { /** * 1. Alice sellAsset with 1:1 exchangeRate * 2. ExchangeRate increases, so there is an excess of backing * 3. Accrued fees does not factor in new yield in form of GHO * 4. A new sellAsset does not accrue fees from yield (only the swap fee) * 5. Bucket capacity is set to 0, so yield in form of GHO cannot be minted * 6. The distribution of fees does not accrue fees from yield in form of GHO */ uint256 ongoingAccruedFees = 0; uint256 grossAmount = DEFAULT_GSM_GHO_AMOUNT; uint256 sellFee = grossAmount.percentMul(DEFAULT_GSM_SELL_FEE); uint256 ghoOut = grossAmount - sellFee; _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); ongoingAccruedFees += sellFee; assertEq(GHO_GSM_4626.getAccruedFees(), ongoingAccruedFees, 'Unexpected GSM accrued fees'); assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), ghoOut, 'Unexpected final GHO balance'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), ongoingAccruedFees, 'Unexpected GSM GHO balance' ); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available underlying exposure' ); assertEq( GHO_GSM_4626.getAvailableLiquidity(), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected available liquidity' ); // Inflate exchange rate _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_AMOUNT, true); // Accrued dees does not change assertEq(GHO_GSM_4626.getAccruedFees(), ongoingAccruedFees, 'Unexpected GSM accrued fees'); assertEq( GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), ongoingAccruedFees, 'Unexpected GSM GHO balance' ); // Yield in form of GHO, not accrued yet uint256 totalBackedGho = GHO_GSM_4626_FIXED_PRICE_STRATEGY.getAssetPriceInGho( GHO_GSM_4626.getAvailableLiquidity(), false ); (, uint256 totalMintedGho) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); uint256 yieldInGho = totalBackedGho - totalMintedGho; assertEq(yieldInGho, DEFAULT_GSM_GHO_AMOUNT); // Sell asset accrues only the swap fee grossAmount = DEFAULT_GSM_GHO_AMOUNT * 2; // taking exchange rate into account sellFee = grossAmount.percentMul(DEFAULT_GSM_SELL_FEE); _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_AMOUNT); ongoingAccruedFees += sellFee; assertEq(GHO_GSM_4626.getAccruedFees(), ongoingAccruedFees, 'Unexpected GSM accrued fees'); // Bucket capacity of GSM set to 0 so no more GHO can be minted (including yield in form of GHO) GHO_TOKEN.setFacilitatorBucketCapacity(address(GHO_GSM_4626), 0); // Fee distribution uint256 treasuryBalanceBefore = GHO_TOKEN.balanceOf(address(TREASURY)); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit FeesDistributedToTreasury(TREASURY, address(GHO_TOKEN), ongoingAccruedFees); GHO_GSM_4626.distributeFeesToTreasury(); assertEq(GHO_GSM_4626.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); assertEq(GHO_TOKEN.balanceOf(address(GHO_GSM_4626)), 0, 'Unexpected GSM GHO balance'); assertEq( GHO_TOKEN.balanceOf(address(TREASURY)) - treasuryBalanceBefore, ongoingAccruedFees, 'Unexpected Treasury GHO balance' ); } function testBuyAssetAfterHighExchangeRate() public { /** * 1. Alice sellAsset with 1:1 exchangeRate * 2. Exchange rate increases, there is an excess of underlying backing GHO * 3. Alice buyAsset of the current exposure. There is a mint of GHO before the action so the level is updated. */ (, uint256 ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(ghoLevel, 0); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), 0); assertEq(USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), 0); assertEq(GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE); assertEq(GHO_GSM_4626.getAvailableLiquidity(), 0); uint128 sellAssetAmount = DEFAULT_GSM_USDC_AMOUNT; uint256 calcGhoMinted = DEFAULT_GSM_GHO_AMOUNT; uint256 calcExposure = DEFAULT_GSM_USDC_AMOUNT; _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, sellAssetAmount); (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(ghoLevel, calcGhoMinted); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), sellAssetAmount); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - calcExposure ); assertEq(GHO_GSM_4626.getAvailableLiquidity(), calcExposure); assertEq( USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), sellAssetAmount ); // Inflate exchange rate _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, sellAssetAmount, true); assertEq(ghoLevel, calcGhoMinted); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), calcExposure); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - calcExposure ); assertEq(GHO_GSM_4626.getAvailableLiquidity(), calcExposure); assertEq( USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), sellAssetAmount * 2 ); // Top up Alice with GHO ghoFaucet(ALICE, 1_000_000e18); // Alice buy all assets and there is a mint of GHO backed by excess of underlying before the action vm.startPrank(ALICE); GHO_TOKEN.approve(address(GHO_GSM_4626), type(uint256).max); uint256 totalBackedGho = GHO_GSM_4626_FIXED_PRICE_STRATEGY.getAssetPriceInGho( GHO_GSM_4626.getAvailableLiquidity(), false ); (, uint256 totalMintedGho) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(totalBackedGho, totalMintedGho + DEFAULT_GSM_GHO_AMOUNT); calcGhoMinted = 0; calcExposure = 0; GHO_GSM_4626.buyAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(ghoLevel, calcGhoMinted); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), calcExposure); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - calcExposure ); assertEq(GHO_GSM_4626.getAvailableLiquidity(), calcExposure); assertEq(USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), 0); } function testBuyAssetAfterLowExchangeRate() public { /** * 1. Alice sellAsset with 1:1 exchangeRate * 2. Exchange rate decreases, there is a portion of GHO unbacked * 3. Alice buyAsset of the current exposure * 4. Exposure is 0 but level is not 0, so there is unbacked GHO */ (, uint256 ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(ghoLevel, 0); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), 0); assertEq(USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), 0); assertEq(GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE); assertEq(GHO_GSM_4626.getAvailableLiquidity(), 0); uint128 sellAssetAmount = DEFAULT_GSM_USDC_AMOUNT; uint256 calcGhoMinted = DEFAULT_GSM_GHO_AMOUNT; uint256 calcExposure = DEFAULT_GSM_USDC_AMOUNT; _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, sellAssetAmount); (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(ghoLevel, calcGhoMinted); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), sellAssetAmount); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - calcExposure ); assertEq(GHO_GSM_4626.getAvailableLiquidity(), calcExposure); assertEq( USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), sellAssetAmount ); // Deflate exchange rate _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_AMOUNT / 2, false); assertEq(ghoLevel, calcGhoMinted); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), calcExposure); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - calcExposure ); assertEq(GHO_GSM_4626.getAvailableLiquidity(), calcExposure); assertEq( USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), sellAssetAmount / 2 ); // Top up Alice with GHO ghoFaucet(ALICE, 1_000_000e18); // Buy all assets vm.startPrank(ALICE); calcGhoMinted = DEFAULT_GSM_GHO_AMOUNT / 2; calcExposure = 0; GHO_TOKEN.approve(address(GHO_GSM_4626), type(uint256).max); GHO_GSM_4626.buyAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); // 0 exposure, but non-zero level (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertTrue(ghoLevel != 0); assertEq(ghoLevel, calcGhoMinted); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), calcExposure); assertEq( GHO_GSM_4626.getAvailableUnderlyingExposure(), DEFAULT_GSM_USDC_EXPOSURE - calcExposure ); assertEq(GHO_GSM_4626.getAvailableLiquidity(), calcExposure); assertEq(USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), 0); } function testBuyAssetAtCapacityWithGain() public { /** * 1. Alice sellAsset with 1:1 exchangeRate, up to the maximum exposure * 2. Exchange rate increases, there is an excess of underlying backing GHO * 3. Alice buyAsset of the maximum exposure, but excess is not minted due to maximum exposure maxed out * 4. Excess is minted once a buyAsset occurs and the maximum is not maxed out */ // Use zero fees for easier calculations vm.expectEmit(true, true, false, true, address(GHO_GSM_4626)); emit FeeStrategyUpdated(address(GHO_GSM_FIXED_FEE_STRATEGY), address(0)); GHO_GSM_4626.updateFeeStrategy(address(0)); // Supply assets to the GSM first _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_EXPOSURE); vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_USDC_EXPOSURE); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_EXPOSURE, DEFAULT_CAPACITY, 0); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_EXPOSURE, ALICE); vm.stopPrank(); (uint256 ghoCapacity, uint256 ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(ghoLevel, ghoCapacity, 'Unexpected GHO bucket level after initial sell'); // Simulate a gain _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_EXPOSURE / 4, true); (uint256 excess, uint256 deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, (DEFAULT_GSM_USDC_EXPOSURE / 4) * 1e12, 'Unexpected excess'); assertEq(deficit, 0, 'Unexpected non-zero deficit'); uint128 buyAmount = DEFAULT_CAPACITY / (((5 * DEFAULT_GSM_USDC_EXPOSURE) / 4) / 100); vm.startPrank(ALICE); GHO_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_CAPACITY); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit BuyAsset(ALICE, ALICE, buyAmount, DEFAULT_CAPACITY, 0); GHO_GSM_4626.buyAsset(buyAmount, ALICE); vm.stopPrank(); assertEq(USDC_4626_TOKEN.balanceOf(ALICE), buyAmount, 'Unexpected final USDC balance'); assertEq(GHO_TOKEN.balanceOf(ALICE), 0, 'Unexpected final GHO balance'); // Ensure GHO level is at 0, but that excess is unchanged (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); (ghoCapacity, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(ghoLevel, 0, 'Unexpected GHO bucket level after initial sell'); (excess, deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, (DEFAULT_GSM_USDC_EXPOSURE / 4) * 1e12, 'Unexpected excess'); assertEq(deficit, 0, 'Unexpected non-zero deficit'); // Sell a bit of asset so its possible to buy vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), 2); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); // Expected amount is a result of a 25% gain on 2 of the underlying getting rounded down emit SellAsset(ALICE, ALICE, 2, 2e12, 0); GHO_GSM_4626.sellAsset(2, ALICE); vm.stopPrank(); // Ensure GHO level is at 2e12, but that excess is unchanged (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq(ghoLevel, 2e12, 'Unexpected GHO bucket level after initial sell'); (excess, deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, (DEFAULT_GSM_USDC_EXPOSURE / 4) * 1e12, 'Unexpected excess'); assertEq(deficit, 0, 'Unexpected non-zero deficit'); // Buy a bit of asset so the excess is minted vm.startPrank(ALICE); GHO_TOKEN.approve(address(GHO_GSM_4626), 2e12); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit BuyAsset(ALICE, ALICE, 1, 2e12, 0); GHO_GSM_4626.buyAsset(1, ALICE); vm.stopPrank(); // Ensure GHO level is at the previous amount of excess, and excess is now 1e12 (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); assertEq( ghoLevel, (DEFAULT_GSM_USDC_EXPOSURE / 4) * 1e12, 'Unexpected GHO bucket level after final buy' ); (excess, deficit) = GHO_GSM_4626.getCurrentBacking(); // Excess of 1e12 due to the last purchase (rounding is causing excess on every sell-buy) assertEq(excess, 1e12, 'Unexpected excess'); assertEq(deficit, 0, 'Unexpected non-zero deficit'); } function testExcessBuildUpDueToUnbalanced4626() public { /** * 1. Vault gets unbalanced, 1 share equals 1.25 assets * 2. Alice sells 2 assets for 2e12 GHO * 3. Alice buys 1 asset for 2e12 GHO * 4. GSM gets 1 asset due to the imprecision error caused by math and unbalance vault */ // Use zero fees for easier calculations vm.expectEmit(true, true, false, true, address(GHO_GSM_4626)); emit FeeStrategyUpdated(address(GHO_GSM_FIXED_FEE_STRATEGY), address(0)); GHO_GSM_4626.updateFeeStrategy(address(0)); // Mint some vault shares first _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_EXPOSURE); // Simulate imbalance in vault (e.g. gift made to the vault, yield accumulation) _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_EXPOSURE / 4, true); // Sell 2 assets for 2e12 GHO vm.startPrank(ALICE); USDC_4626_TOKEN.approve(address(GHO_GSM_4626), 2); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); // Expected amount is a result of a 25% gain on 2 of the underlying getting rounded down emit SellAsset(ALICE, ALICE, 2, 2e12, 0); GHO_GSM_4626.sellAsset(2, ALICE); vm.stopPrank(); // Buy 1 asset for 2e12 GHO vm.startPrank(ALICE); GHO_TOKEN.approve(address(GHO_GSM_4626), 2e12); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); emit BuyAsset(ALICE, ALICE, 1, 2e12, 0); GHO_GSM_4626.buyAsset(1, ALICE); vm.stopPrank(); (uint256 excess, uint256 deficit) = GHO_GSM_4626.getCurrentBacking(); // Excess of 1e12 due to the last purchase (rounding is causing excess on every sell-buy) assertEq(excess, 1e12, 'Unexpected excess'); assertEq(deficit, 0, 'Unexpected non-zero deficit'); } } ================================================ FILE: src/test/TestGsmFixedFeeStrategy.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGsmFixedFeeStrategy is TestGhoBase { function testRevertAboveMaximumFee() public { vm.expectRevert('INVALID_BUY_FEE'); FixedFeeStrategy feeStrategy = new FixedFeeStrategy(5000, DEFAULT_GSM_SELL_FEE); vm.expectRevert('INVALID_SELL_FEE'); feeStrategy = new FixedFeeStrategy(DEFAULT_GSM_BUY_FEE, 5000); } function testZeroBuyFee() public { FixedFeeStrategy feeStrategy = new FixedFeeStrategy(0, DEFAULT_GSM_SELL_FEE); uint256 fee = feeStrategy.getBuyFee(DEFAULT_GSM_GHO_AMOUNT); assertEq(fee, 0, 'Unexpected non-zero fee'); assertEq( feeStrategy.getGrossAmountFromTotalBought(DEFAULT_GSM_GHO_AMOUNT + fee), DEFAULT_GSM_GHO_AMOUNT ); } function testZeroSellFee() public { FixedFeeStrategy feeStrategy = new FixedFeeStrategy(DEFAULT_GSM_BUY_FEE, 0); uint256 fee = feeStrategy.getSellFee(DEFAULT_GSM_GHO_AMOUNT); assertEq(fee, 0, 'Unexpected non-zero fee'); assertEq( feeStrategy.getGrossAmountFromTotalSold(DEFAULT_GSM_GHO_AMOUNT + fee), DEFAULT_GSM_GHO_AMOUNT ); } function testRevertBothFeesZero() public { vm.expectRevert('MUST_HAVE_ONE_NONZERO_FEE'); new FixedFeeStrategy(0, 0); } } ================================================ FILE: src/test/TestGsmFixedPriceStrategy.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGsmFixedPriceStrategy is TestGhoBase { function testConstructor(uint256 ratio, address underlying, uint8 decimals) public { vm.assume(ratio > 0); decimals = uint8(bound(decimals, 0, 40)); FixedPriceStrategy strategy = new FixedPriceStrategy(ratio, underlying, decimals); assertEq(strategy.GHO_DECIMALS(), 18, 'Unexpected GHO decimals'); assertEq(strategy.PRICE_RATIO(), ratio, 'Unexpected price ratio'); assertEq(strategy.UNDERLYING_ASSET(), underlying, 'Unexpected underlying asset'); assertEq( strategy.UNDERLYING_ASSET_DECIMALS(), decimals, 'Unexpected underlying asset decimals' ); } function testOneToOnePriceRatio() public { FixedPriceStrategy strategy = new FixedPriceStrategy(1e18, address(USDC_TOKEN), 6); uint256 usdcIn = 100e6; uint256 ghoOut = 100e18; assertEq(strategy.getAssetPriceInGho(usdcIn, true), ghoOut, 'Unexpected asset price in GHO'); assertEq(strategy.getGhoPriceInAsset(ghoOut, false), usdcIn, 'Unexpected gho price in asset'); } function testOneToTwoPriceRatio() public { FixedPriceStrategy strategy = new FixedPriceStrategy(2e18, address(USDC_TOKEN), 6); uint256 usdcIn = 100e6; uint256 ghoOut = 200e18; assertEq(strategy.getAssetPriceInGho(usdcIn, true), ghoOut, 'Unexpected asset price in GHO'); assertEq(strategy.getGhoPriceInAsset(ghoOut, false), usdcIn, 'Unexpected gho price in asset'); } function testTwoToOnePriceRatio() public { FixedPriceStrategy strategy = new FixedPriceStrategy(0.5e18, address(USDC_TOKEN), 6); uint256 usdcIn = 100e6; uint256 ghoOut = 50e18; assertEq(strategy.getAssetPriceInGho(usdcIn, true), ghoOut, 'Unexpected asset price in GHO'); assertEq(strategy.getGhoPriceInAsset(ghoOut, false), usdcIn, 'Unexpected gho price in asset'); } function testRevertZeroPriceRatio() public { vm.expectRevert('INVALID_PRICE_RATIO'); new FixedPriceStrategy(0, address(USDC_TOKEN), 6); } function testFuzzingExchangeRate( uint256 ratio, address underlying, uint8 decimals, uint256 amount ) public { decimals = uint8(bound(decimals, 1, 40)); ratio = bound(ratio, 1, type(uint128).max - 1); amount = bound(amount, 0, type(uint128).max - 1); FixedPriceStrategy strategy = new FixedPriceStrategy(ratio, underlying, decimals); uint256 amountInGho = (amount * ratio) / (10 ** decimals); assertEq( strategy.getAssetPriceInGho(amount, false), amountInGho, 'Unexpected asset price in GHO' ); } } ================================================ FILE: src/test/TestGsmFixedPriceStrategy4626.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGsmFixedPriceStrategy4626 is TestGhoBase { function testConstructor(uint256 ratio, address underlying, uint8 decimals) public { vm.assume(ratio > 0); decimals = uint8(bound(decimals, 0, 40)); FixedPriceStrategy4626 strategy = new FixedPriceStrategy4626(ratio, underlying, decimals); assertEq(strategy.GHO_DECIMALS(), 18, 'Unexpected GHO decimals'); assertEq(strategy.PRICE_RATIO(), ratio, 'Unexpected price ratio'); assertEq(strategy.UNDERLYING_ASSET(), underlying, 'Unexpected underlying asset'); assertEq( strategy.UNDERLYING_ASSET_DECIMALS(), decimals, 'Unexpected underlying asset decimals' ); } function testOneToOnePriceRatio() public { FixedPriceStrategy4626 strategy = new FixedPriceStrategy4626(1e18, address(USDC_4626_TOKEN), 6); uint256 usdcIn = 100e6; uint256 ghoOut = 100e18; assertEq(strategy.getAssetPriceInGho(usdcIn, true), ghoOut, 'Unexpected asset price in GHO'); assertEq(strategy.getGhoPriceInAsset(ghoOut, false), usdcIn, 'Unexpected gho price in asset'); } function testOneToTwoPriceRatio() public { FixedPriceStrategy4626 strategy = new FixedPriceStrategy4626(2e18, address(USDC_4626_TOKEN), 6); uint256 usdcIn = 100e6; uint256 ghoOut = 200e18; assertEq(strategy.getAssetPriceInGho(usdcIn, true), ghoOut, 'Unexpected asset price in GHO'); assertEq(strategy.getGhoPriceInAsset(ghoOut, false), usdcIn, 'Unexpected gho price in asset'); } function testTwoToOnePriceRatio() public { FixedPriceStrategy4626 strategy = new FixedPriceStrategy4626( 0.5e18, address(USDC_4626_TOKEN), 6 ); uint256 usdcIn = 100e6; uint256 ghoOut = 50e18; assertEq(strategy.getAssetPriceInGho(usdcIn, true), ghoOut, 'Unexpected asset price in GHO'); assertEq(strategy.getGhoPriceInAsset(ghoOut, false), usdcIn, 'Unexpected gho price in asset'); } function testRevertZeroPriceRatio() public { vm.expectRevert('INVALID_PRICE_RATIO'); new FixedPriceStrategy4626(0, address(USDC_4626_TOKEN), 6); } function testPriceFeedHighExchangeRate() public { FixedPriceStrategy4626 strategy = new FixedPriceStrategy4626(1e18, address(USDC_4626_TOKEN), 6); _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, 100e6); // Inflate exchange rate to 2 _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, 100e6, true); assertEq(strategy.getAssetPriceInGho(50e6, true), 100e18); assertEq(strategy.getGhoPriceInAsset(100e18, true), 50e6); } function testPriceFeedLowExchangeRate() public { FixedPriceStrategy4626 strategy = new FixedPriceStrategy4626(1e18, address(USDC_4626_TOKEN), 6); _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, 100e6); // Deflate exchange rate to 1/2 _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, 50e6, false); assertEq(strategy.getAssetPriceInGho(200e6, true), 100e18); assertEq(strategy.getGhoPriceInAsset(100e18, true), 200e6); } function testFuzzingExchangeRate(uint256 ratio, uint8 decimals, uint256 amount) public { decimals = uint8(bound(decimals, 1, 40)); ratio = bound(ratio, 1, type(uint128).max - 1); amount = bound(amount, 0, type(uint128).max - 1); FixedPriceStrategy4626 strategy = new FixedPriceStrategy4626( ratio, address(USDC_4626_TOKEN), decimals ); uint256 amountInGho = (amount * ratio) / (10 ** decimals); assertEq( strategy.getAssetPriceInGho(amount, false), amountInGho, 'Unexpected asset price in GHO' ); } } ================================================ FILE: src/test/TestGsmOracleSwapFreezer.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; import {OracleSwapFreezer} from '../contracts/facilitators/gsm/swapFreezer/OracleSwapFreezer.sol'; contract TestGsmOracleSwapFreezer is TestGhoBase { OracleSwapFreezer swapFreezer; uint128 constant DEFAULT_FREEZE_LOWER_BOUND = 0.97e8; uint128 constant DEFAULT_FREEZE_UPPER_BOUND = 1.03e8; uint128 constant DEFAULT_UNFREEZE_LOWER_BOUND = 0.99e8; uint128 constant DEFAULT_UNFREEZE_UPPER_BOUND = 1.01e8; function setUp() public { PRICE_ORACLE.setAssetPrice(address(USDC_TOKEN), 1e8); swapFreezer = new OracleSwapFreezer( GHO_GSM, address(USDC_TOKEN), IPoolAddressesProvider(address(PROVIDER)), DEFAULT_FREEZE_LOWER_BOUND, DEFAULT_FREEZE_UPPER_BOUND, DEFAULT_UNFREEZE_LOWER_BOUND, DEFAULT_UNFREEZE_UPPER_BOUND, true ); GHO_GSM.grantRole(GSM_SWAP_FREEZER_ROLE, address(swapFreezer)); } function testRevertConstructorInvalidZeroAddress() public { vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); new OracleSwapFreezer( GHO_GSM, address(0), IPoolAddressesProvider(address(PROVIDER)), DEFAULT_FREEZE_LOWER_BOUND, DEFAULT_FREEZE_UPPER_BOUND, DEFAULT_UNFREEZE_LOWER_BOUND, DEFAULT_UNFREEZE_UPPER_BOUND, true ); } function testConstructorInvalidUnfreezeWhileFreezeNotAllowed() public { uint128 unfreezeLowerBound = 1; uint128 unfreezeUpperBound = type(uint128).max; // Ensure bound check fails if allowing unfreezing, as expected vm.expectRevert('BOUNDS_NOT_VALID'); new OracleSwapFreezer( GHO_GSM, address(USDC_TOKEN), IPoolAddressesProvider(address(PROVIDER)), DEFAULT_FREEZE_LOWER_BOUND, DEFAULT_FREEZE_UPPER_BOUND, unfreezeLowerBound, unfreezeUpperBound, true ); // Revert expected when non-zero unfreeze lower bound unfreezeUpperBound = 0; vm.expectRevert('BOUNDS_NOT_VALID'); new OracleSwapFreezer( GHO_GSM, address(USDC_TOKEN), IPoolAddressesProvider(address(PROVIDER)), DEFAULT_FREEZE_LOWER_BOUND, DEFAULT_FREEZE_UPPER_BOUND, unfreezeLowerBound, unfreezeUpperBound, false ); // Revert expected when non-zero unfreeze upper bound unfreezeLowerBound = 0; unfreezeUpperBound = type(uint128).max; vm.expectRevert('BOUNDS_NOT_VALID'); new OracleSwapFreezer( GHO_GSM, address(USDC_TOKEN), IPoolAddressesProvider(address(PROVIDER)), DEFAULT_FREEZE_LOWER_BOUND, DEFAULT_FREEZE_UPPER_BOUND, unfreezeLowerBound, unfreezeUpperBound, false ); // No revert expected with 0 unfreeze lower/upper bound unfreezeLowerBound = 0; unfreezeUpperBound = 0; new OracleSwapFreezer( GHO_GSM, address(USDC_TOKEN), IPoolAddressesProvider(address(PROVIDER)), DEFAULT_FREEZE_LOWER_BOUND, DEFAULT_FREEZE_UPPER_BOUND, unfreezeLowerBound, unfreezeUpperBound, false ); } function testRevertConstructorInvalidBounds() public { // Case 1: Freeze upper bound less than or equal to lower bound uint128 freezeLowerBound = DEFAULT_FREEZE_LOWER_BOUND; uint128 freezeUpperBound = DEFAULT_FREEZE_LOWER_BOUND; vm.expectRevert('BOUNDS_NOT_VALID'); new OracleSwapFreezer( GHO_GSM, address(USDC_TOKEN), IPoolAddressesProvider(address(PROVIDER)), freezeLowerBound, freezeUpperBound, DEFAULT_UNFREEZE_LOWER_BOUND, DEFAULT_UNFREEZE_UPPER_BOUND, true ); // Case 2: Unfreeze upper bound less than or equal to lower bound uint128 unfreezeLowerBound = DEFAULT_UNFREEZE_UPPER_BOUND; uint128 unfreezeUpperBound = DEFAULT_UNFREEZE_UPPER_BOUND; vm.expectRevert('BOUNDS_NOT_VALID'); new OracleSwapFreezer( GHO_GSM, address(USDC_TOKEN), IPoolAddressesProvider(address(PROVIDER)), DEFAULT_FREEZE_LOWER_BOUND, DEFAULT_FREEZE_UPPER_BOUND, unfreezeLowerBound, unfreezeUpperBound, true ); // Case 3: Freeze lower bound is greater than or equal to unfreeze lower bound freezeLowerBound = DEFAULT_UNFREEZE_LOWER_BOUND; freezeUpperBound = DEFAULT_FREEZE_UPPER_BOUND; vm.expectRevert('BOUNDS_NOT_VALID'); new OracleSwapFreezer( GHO_GSM, address(USDC_TOKEN), IPoolAddressesProvider(address(PROVIDER)), freezeLowerBound, freezeUpperBound, DEFAULT_UNFREEZE_LOWER_BOUND, DEFAULT_UNFREEZE_UPPER_BOUND, true ); // Case 4: Unfreeze upper bound is greater than or equal to freeze upper bound unfreezeLowerBound = DEFAULT_UNFREEZE_LOWER_BOUND; unfreezeUpperBound = DEFAULT_FREEZE_UPPER_BOUND; vm.expectRevert('BOUNDS_NOT_VALID'); new OracleSwapFreezer( GHO_GSM, address(USDC_TOKEN), IPoolAddressesProvider(address(PROVIDER)), DEFAULT_FREEZE_LOWER_BOUND, DEFAULT_FREEZE_UPPER_BOUND, unfreezeLowerBound, unfreezeUpperBound, true ); } function testCheckUpkeepCanFreeze() public { (bool canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, false, 'Unexpected initial upkeep state'); PRICE_ORACLE.setAssetPrice(address(USDC_TOKEN), DEFAULT_FREEZE_LOWER_BOUND); (canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, true, 'Unexpected upkeep state after price == freeze lower bound'); assertLt(1, DEFAULT_FREEZE_LOWER_BOUND, '1 not less than freeze lower bound'); PRICE_ORACLE.setAssetPrice(address(USDC_TOKEN), 1); (canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, true, 'Unexpected upkeep state after price < freeze lower bound'); PRICE_ORACLE.setAssetPrice(address(USDC_TOKEN), DEFAULT_FREEZE_UPPER_BOUND); (canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, true, 'Unexpected upkeep state after price == freeze upper bound'); assertGt( type(uint128).max, DEFAULT_FREEZE_UPPER_BOUND, 'uint128.max not greater than freeze upper bound' ); PRICE_ORACLE.setAssetPrice(address(USDC_TOKEN), type(uint128).max); (canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, true, 'Unexpected upkeep state after price > freeze upper bound'); } function testCheckUpkeepCannotFreezeWhenOracleZero() public { (bool canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, false, 'Unexpected initial upkeep state'); assertLt(0, DEFAULT_FREEZE_LOWER_BOUND, '0 not less than freeze lower bound'); PRICE_ORACLE.setAssetPrice(address(USDC_TOKEN), 0); (canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, false, 'Unexpected upkeep state when oracle price is zero'); } function testCheckUpkeepCanUnfreeze() public { // Freeze the GSM and set the asset price to 1 wei vm.prank(address(GHO_GSM_SWAP_FREEZER)); vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit SwapFreeze(address(GHO_GSM_SWAP_FREEZER), true); GHO_GSM.setSwapFreeze(true); PRICE_ORACLE.setAssetPrice(address(USDC_TOKEN), 1); (bool canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, false, 'Unexpected initial upkeep state'); PRICE_ORACLE.setAssetPrice(address(USDC_TOKEN), DEFAULT_UNFREEZE_LOWER_BOUND); (canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, true, 'Unexpected upkeep state after price >= unfreeze lower bound'); PRICE_ORACLE.setAssetPrice(address(USDC_TOKEN), DEFAULT_UNFREEZE_UPPER_BOUND); (canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, true, 'Unexpected upkeep state after price <= unfreeze upper bound'); PRICE_ORACLE.setAssetPrice( address(USDC_TOKEN), (DEFAULT_UNFREEZE_LOWER_BOUND + DEFAULT_UNFREEZE_UPPER_BOUND) / 2 ); (canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, true, 'Unexpected upkeep state after price in unfreeze bound range'); } function testCheckUpkeepCannotUnfreeze() public { OracleSwapFreezer swapFreezerWithoutUnfreeze = new OracleSwapFreezer( GHO_GSM, address(USDC_TOKEN), IPoolAddressesProvider(address(PROVIDER)), DEFAULT_FREEZE_LOWER_BOUND, DEFAULT_FREEZE_UPPER_BOUND, 0, 0, false ); // Freeze the GSM vm.prank(address(GHO_GSM_SWAP_FREEZER)); vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit SwapFreeze(address(GHO_GSM_SWAP_FREEZER), true); GHO_GSM.setSwapFreeze(true); (bool canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, true, 'Unexpected upkeep state for default freezer'); (canPerformUpkeep, ) = swapFreezerWithoutUnfreeze.checkUpkeep(''); assertEq(canPerformUpkeep, false, 'Unexpected upkeep state for no-unfreeze freezer'); } function testCheckUpkeepCannotUnfreezeWhenSeized() public { // Set oracle price to a value allowing a freeze PRICE_ORACLE.setAssetPrice(address(USDC_TOKEN), DEFAULT_FREEZE_LOWER_BOUND); (bool canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, true, 'Unexpected initial upkeep state for default freezer'); // Seize the GSM vm.prank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit Seized(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), TREASURY, 0, 0); GHO_GSM.seize(); (canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, false, 'Unexpected upkeep state post-seize'); } function testPerformUpkeepCanFreeze() public { (bool canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, false, 'Unexpected initial upkeep state'); assertEq(GHO_GSM.getIsFrozen(), false, 'Unexpected initial freeze state for GSM'); PRICE_ORACLE.setAssetPrice(address(USDC_TOKEN), DEFAULT_FREEZE_LOWER_BOUND); vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit SwapFreeze(address(swapFreezer), true); swapFreezer.performUpkeep(''); assertEq(GHO_GSM.getIsFrozen(), true, 'Unexpected final freeze state for GSM'); } function testPerformUpkeepCanUnfreeze() public { // Freeze the GSM and set price to 1 wei vm.prank(address(GHO_GSM_SWAP_FREEZER)); vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit SwapFreeze(address(GHO_GSM_SWAP_FREEZER), true); GHO_GSM.setSwapFreeze(true); PRICE_ORACLE.setAssetPrice(address(USDC_TOKEN), 1); (bool canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, false, 'Unexpected initial upkeep state'); assertEq(GHO_GSM.getIsFrozen(), true, 'Unexpected initial freeze state for GSM'); PRICE_ORACLE.setAssetPrice(address(USDC_TOKEN), DEFAULT_UNFREEZE_LOWER_BOUND); vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit SwapFreeze(address(swapFreezer), false); swapFreezer.performUpkeep(''); assertEq(GHO_GSM.getIsFrozen(), false, 'Unexpected final freeze state for GSM'); } function testCheckUpkeepNoSwapFreezeRole() public { // Move price outside freeze range PRICE_ORACLE.setAssetPrice(address(USDC_TOKEN), DEFAULT_FREEZE_LOWER_BOUND - 1); (bool canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, true, 'Unexpected initial upkeep state'); // Revoke SwapFreezer role GHO_GSM.revokeRole(GSM_SWAP_FREEZER_ROLE, address(swapFreezer)); // Upkeep shouldn't be possible (canPerformUpkeep, ) = swapFreezer.checkUpkeep(''); assertEq(canPerformUpkeep, false, 'Unexpected upkeep state'); // Do not revert, it's a no-op execution swapFreezer.performUpkeep(''); } function testGetCanUnfreeze() public { assertEq(swapFreezer.getCanUnfreeze(), true, 'Unexpected initial unfreeze state'); swapFreezer = new OracleSwapFreezer( GHO_GSM, address(USDC_TOKEN), IPoolAddressesProvider(address(PROVIDER)), DEFAULT_FREEZE_LOWER_BOUND, DEFAULT_FREEZE_UPPER_BOUND, 0, 0, false ); assertEq(swapFreezer.getCanUnfreeze(), false, 'Unexpected final unfreeze state'); } function testFuzzUpkeepConsistency(uint256 assetPrice, bool grantRole) public { PRICE_ORACLE.setAssetPrice(address(USDC_TOKEN), assetPrice); OracleSwapFreezer agent = new OracleSwapFreezer( GHO_GSM, address(USDC_TOKEN), IPoolAddressesProvider(address(PROVIDER)), DEFAULT_FREEZE_LOWER_BOUND, DEFAULT_FREEZE_UPPER_BOUND, DEFAULT_UNFREEZE_LOWER_BOUND, DEFAULT_UNFREEZE_UPPER_BOUND, true ); if (grantRole) { GHO_GSM.grantRole(GSM_SWAP_FREEZER_ROLE, address(agent)); } // If canPerformUpkeep, there must be a state change bool freezeState = GHO_GSM.getIsFrozen(); (bool canPerformUpkeep, ) = agent.checkUpkeep(''); agent.performUpkeep(''); if (canPerformUpkeep) { // state change assertEq(freezeState, !GHO_GSM.getIsFrozen(), 'no state change after performUpkeep'); } else { // no state change assertEq(freezeState, GHO_GSM.getIsFrozen(), 'state change after performUpkeep'); } } } ================================================ FILE: src/test/TestGsmRegistry.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGsmRegistry is TestGhoBase { function testConstructor(address newOwner) public { vm.assume(newOwner != address(this) && newOwner != address(0)); vm.expectEmit(true, true, false, true); emit OwnershipTransferred(address(0), address(this)); vm.expectEmit(true, true, false, true); emit OwnershipTransferred(address(this), newOwner); GsmRegistry registry = new GsmRegistry(newOwner); assertEq(registry.owner(), newOwner, 'Unexpected contract owner'); assertEq(registry.getGsmList().length, 0, 'Unexpected gsm list length'); assertEq(registry.getGsmListLength(), 0, 'Unexpected gsm list length'); } function testRevertConstructorZeroAddress() public { vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); new GsmRegistry(address(0)); } function testAddGsm(address newGsm) public { vm.assume(newGsm != address(0)); vm.expectEmit(true, false, false, true); emit GsmAdded(newGsm); GHO_GSM_REGISTRY.addGsm(newGsm); assertEq(GHO_GSM_REGISTRY.getGsmListLength(), 1, 'Unexpected gsm list length'); assertEq(GHO_GSM_REGISTRY.getGsmAtIndex(0), newGsm, 'Unexpected gsm registered'); } function testAddGsmMultiple(uint256 size) public { size = bound(size, 0, 20); for (uint256 i = 0; i < size; i++) { address newGsm = address(uint160(i + 123)); vm.expectEmit(true, false, false, true); emit GsmAdded(newGsm); GHO_GSM_REGISTRY.addGsm(newGsm); assertEq(GHO_GSM_REGISTRY.getGsmAtIndex(i), newGsm, 'Unexpected gsm registered'); } assertEq(GHO_GSM_REGISTRY.getGsmListLength(), size, 'Unexpected gsm list length'); } function testRevertAddGsmUnauthorized(address caller) public { vm.assume(caller != GHO_GSM_REGISTRY.owner()); vm.expectRevert(OwnableErrorsLib.CALLER_NOT_OWNER()); vm.prank(caller); GHO_GSM_REGISTRY.addGsm(address(123)); } function testRevertAddGsmInvalidAddress() public { vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); GHO_GSM_REGISTRY.addGsm(address(0)); assertEq(GHO_GSM_REGISTRY.getGsmListLength(), 0, 'Unexpected gsm list length'); } function testRevertAddSameGsmTwice(address newGsm) public { vm.assume(newGsm != address(0)); vm.expectEmit(true, false, false, true); emit GsmAdded(newGsm); GHO_GSM_REGISTRY.addGsm(newGsm); assertEq(GHO_GSM_REGISTRY.getGsmListLength(), 1, 'Unexpected gsm list length'); assertEq(GHO_GSM_REGISTRY.getGsmAtIndex(0), newGsm, 'Unexpected gsm registered'); vm.expectRevert('GSM_ALREADY_ADDED'); GHO_GSM_REGISTRY.addGsm(newGsm); assertEq(GHO_GSM_REGISTRY.getGsmListLength(), 1, 'Unexpected gsm list length'); assertEq(GHO_GSM_REGISTRY.getGsmAtIndex(0), newGsm, 'Unexpected gsm registered'); } function testRemoveGsm(address gsmToRemove) public { vm.assume(gsmToRemove != address(0)); uint256 sizeBefore = GHO_GSM_REGISTRY.getGsmListLength(); GHO_GSM_REGISTRY.addGsm(gsmToRemove); vm.expectEmit(true, false, false, true); emit GsmRemoved(gsmToRemove); GHO_GSM_REGISTRY.removeGsm(gsmToRemove); assertEq(GHO_GSM_REGISTRY.getGsmListLength(), sizeBefore, 'Unexpected gsm list length'); } function testRemoveGsmMultiple(uint256 size) public { size = bound(size, 0, 20); for (uint256 i = 0; i < size; i++) { address newGsm = address(uint160(i + 123)); GHO_GSM_REGISTRY.addGsm(newGsm); assertEq(GHO_GSM_REGISTRY.getGsmAtIndex(0), newGsm, 'Unexpected gsm registered'); vm.expectEmit(true, false, false, true); emit GsmRemoved(newGsm); GHO_GSM_REGISTRY.removeGsm(newGsm); } assertEq(GHO_GSM_REGISTRY.getGsmListLength(), 0, 'Unexpected gsm list length'); } function testRevertRemoveGsmUnauthorized(address caller) public { vm.assume(caller != GHO_GSM_REGISTRY.owner()); vm.expectRevert(OwnableErrorsLib.CALLER_NOT_OWNER()); vm.prank(caller); GHO_GSM_REGISTRY.removeGsm(address(123)); } function testRevertRemoveGsmInvalidAddress() public { vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); GHO_GSM_REGISTRY.removeGsm(address(0)); } function testRevertRemoveSameGsmTwice(address newGsm) public { vm.assume(newGsm != address(0)); GHO_GSM_REGISTRY.addGsm(newGsm); vm.expectEmit(true, false, false, true); emit GsmRemoved(newGsm); GHO_GSM_REGISTRY.removeGsm(newGsm); assertEq(GHO_GSM_REGISTRY.getGsmListLength(), 0, 'Unexpected gsm list length'); vm.expectRevert('NONEXISTENT_GSM'); GHO_GSM_REGISTRY.removeGsm(newGsm); assertEq(GHO_GSM_REGISTRY.getGsmListLength(), 0, 'Unexpected gsm list length'); } function testGetGsmList(uint256 sizeToAdd, uint256 sizeToRemove) public { sizeToAdd = bound(sizeToAdd, 1, 20); sizeToRemove = bound(sizeToRemove, 0, sizeToAdd - 1); address[] memory localGsmList = new address[](sizeToAdd); uint256 i; for (i = 0; i < sizeToAdd; i++) { address newGsm = address(uint160(i + 123)); localGsmList[i] = newGsm; GHO_GSM_REGISTRY.addGsm(newGsm); } for (i = 0; i < sizeToRemove; i++) { GHO_GSM_REGISTRY.removeGsm(localGsmList[sizeToAdd - i - 1]); } uint256 leftOvers = sizeToAdd - sizeToRemove; assertEq(leftOvers, GHO_GSM_REGISTRY.getGsmListLength()); address[] memory gsmList = GHO_GSM_REGISTRY.getGsmList(); for (i = 0; i < leftOvers; i++) { assertEq(gsmList[i], localGsmList[i], 'unexpected GSM address'); assertEq( GHO_GSM_REGISTRY.getGsmAtIndex(i), localGsmList[i], 'unexpected GSM address at given index' ); } } function testRevertGetGsmAtIndex() public { assertEq(GHO_GSM_REGISTRY.getGsmListLength(), 0, 'Unexpected gsm list length'); vm.expectRevert('INVALID_INDEX'); GHO_GSM_REGISTRY.getGsmAtIndex(0); address newGsm = address(0x123); GHO_GSM_REGISTRY.addGsm(newGsm); assertEq(GHO_GSM_REGISTRY.getGsmListLength(), 1, 'Unexpected gsm list length'); assertEq(GHO_GSM_REGISTRY.getGsmAtIndex(0), newGsm, 'Unexpected gsm address at index 0'); vm.expectRevert('INVALID_INDEX'); GHO_GSM_REGISTRY.getGsmAtIndex(1); } } ================================================ FILE: src/test/TestGsmSampleLiquidator.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGsmSampleLiquidator is TestGhoBase { function testSeize() public { vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit Seized(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), TREASURY, 0, 0); GHO_GSM_LAST_RESORT_LIQUIDATOR.triggerSeize(address(GHO_GSM)); } function testRevertSeizeNotAuthorized() public { vm.expectRevert(OwnableErrorsLib.CALLER_NOT_OWNER()); vm.prank(ALICE); GHO_GSM_LAST_RESORT_LIQUIDATOR.triggerSeize(address(GHO_GSM)); } function testRevertSeizeAlreadySeized() public { vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit Seized(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), TREASURY, 0, 0); GHO_GSM_LAST_RESORT_LIQUIDATOR.triggerSeize(address(GHO_GSM)); vm.expectRevert('GSM_SEIZED'); GHO_GSM_LAST_RESORT_LIQUIDATOR.triggerSeize(address(GHO_GSM)); } function testBurnAfterSeize() public { // Mint GHO in the GSM vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); // Seize the GSM uint256 seizedAmount = GHO_GSM_LAST_RESORT_LIQUIDATOR.triggerSeize(address(GHO_GSM)); assertEq(seizedAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected seize amount returned'); // Mint the current bucket level (, uint256 bucketLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM)); assertGt(bucketLevel, 0, 'Unexpected 0 minted GHO'); ghoFaucet(address(this), bucketLevel); GHO_TOKEN.approve(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), bucketLevel); vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit BurnAfterSeize(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), bucketLevel, 0); uint256 burnAmount = GHO_GSM_LAST_RESORT_LIQUIDATOR.triggerBurnAfterSeize( address(GHO_GSM), bucketLevel ); assertEq(burnAmount, bucketLevel, 'Unexpected burn amount returned'); } function testBurnMoreThanMintedAfterSeize() public { // Mint GHO in the GSM vm.prank(FAUCET); USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); vm.startPrank(ALICE); USDC_TOKEN.approve(address(GHO_GSM), DEFAULT_GSM_USDC_AMOUNT); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); // Seize the GSM uint256 seizedAmount = GHO_GSM_LAST_RESORT_LIQUIDATOR.triggerSeize(address(GHO_GSM)); assertEq(seizedAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected seize amount returned'); // Mint the current bucket level + 1, to have more GHO than necessary (, uint256 bucketLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM)); assertGt(bucketLevel, 0, 'Unexpected 0 minted GHO'); ghoFaucet(address(this), bucketLevel + 1); // Attempt to burn more than what was minted, leaving 1 GHO left-over and burning the bucketLevel GHO_TOKEN.approve(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), bucketLevel + 1); vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit BurnAfterSeize(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), bucketLevel, 0); uint256 burnAmount = GHO_GSM_LAST_RESORT_LIQUIDATOR.triggerBurnAfterSeize( address(GHO_GSM), bucketLevel + 1 ); assertEq(burnAmount, bucketLevel, 'Unexpected burn amount returned'); assertEq(GHO_TOKEN.balanceOf(address(this)), 1, 'Unexpected final GHO amount'); } } ================================================ FILE: src/test/TestGsmSampleSwapFreezer.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGsmSampleSwapFreezer is TestGhoBase { function testFreeze() public { vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit SwapFreeze(address(GHO_GSM_SWAP_FREEZER), true); GHO_GSM_SWAP_FREEZER.triggerFreeze(address(GHO_GSM)); } function testUnfreeze() public { GHO_GSM_SWAP_FREEZER.triggerFreeze(address(GHO_GSM)); vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit SwapFreeze(address(GHO_GSM_SWAP_FREEZER), false); GHO_GSM_SWAP_FREEZER.triggerUnfreeze(address(GHO_GSM)); } function testRevertNotAuthorized() public { vm.startPrank(ALICE); vm.expectRevert(OwnableErrorsLib.CALLER_NOT_OWNER()); GHO_GSM_SWAP_FREEZER.triggerFreeze(address(GHO_GSM)); vm.expectRevert(OwnableErrorsLib.CALLER_NOT_OWNER()); GHO_GSM_SWAP_FREEZER.triggerUnfreeze(address(GHO_GSM)); vm.stopPrank(); } function testRevertFreezeAlreadyFrozen() public { GHO_GSM_SWAP_FREEZER.triggerFreeze(address(GHO_GSM)); vm.expectRevert('GSM_ALREADY_FROZEN'); GHO_GSM_SWAP_FREEZER.triggerFreeze(address(GHO_GSM)); } function testRevertUnfreezeNotFrozen() public { vm.expectRevert('GSM_ALREADY_UNFROZEN'); GHO_GSM_SWAP_FREEZER.triggerUnfreeze(address(GHO_GSM)); } } ================================================ FILE: src/test/TestGsmSwapEdge.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGsmSwapEdge is TestGhoBase { using PercentageMath for uint256; using PercentageMath for uint128; /** * @dev Edge case where it is not possible to burn all GHO minted due to rounding issues. * e.g. With (1e16 + 1) priceRatio, a user gets 1e11 gho for selling 1 asset but gets 1 asset by selling 1e11+1 gho */ function testCannotBuyAllUnderlying() public { TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', 5, FAUCET); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( 10000000000000001, // 1e16 + 1 address(newToken), 5 ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(address(this), TREASURY, type(uint128).max); GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', type(uint128).max); // Sell 2 assets for 2e11 GHO vm.prank(FAUCET); newToken.mint(ALICE, 2); vm.startPrank(ALICE); newToken.approve(address(gsm), 2); gsm.sellAsset(2, ALICE); vm.stopPrank(); // Top up with GHO (, uint256 estGhoBought, , ) = gsm.getGhoAmountForBuyAsset(2); ghoFaucet(ALICE, estGhoBought * 20); vm.startPrank(ALICE); GHO_TOKEN.approve(address(gsm), type(uint256).max); // Try to buy all, which is 2 assets for 2e11+1 GHO uint256 allUnderlying = gsm.getAvailableLiquidity(); vm.expectRevert(); gsm.buyAsset(allUnderlying, ALICE); // Buy a portion uint256 userGhoBefore = GHO_TOKEN.balanceOf(ALICE); uint256 userAssetBefore = newToken.balanceOf(ALICE); (uint256 exactAssetAmount, uint256 exactGhoAmount) = gsm.buyAsset(1, ALICE); assertEq( newToken.balanceOf(ALICE) - userAssetBefore, exactAssetAmount, 'unexpected underlying balance of ALICE' ); assertEq( userGhoBefore - GHO_TOKEN.balanceOf(ALICE), exactGhoAmount, 'unexpected GHO balance of ALICE' ); assertTrue(gsm.getAvailableLiquidity() > 0, 'unexpected remaining liquidity'); vm.stopPrank(); } /** * @dev Edge case where it is not possible to sell less than the minimum amount because would lead to 0 GHO. */ function testGetAssetAmountForSellAssetBelowMinimumAmount() public { TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', 18, FAUCET); FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(4900, 4900); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( DEFAULT_FIXED_PRICE, address(newToken), 18 ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(address(this), TREASURY, 100_000_000e18); gsm.updateFeeStrategy(address(newFeeStrategy)); GHO_TOKEN.addFacilitator(address(gsm), 'Test GSM', DEFAULT_CAPACITY); // Get asset amount required to receive 1 GHO (uint256 assetAmount, uint256 ghoBought, uint256 grossAmount, uint256 fee) = gsm .getAssetAmountForSellAsset(1); assertEq(assetAmount, 2, 'Unexpected asset to sell'); assertEq(ghoBought, 1, 'Unexpected gho amount bought'); assertEq(grossAmount, 2, 'Unexpected gross amount'); assertEq(fee, 1, 'Unexpected fee'); // Using 1 wei less than the assetAmount will round down the asset amount to 0, so should revert vm.expectRevert('INVALID_AMOUNT'); gsm.sellAsset(assetAmount - 1, ALICE); _sellAsset(gsm, newToken, ALICE, assetAmount); assertEq(GHO_TOKEN.balanceOf(ALICE), 1, 'Unexpected GHO balance'); } /** * @dev Sell asset function meets the maximum amount to be sold and does not over-charge the user. * e.g. if the GSM can provide 10 GHO for 12 assets, it charges only 12 even if the user provides more than 12. */ function testSellAssetWithMinimumAmount() public { TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', 18, FAUCET); FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(3000, 3000); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( DEFAULT_FIXED_PRICE, address(newToken), 18 ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(address(this), TREASURY, 100_000_000e18); gsm.updateFeeStrategy(address(newFeeStrategy)); GHO_TOKEN.addFacilitator(address(gsm), 'Test GSM', DEFAULT_CAPACITY); // Get asset amount required to receive 10000 GHO (uint256 assetAmount, uint256 ghoBought, uint256 grossAmount, uint256 fee) = gsm .getAssetAmountForSellAsset(10000); assertEq(assetAmount, 14286, 'Unexpected asset to sell'); assertEq(ghoBought, 10000, 'Unexpected gho amount bought'); assertEq(grossAmount, 14286, 'Unexpected gross amount'); assertEq(fee, 4286, 'Unexpected fee'); assertEq(newToken.balanceOf(ALICE), 0, 'Unexpected asset amount before'); // Mint 1 more asset than required (14286) to receive 10000 GHO vm.prank(FAUCET); newToken.mint(ALICE, assetAmount + 1); vm.startPrank(ALICE); newToken.approve(address(gsm), assetAmount); // Sell 1 more asset than required to receive 10000 GHO gsm.sellAsset(assetAmount + 1, ALICE); vm.stopPrank(); assertEq(GHO_TOKEN.balanceOf(ALICE), 10000, 'Unexpected GHO balance'); // Should have 1 "leftover" asset, as sellAsset prevents "overpaying" so only assetAmount spent assertEq(newToken.balanceOf(ALICE), 1, 'Unexpected ending asset amount'); } /** * @dev Checks sellAsset does not charge more asset than needed * in case the underlying asset has more decimals than GHO (18) */ function testSellAssetWithHigherDecimals() public { TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', 24, FAUCET); vm.prank(FAUCET); newToken.mint(ALICE, 1_000_000e24); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( DEFAULT_FIXED_PRICE, // 1e18 address(newToken), 24 // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(ALICE, TREASURY, 1_000_000e24); GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', DEFAULT_CAPACITY); vm.startPrank(ALICE); newToken.approve(address(gsm), type(uint256).max); // Less than 1e6 results in 0 GHO vm.expectRevert('INVALID_AMOUNT'); gsm.sellAsset(0.9e6, ALICE); // Lowest amount that can be sold is 1e6 uint256 userGhoBefore = GHO_TOKEN.balanceOf(ALICE); uint256 userAssetBefore = newToken.balanceOf(ALICE); gsm.sellAsset(1e6, ALICE); assertEq(GHO_TOKEN.balanceOf(ALICE) - userGhoBefore, 1, 'unexpected amount of GHO purchased'); assertEq(userAssetBefore - newToken.balanceOf(ALICE), 1e6, 'unexpected amount of asset sold'); // It does not overcharge in case of non-divisible amount of assets userGhoBefore = GHO_TOKEN.balanceOf(ALICE); userAssetBefore = newToken.balanceOf(ALICE); (uint256 assetAmount, uint256 ghoBought) = gsm.sellAsset(1.9e6, ALICE); assertEq(GHO_TOKEN.balanceOf(ALICE) - userGhoBefore, 1, 'unexpected amount of GHO purchased'); assertEq(ghoBought, 1, 'unexpected amount of returned GHO purchased'); assertEq(assetAmount, 1e6, 'Unexpected asset amount sold'); assertEq(userAssetBefore - newToken.balanceOf(ALICE), 1e6, 'unexpected amount of asset sold'); vm.stopPrank(); } /** * @dev Checks buyAsset does not provide more asset than the corresponding to the * gho amount charged, in case the underlying asset has more decimals than GHO (18) */ function testBuyAssetWithHigherDecimals() public { TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', 24, FAUCET); vm.prank(FAUCET); newToken.mint(ALICE, 1_000_000e24); ghoFaucet(ALICE, DEFAULT_CAPACITY); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( DEFAULT_FIXED_PRICE, // 1e18 address(newToken), 24 // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(ALICE, TREASURY, 1_000_000e24); GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', DEFAULT_CAPACITY); vm.startPrank(ALICE); // Alice sells some asset to the GSM newToken.approve(address(gsm), type(uint256).max); GHO_TOKEN.approve(address(gsm), type(uint256).max); gsm.sellAsset(1_000_000e24, ALICE); // The minimum amount of assets that can be bought is 1e6, and the contract recalculates // the corresponding amount of assets to the exact GHO burned. // User buys more asset than it should due to price conversion uint256 userGhoBefore = GHO_TOKEN.balanceOf(ALICE); uint256 userAssetBefore = newToken.balanceOf(ALICE); (uint256 assetAmount, uint256 ghoSold) = gsm.buyAsset(1.9e6, ALICE); // should by just 1e6 asset in exchange of 1 GHO assertEq(userGhoBefore - GHO_TOKEN.balanceOf(ALICE), 2, 'unexpected amount of GHO spent'); assertEq(ghoSold, 2, 'unexpected amount of returned GHO spent'); assertEq(assetAmount, 2e6, 'Unexpected asset amount bought'); assertEq( newToken.balanceOf(ALICE) - userAssetBefore, 2e6, 'unexpected amount of assets purchased' ); vm.stopPrank(); } /** * @dev Checks sellAsset function is aligned with getAssetAmountForSellAsset, * in case the underlying asset has less decimals than GHO (18) */ function testSellAssetByGhoAmountWithLowerDecimals() public { TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', 6, FAUCET); vm.prank(FAUCET); newToken.mint(ALICE, 1_000_000e6); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( DEFAULT_FIXED_PRICE, // 1e18 address(newToken), 6 // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(ALICE, TREASURY, 1_000_000e6); GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', DEFAULT_CAPACITY); // User wants to know how much asset must sell to get 1.9e12 GHO vm.startPrank(ALICE); uint256 ghoAmountToGet = 1.9e12; // this is the minimum that must get (uint256 estSellAssetAmount, uint256 exactGhoToGet, , ) = gsm.getAssetAmountForSellAsset( ghoAmountToGet ); uint256 userGhoBefore = GHO_TOKEN.balanceOf(ALICE); uint256 userAssetBefore = newToken.balanceOf(ALICE); newToken.approve(address(gsm), type(uint256).max); gsm.sellAsset(estSellAssetAmount, ALICE); assertEq( GHO_TOKEN.balanceOf(ALICE) - userGhoBefore, exactGhoToGet, 'exact gho amount to get does not match' ); assertGt( GHO_TOKEN.balanceOf(ALICE) - userGhoBefore, ghoAmountToGet, 'minimum gho to get not reached' ); assertEq( userAssetBefore - newToken.balanceOf(ALICE), estSellAssetAmount, 'sold assets above maximum amount' ); vm.stopPrank(); } /** * @dev Checks sellAsset function is aligned with getAssetAmountForSellAsset, * in case the underlying asset has more decimals than GHO (18) */ function testSellAssetByGhoAmountWithHigherDecimals() public { TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', 24, FAUCET); vm.prank(FAUCET); newToken.mint(ALICE, 1_000_000e24); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( DEFAULT_FIXED_PRICE, // 1e18 address(newToken), 24 // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(ALICE, TREASURY, 1_000_000e24); GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', DEFAULT_CAPACITY); // User wants to know how much asset must sell to get 1 GHO vm.startPrank(ALICE); uint256 ghoAmountToGet = 1; // this is the lowest GHO that can be purchased (uint256 estSellAssetAmount, uint256 exactGhoToGet, , ) = gsm.getAssetAmountForSellAsset( ghoAmountToGet ); uint256 userGhoBefore = GHO_TOKEN.balanceOf(ALICE); uint256 userAssetBefore = newToken.balanceOf(ALICE); newToken.approve(address(gsm), type(uint256).max); gsm.sellAsset(estSellAssetAmount, ALICE); assertEq( GHO_TOKEN.balanceOf(ALICE) - userGhoBefore, exactGhoToGet, 'exact gho amount to get does not match' ); assertEq( GHO_TOKEN.balanceOf(ALICE) - userGhoBefore, ghoAmountToGet, 'unexpected amount of GHO purchased' ); assertEq( userAssetBefore - newToken.balanceOf(ALICE), estSellAssetAmount, 'unexpected amount of asset sold' ); vm.stopPrank(); } /** * @dev Checks buyAsset function is aligned with getAssetAmountForBuyAsset, * in case the underlying asset has less decimals than GHO (18) */ function testBuyAssetByGhoAmountWithLowerDecimals() public { TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', 6, FAUCET); vm.prank(FAUCET); newToken.mint(ALICE, 1_000_000e6); ghoFaucet(ALICE, DEFAULT_CAPACITY); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( DEFAULT_FIXED_PRICE, // 1e18 address(newToken), 6 // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(ALICE, TREASURY, 1_000_000e6); GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', DEFAULT_CAPACITY); vm.startPrank(ALICE); // Alice sells some asset to the GSM newToken.approve(address(gsm), type(uint256).max); gsm.sellAsset(1_000_000e6, ALICE); // User wants to know how much asset can buy with 1.9e12 GHO uint256 ghoAmountToSpend = 1.9e12; // this is the maximum that can spend (uint256 estBuyAssetAmount, uint256 exactGhoSpent, , ) = gsm.getAssetAmountForBuyAsset( ghoAmountToSpend ); uint256 userGhoBefore = GHO_TOKEN.balanceOf(ALICE); uint256 userAssetBefore = newToken.balanceOf(ALICE); GHO_TOKEN.approve(address(gsm), type(uint256).max); gsm.buyAsset(estBuyAssetAmount, ALICE); assertTrue( userGhoBefore - GHO_TOKEN.balanceOf(ALICE) <= ghoAmountToSpend, 'gho spend above maximum amount' ); assertEq(userGhoBefore - GHO_TOKEN.balanceOf(ALICE), exactGhoSpent, 'gho spent does not match'); assertEq( newToken.balanceOf(ALICE) - userAssetBefore, estBuyAssetAmount, 'bought assets and amount diff do not match' ); vm.stopPrank(); } /** * @dev Checks buyAsset function is aligned with getAssetAmountForBuyAsset, * in case the underlying asset has more decimals than GHO (18) */ function testBuyAssetByGhoAmountWithHigherDecimals() public { TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', 24, FAUCET); vm.prank(FAUCET); newToken.mint(ALICE, 1_000_000e24); ghoFaucet(ALICE, DEFAULT_CAPACITY); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( DEFAULT_FIXED_PRICE, // 1e18 address(newToken), 24 // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(ALICE, TREASURY, 1_000_000e24); GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', DEFAULT_CAPACITY); vm.startPrank(ALICE); // Alice sells some asset to the GSM newToken.approve(address(gsm), type(uint256).max); gsm.sellAsset(1_000_000e24, ALICE); // User wants to know how much asset can buy with 1 GHO uint256 ghoAmountToSpend = 1; // this is the lowest amount that can spend (uint256 estBuyAssetAmount, uint256 exactGhoSpent, , ) = gsm.getAssetAmountForBuyAsset( ghoAmountToSpend ); uint256 userGhoBefore = GHO_TOKEN.balanceOf(ALICE); uint256 userAssetBefore = newToken.balanceOf(ALICE); GHO_TOKEN.approve(address(gsm), type(uint256).max); gsm.buyAsset(estBuyAssetAmount, ALICE); assertEq( userGhoBefore - GHO_TOKEN.balanceOf(ALICE), exactGhoSpent, 'exact gho spent does not match' ); assertEq( userGhoBefore - GHO_TOKEN.balanceOf(ALICE), ghoAmountToSpend, 'unexpected amount of GHO spent' ); assertEq( newToken.balanceOf(ALICE) - userAssetBefore, estBuyAssetAmount, 'unexpected amount of assets purchased' ); vm.stopPrank(); } } ================================================ FILE: src/test/TestGsmSwapFuzz.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; /** * @title TestGsmSwapFuzz * @dev Fuzzing tests for swap functions * @dev Bounds for priceRatio: [0.01e18, 100e18] * @dev Bounds for fees: [0, 5000] * @dev Bounds for underlyingDecimals: [5, 27] */ contract TestGsmSwapFuzz is TestGhoBase { using PercentageMath for uint256; using PercentageMath for uint128; struct TestFuzzSwapAssetVars { // estimation function 1 uint256 estAssetAmount1; uint256 estGhoAmount1; uint256 estGrossAmount1; uint256 estFeeAmount1; // estimation function 2 uint256 estAssetAmount2; uint256 estGhoAmount2; uint256 estGrossAmount2; uint256 estFeeAmount2; // swap function uint256 exactAssetAmount; uint256 exactGhoAmount; } function _checkValidPrice( FixedPriceStrategy priceStrat, uint256 assetAmount, uint256 ghoAmount ) internal { assertApproxEqAbs( priceStrat.getAssetPriceInGho(assetAmount, false), ghoAmount, 2, 'price between asset and gho amounts is not valid _1' ); assertApproxEqAbs( priceStrat.getAssetPriceInGho(assetAmount, true), ghoAmount, 2, 'price between asset and gho amounts is not valid _2' ); assertApproxEqAbs( priceStrat.getGhoPriceInAsset(ghoAmount, false), assetAmount, 2, 'price between asset and gho amounts is not valid _3' ); assertApproxEqAbs( priceStrat.getGhoPriceInAsset(ghoAmount, true), assetAmount, 2, 'price between asset and gho amounts is not valid _4' ); } /** * @dev Check there is no way of making money by sell-buy actions */ function testFuzzSellBuyNoArb( uint8 underlyingDecimals, uint256 priceRatio, uint256 assetAmount, uint256 buyFeeBps, uint256 sellFeeBps ) public { TestFuzzSwapAssetVars memory vars; underlyingDecimals = uint8(bound(underlyingDecimals, 5, 27)); buyFeeBps = bound(buyFeeBps, 0, 5000 - 1); sellFeeBps = bound(sellFeeBps, 0, 5000 - 1); priceRatio = bound(priceRatio, 0.01e18, 100e18); assetAmount = bound(assetAmount, 1, type(uint64).max - 1); TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', underlyingDecimals, FAUCET); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( priceRatio, address(newToken), underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(address(this), TREASURY, type(uint128).max); GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', type(uint128).max); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); gsm.updateFeeStrategy(address(newFeeStrategy)); } // Strat estimation (, vars.estGhoAmount1, , ) = gsm.getGhoAmountForSellAsset(assetAmount); (vars.estAssetAmount2, , , ) = gsm.getAssetAmountForBuyAsset(vars.estGhoAmount1); assertLe( vars.estAssetAmount2, assetAmount, 'getting more assetAmount than provided in estimation' ); // Init GSM with some assets (, uint256 aux, , ) = gsm.getGhoAmountForSellAsset(assetAmount); vm.assume(aux > 0); vm.startPrank(FAUCET); newToken.mint(FAUCET, assetAmount); newToken.approve(address(gsm), type(uint256).max); gsm.sellAsset(assetAmount, FAUCET); vm.stopPrank(); // Arb Strat estimation (, vars.estGhoAmount1, , ) = gsm.getGhoAmountForSellAsset(assetAmount); (vars.estAssetAmount2, , , ) = gsm.getAssetAmountForBuyAsset(vars.estGhoAmount1); assertLe( vars.estAssetAmount2, assetAmount, 'getting more assetAmount than provided in estimation' ); // Top up Alice vm.prank(FAUCET); newToken.mint(ALICE, assetAmount); // Arb Strat vm.startPrank(ALICE); uint256 aliceBalanceBefore = newToken.balanceOf(ALICE); newToken.approve(address(gsm), type(uint256).max); GHO_TOKEN.approve(address(gsm), type(uint256).max); (, vars.exactGhoAmount) = gsm.sellAsset(assetAmount, ALICE); (vars.estAssetAmount1, , , ) = gsm.getAssetAmountForBuyAsset(vars.exactGhoAmount); assertLe( vars.estAssetAmount1, assetAmount, 'getting more assetAmount than provided in estimation' ); vm.assume(vars.estAssetAmount1 > 0); // 0 value is a valid for the property to hold, but buyAsset op would fail (vars.exactAssetAmount, ) = gsm.buyAsset(vars.estAssetAmount1, ALICE); assertLe(vars.exactAssetAmount, assetAmount, 'getting more assetAmount than provided in swap'); assertLe(newToken.balanceOf(ALICE), aliceBalanceBefore, 'asset balance more than before'); vm.stopPrank(); } /** * @dev It is possible to use values for price ratio that creates unbalance in the GSM, so all GHO cannot be burned. * e.g. With (1e16 + 1) priceRatio, a user gets 1e11 gho for selling 1 asset but gets 1 asset by selling 1e11+1 gho */ function testFuzzPriceRatioRoundingUnbalance( uint8 underlyingDecimals, uint256 priceRatio, uint256 amount, uint256 buyFeeBps, uint256 sellFeeBps ) public { underlyingDecimals = uint8(bound(underlyingDecimals, 5, 27)); buyFeeBps = bound(buyFeeBps, 0, 5000 - 1); sellFeeBps = bound(sellFeeBps, 0, 5000 - 1); priceRatio = bound(priceRatio, 0.01e18, 100e18); amount = bound(amount, 1, type(uint128).max - 1); TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', underlyingDecimals, FAUCET); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( priceRatio, address(newToken), underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(address(this), TREASURY, type(uint128).max); // Get gho amount for selling assets (uint256 assetSold, , uint256 ghoMinted, ) = gsm.getGhoAmountForSellAsset(amount); (, , uint256 ghoToBurn, ) = gsm.getGhoAmountForBuyAsset(assetSold); // 1 unit of imprecision due to rounding assertTrue(ghoToBurn <= ghoMinted + 1, 'unexpected gsm unbalance'); // Get amount of assets can be purchased based on minted GHO amount (, uint256 ghoAmount, , ) = gsm.getAssetAmountForBuyAsset(ghoMinted); assertTrue(ghoAmount <= ghoMinted); } /** * @dev Checks that passing an amount higher than 2*128-1 (`maxUint128`) to a swap function reverts */ function testFuzzSwapAmountAbove128( uint8 underlyingDecimals, uint256 priceRatio, uint256 amount, uint256 buyFeeBps, uint256 sellFeeBps ) public { underlyingDecimals = uint8(bound(underlyingDecimals, 5, 27)); buyFeeBps = bound(buyFeeBps, 0, 5000 - 1); sellFeeBps = bound(sellFeeBps, 0, 5000 - 1); priceRatio = bound(priceRatio, 0.01e18, 100e18); amount = bound(amount, 1, type(uint128).max) + type(uint128).max; // avoiding a bug in forge-std where bound will revert TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', underlyingDecimals, FAUCET); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( priceRatio, address(newToken), underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(address(this), TREASURY, type(uint128).max); GHO_TOKEN.addFacilitator(address(gsm), 'Test GSM', type(uint128).max); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); gsm.updateFeeStrategy(address(newFeeStrategy)); } (uint256 ghoBought, , , ) = gsm.getGhoAmountForSellAsset(amount); vm.assume(ghoBought > type(uint128).max); vm.startPrank(FAUCET); newToken.mint(FAUCET, amount); newToken.approve(address(gsm), amount); vm.expectRevert(); gsm.sellAsset(amount, ALICE); vm.stopPrank(); } /** * @dev Tests to ensure a revert when the GSM holds the maximum amount of asset possible and a user attempts to buy * 1 more unit of the asset than is available */ function testFuzzBuyAmountAboveMaximum( uint8 underlyingDecimals, uint256 priceRatio, uint256 buyFeeBps, uint256 sellFeeBps ) public { underlyingDecimals = uint8(bound(underlyingDecimals, 5, 27)); priceRatio = bound(priceRatio, 0.01e18, 100e18); buyFeeBps = bound(buyFeeBps, 0, 5000 - 1); sellFeeBps = bound(sellFeeBps, 0, 5000 - 1); TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', underlyingDecimals, FAUCET); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( priceRatio, address(newToken), underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(address(this), TREASURY, type(uint128).max); GHO_TOKEN.addFacilitator(address(gsm), 'Test GSM', type(uint128).max); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); gsm.updateFeeStrategy(address(newFeeStrategy)); } uint256 amount = newPriceStrategy.getGhoPriceInAsset(type(uint128).max, false); if (amount > type(uint128).max) { amount = type(uint128).max; } vm.startPrank(FAUCET); newToken.mint(FAUCET, amount); newToken.approve(address(gsm), amount); gsm.sellAsset(amount, ALICE); vm.stopPrank(); ghoFaucet(BOB, type(uint128).max); vm.startPrank(BOB); GHO_TOKEN.approve(address(gsm), type(uint128).max); vm.expectRevert(); gsm.buyAsset(amount + 1, BOB); vm.stopPrank(); } /** * @dev Checks behaviour of getGhoAmountForSellAsset */ function testFuzzGetGhoAmountForSellAsset( uint8 underlyingDecimals, uint256 priceRatio, uint256 amount, uint256 buyFeeBps, uint256 sellFeeBps ) public { underlyingDecimals = uint8(bound(underlyingDecimals, 5, 27)); buyFeeBps = bound(buyFeeBps, 0, 5000 - 1); sellFeeBps = bound(sellFeeBps, 0, 5000 - 1); priceRatio = bound(priceRatio, 0.01e18, 100e18); amount = bound(amount, 1, type(uint128).max - 1); TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', underlyingDecimals, FAUCET); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( priceRatio, address(newToken), underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(address(this), TREASURY, type(uint128).max); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); gsm.updateFeeStrategy(address(newFeeStrategy)); } (uint256 exactAssetAmount, uint256 ghoBought, uint256 grossAmount, uint256 fee) = gsm .getGhoAmountForSellAsset(amount); assertTrue(exactAssetAmount <= amount, 'maximum asset amount exceeded'); assertTrue(ghoBought <= grossAmount, 'gross amount lower than ghoBought'); _checkValidPrice(newPriceStrategy, exactAssetAmount, grossAmount); // In case of 0 sellFee if (sellFeeBps == 0) { assertEq(grossAmount, ghoBought, 'unexpected gross amount'); assertEq(fee, 0, 'unexpected fee'); } } /** * @dev Checks behaviour of getGhoAmountForBuyAsset */ function testFuzzGetGhoAmountForBuyAsset( uint8 underlyingDecimals, uint256 priceRatio, uint256 amount, uint256 buyFeeBps, uint256 sellFeeBps ) public { underlyingDecimals = uint8(bound(underlyingDecimals, 5, 27)); buyFeeBps = bound(buyFeeBps, 0, 5000 - 1); sellFeeBps = bound(sellFeeBps, 0, 5000 - 1); priceRatio = bound(priceRatio, 0.01e18, 100e18); amount = bound(amount, 1, type(uint128).max - 1); TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', underlyingDecimals, FAUCET); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( priceRatio, address(newToken), underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(address(this), TREASURY, type(uint128).max); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); gsm.updateFeeStrategy(address(newFeeStrategy)); } (uint256 exactAssetAmount, uint256 ghoSold, uint256 grossAmount, uint256 fee) = gsm .getGhoAmountForBuyAsset(amount); assertTrue(exactAssetAmount >= amount, 'minimum asset amount not reached'); assertTrue(ghoSold >= grossAmount, 'gross amount lower than ghoSold'); _checkValidPrice(newPriceStrategy, exactAssetAmount, grossAmount); // In case of 0 buyFee if (buyFeeBps == 0) { assertEq(grossAmount, ghoSold, 'unexpected gross amount'); assertEq(fee, 0, 'unexpected fee'); } } /** * @dev Checks behaviour of getAssetAmountForSellAsset */ function testFuzzGetAssetAmountForSellAsset( uint8 underlyingDecimals, uint256 priceRatio, uint256 amount, uint256 buyFeeBps, uint256 sellFeeBps ) public { underlyingDecimals = uint8(bound(underlyingDecimals, 5, 27)); buyFeeBps = bound(buyFeeBps, 0, 5000 - 1); sellFeeBps = bound(sellFeeBps, 0, 5000 - 1); priceRatio = bound(priceRatio, 0.01e18, 100e18); amount = bound(amount, 1, type(uint128).max - 1); TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', underlyingDecimals, FAUCET); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( priceRatio, address(newToken), underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(address(this), TREASURY, type(uint128).max); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); gsm.updateFeeStrategy(address(newFeeStrategy)); } (uint256 exactAssetAmount, uint256 ghoBought, uint256 grossAmount, uint256 fee) = gsm .getAssetAmountForSellAsset(amount); assertTrue(ghoBought > 0, 'unexpected 0 value for ghoBought'); assertTrue(ghoBought >= amount, 'minimum gho amount not reached'); assertTrue(ghoBought <= grossAmount, 'gross amount lower than ghoBought'); _checkValidPrice(newPriceStrategy, exactAssetAmount, grossAmount); // In case of 0 sellFee if (sellFeeBps == 0) { assertEq(grossAmount, ghoBought, 'unexpected gross amount'); assertEq(fee, 0, 'unexpected fee'); } } /** * @dev Checks behaviour of getAssetAmountForBuyAsset */ function testFuzzGetAssetAmountForBuyAsset( uint8 underlyingDecimals, uint256 priceRatio, uint256 amount, uint256 buyFeeBps, uint256 sellFeeBps ) public { underlyingDecimals = uint8(bound(underlyingDecimals, 5, 27)); buyFeeBps = bound(buyFeeBps, 0, 5000 - 1); sellFeeBps = bound(sellFeeBps, 0, 5000 - 1); priceRatio = bound(priceRatio, 0.01e18, 100e18); amount = bound(amount, 1, type(uint128).max - 1); TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', underlyingDecimals, FAUCET); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( priceRatio, address(newToken), underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(address(this), TREASURY, type(uint128).max); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); gsm.updateFeeStrategy(address(newFeeStrategy)); } (uint256 exactAssetAmount, uint256 ghoSold, uint256 grossAmount, uint256 fee) = gsm .getAssetAmountForBuyAsset(amount); assertTrue(ghoSold <= amount, 'maximum gho amount exceeded'); assertTrue(ghoSold >= grossAmount, 'gross amount lower than ghoSold'); _checkValidPrice(newPriceStrategy, exactAssetAmount, grossAmount); // In case of 0 buyFee if (buyFeeBps == 0) { assertEq(grossAmount, ghoSold, 'unexpected gross amount'); assertEq(fee, 0, 'unexpected fee'); } } /** * @dev Checks invariant between inverse functions to query amounts for the sell action: getGhoAmountForSellAsset * and getAssetAmountForSellAsset. */ function testFuzzSellEstimation( uint8 underlyingDecimals, uint256 priceRatio, uint256 assetAmount, uint256 buyFeeBps, uint256 sellFeeBps ) public { TestFuzzSwapAssetVars memory vars; underlyingDecimals = uint8(bound(underlyingDecimals, 5, 27)); buyFeeBps = bound(buyFeeBps, 0, 5000 - 1); sellFeeBps = bound(sellFeeBps, 0, 5000 - 1); priceRatio = bound(priceRatio, 0.01e18, 100e18); assetAmount = bound(assetAmount, 1, type(uint64).max - 1); TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', underlyingDecimals, FAUCET); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( priceRatio, address(newToken), underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(address(this), TREASURY, uint128(assetAmount)); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); gsm.updateFeeStrategy(address(newFeeStrategy)); } (vars.estAssetAmount1, vars.estGhoAmount1, vars.estGrossAmount1, vars.estFeeAmount1) = gsm .getGhoAmountForSellAsset(assetAmount); vm.assume(vars.estGhoAmount1 > 0); (vars.estAssetAmount2, vars.estGhoAmount2, vars.estGrossAmount2, vars.estFeeAmount2) = gsm .getAssetAmountForSellAsset(vars.estGhoAmount1); assertTrue( assetAmount >= vars.estAssetAmount1, 'exact asset amount being used is higher than the amount passed' ); assertTrue( assetAmount >= vars.estAssetAmount2, 'exact asset amount being used is higher than the amount passed' ); assertEq(vars.estGhoAmount1, vars.estGhoAmount2, 'bought gho amount do not match'); assertEq( vars.estAssetAmount1, vars.estAssetAmount2, 'given assetAmount and estimated do not match' ); // 1 wei precision error assertApproxEqAbs( vars.estGrossAmount1, vars.estGrossAmount2, 1, 'estimated gross amounts do not match' ); assertApproxEqAbs(vars.estFeeAmount1, vars.estFeeAmount2, 1, 'estimated fees do not match'); // In case of 0 sellFee if (sellFeeBps == 0) { assertEq(vars.estGrossAmount1, vars.estGhoAmount1, 'unexpected grossAmount1 and ghoBought'); assertEq(vars.estGrossAmount2, vars.estGhoAmount1, 'unexpected grossAmount2 and ghoBought'); assertEq(vars.estFeeAmount1, 0, 'expected fee1'); assertEq(vars.estFeeAmount2, 0, 'expected fee2'); } } /** * @dev Checks invariant between inverse functions to query amounts for the buy action: getGhoAmountForBuyAsset * and getAssetAmountForBuyAsset. */ function testFuzzBuyEstimation( uint8 underlyingDecimals, uint256 priceRatio, uint256 assetAmount, uint256 buyFeeBps, uint256 sellFeeBps ) public { TestFuzzSwapAssetVars memory vars; underlyingDecimals = uint8(bound(underlyingDecimals, 5, 27)); buyFeeBps = bound(buyFeeBps, 0, 5000 - 1); sellFeeBps = bound(sellFeeBps, 0, 5000 - 1); priceRatio = bound(priceRatio, 0.01e18, 100e18); assetAmount = bound(assetAmount, 1, type(uint64).max - 1); TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', underlyingDecimals, FAUCET); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( priceRatio, address(newToken), underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(address(this), TREASURY, uint128(assetAmount)); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); gsm.updateFeeStrategy(address(newFeeStrategy)); } (vars.estAssetAmount1, vars.estGhoAmount1, vars.estGrossAmount1, vars.estFeeAmount1) = gsm .getGhoAmountForBuyAsset(assetAmount); vm.assume(vars.estGhoAmount1 > 0); (vars.estAssetAmount2, vars.estGhoAmount2, vars.estGrossAmount2, vars.estFeeAmount2) = gsm .getAssetAmountForBuyAsset(vars.estGhoAmount1); assertTrue( vars.estAssetAmount1 >= assetAmount, 'exact asset amount being used is less than the amount passed' ); assertTrue( vars.estAssetAmount2 >= assetAmount, 'exact asset amount being used is less than the amount passed' ); assertEq(vars.estGhoAmount1, vars.estGhoAmount2, 'sold gho amount do not match'); assertEq( vars.estAssetAmount1, vars.estAssetAmount2, 'given assetAmount and estimated do not match' ); assertEq(vars.estGrossAmount1, vars.estGrossAmount2, 'estimated gross amounts do not match'); assertEq(vars.estFeeAmount1, vars.estFeeAmount2, 'estimated fees do not match'); // In case of 0 buyFee if (buyFeeBps == 0) { assertEq(vars.estGrossAmount1, vars.estGhoAmount1, 'unexpected grossAmount1 and ghoSold'); assertEq(vars.estGrossAmount2, vars.estGhoAmount1, 'unexpected grossAmount2 and ghoSold'); assertEq(vars.estFeeAmount1, 0, 'expected fee1'); assertEq(vars.estFeeAmount2, 0, 'expected fee2'); } } /** * @dev Checks sellAsset is aligned with getAssetAmountForSellAsset and getGhoAmountForSellAsset */ function testFuzzSellAssetWithEstimation( uint8 underlyingDecimals, uint256 priceRatio, uint256 assetAmount, uint256 buyFeeBps, uint256 sellFeeBps ) public { TestFuzzSwapAssetVars memory vars; underlyingDecimals = uint8(bound(underlyingDecimals, 5, 27)); buyFeeBps = bound(buyFeeBps, 0, 5000 - 1); sellFeeBps = bound(sellFeeBps, 0, 5000 - 1); priceRatio = bound(priceRatio, 0.01e18, 100e18); assetAmount = bound(assetAmount, 1, type(uint64).max - 1); TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', underlyingDecimals, FAUCET); vm.prank(FAUCET); newToken.mint(ALICE, assetAmount); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( priceRatio, address(newToken), underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(address(this), TREASURY, uint128(assetAmount)); GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', type(uint128).max); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); gsm.updateFeeStrategy(address(newFeeStrategy)); } vm.startPrank(ALICE); newToken.approve(address(gsm), type(uint256).max); uint256 userGhoBefore = GHO_TOKEN.balanceOf(ALICE); uint256 userAssetBefore = newToken.balanceOf(ALICE); // Calculate GHO amount to purchase with given asset amount, bail if 0 (, vars.estGhoAmount1, vars.estGrossAmount1, vars.estFeeAmount1) = gsm.getGhoAmountForSellAsset( assetAmount ); vm.assume(vars.estGhoAmount1 > 0); (vars.exactAssetAmount, vars.exactGhoAmount) = gsm.sellAsset(assetAmount, ALICE); // Calculate asset amount needed for the amount of GHO required to buy (vars.estAssetAmount2, , vars.estGrossAmount2, vars.estFeeAmount2) = gsm .getAssetAmountForSellAsset(vars.exactGhoAmount); assertEq( userAssetBefore - newToken.balanceOf(ALICE), vars.exactAssetAmount, 'real assets sold are not equal to the exact amount' ); assertTrue( userAssetBefore - newToken.balanceOf(ALICE) <= assetAmount, 'real assets sold are more than the input' ); assertEq( GHO_TOKEN.balanceOf(ALICE) - userGhoBefore, vars.exactGhoAmount, 'real gho bought does not match returned value' ); assertEq( GHO_TOKEN.balanceOf(ALICE) - userGhoBefore, vars.estGhoAmount1, 'real gho bought does not match estimated value' ); assertEq( userAssetBefore - newToken.balanceOf(ALICE), vars.estAssetAmount2, 'real assets sold does not match estimated value' ); // 1 wei precision error assertApproxEqAbs( vars.estGrossAmount1, vars.estGrossAmount2, 1, 'estimated gross amounts do not match' ); assertApproxEqAbs(vars.estFeeAmount1, vars.estFeeAmount2, 1, 'estimated fees do not match'); // In case of 0 sellFeeBps if (sellFeeBps == 0) { assertEq(vars.estGrossAmount1, vars.exactGhoAmount, 'unexpected grossAmount1 and ghoBought'); assertEq(vars.estGrossAmount2, vars.exactGhoAmount, 'unexpected grossAmount2 and ghoBought'); assertEq(vars.estFeeAmount1, 0, 'expected fee1'); assertEq(vars.estFeeAmount2, 0, 'expected fee2'); } vm.stopPrank(); } /** * @dev Checks buyAsset is aligned with getAssetAmountForBuyAsset and getGhoAmountForBuyAsset */ function testFuzzBuyAssetWithEstimation( uint8 underlyingDecimals, uint256 priceRatio, uint256 assetAmount, uint256 buyFeeBps, uint256 sellFeeBps ) public { TestFuzzSwapAssetVars memory vars; underlyingDecimals = uint8(bound(underlyingDecimals, 5, 27)); buyFeeBps = bound(buyFeeBps, 0, 5000 - 1); sellFeeBps = bound(sellFeeBps, 0, 5000 - 1); priceRatio = bound(priceRatio, 0.01e18, 100e18); assetAmount = bound(assetAmount, 1, type(uint64).max - 1); TestnetERC20 newToken = new TestnetERC20('Test Coin', 'TEST', underlyingDecimals, FAUCET); FixedPriceStrategy newPriceStrategy = new FixedPriceStrategy( priceRatio, address(newToken), underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); gsm.initialize(address(this), TREASURY, type(uint128).max); GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', type(uint128).max); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); gsm.updateFeeStrategy(address(newFeeStrategy)); } // Alice sells some assets to the GSM, so the purchase is doable uint256 sellAssetAmount = newPriceStrategy.getGhoPriceInAsset(type(uint128).max, false); if (sellAssetAmount > type(uint128).max) { sellAssetAmount = type(uint128).max; } vm.prank(FAUCET); newToken.mint(ALICE, sellAssetAmount); vm.startPrank(ALICE); newToken.approve(address(gsm), type(uint256).max); gsm.sellAsset(sellAssetAmount, ALICE); vm.stopPrank(); // rough estimation of GHO funds needed for buyAsset (, uint256 estGhoBought, , ) = gsm.getGhoAmountForBuyAsset(assetAmount); ghoFaucet(ALICE, estGhoBought * 20); // Buy vm.startPrank(ALICE); uint256 userGhoBefore = GHO_TOKEN.balanceOf(ALICE); uint256 userAssetBefore = newToken.balanceOf(ALICE); // Calculate GHO amount to sell with given asset amount (, vars.estGhoAmount1, vars.estGrossAmount1, vars.estFeeAmount1) = gsm.getGhoAmountForBuyAsset( assetAmount ); userGhoBefore = GHO_TOKEN.balanceOf(ALICE); userAssetBefore = newToken.balanceOf(ALICE); GHO_TOKEN.approve(address(gsm), type(uint256).max); (vars.exactAssetAmount, vars.exactGhoAmount) = gsm.buyAsset(assetAmount, ALICE); // Calculate asset amount can be bought for the amount of GHO available (vars.estAssetAmount2, vars.estGhoAmount2, vars.estGrossAmount2, vars.estFeeAmount2) = gsm .getAssetAmountForBuyAsset(vars.exactGhoAmount); assertEq( newToken.balanceOf(ALICE) - userAssetBefore, vars.exactAssetAmount, 'real assets bought are not equal to the exact amount' ); assertTrue( newToken.balanceOf(ALICE) - userAssetBefore >= assetAmount, 'real assets bought are less than the input' ); assertEq( newToken.balanceOf(ALICE) - userAssetBefore, vars.estAssetAmount2, 'real assets bought does not match estimated value' ); assertEq( userGhoBefore - GHO_TOKEN.balanceOf(ALICE), vars.exactGhoAmount, 'real gho sold does not match returned value' ); assertEq( userGhoBefore - GHO_TOKEN.balanceOf(ALICE), vars.estGhoAmount1, 'real gho sold does not match estimated value' ); assertEq(vars.estGhoAmount1, vars.estGhoAmount2, 'estimated gross amounts do not match'); assertEq(vars.estFeeAmount1, vars.estFeeAmount2, 'estimated fees do not match'); // In case of 0 buyFeeBps if (buyFeeBps == 0) { assertEq(vars.estGhoAmount1, vars.exactGhoAmount, 'unexpected grossAmount1 and ghoSold'); assertEq(vars.estGhoAmount2, vars.exactGhoAmount, 'unexpected grossAmount2 and ghoSold'); assertEq(vars.estFeeAmount1, 0, 'expected fee1'); assertEq(vars.estFeeAmount2, 0, 'expected fee2'); } vm.stopPrank(); } } ================================================ FILE: src/test/TestGsmUpgrade.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestGsmUpgrade is TestGhoBase { function testUpgrade() public { assertEq(GHO_GSM.GSM_REVISION(), 1, 'Unexpected pre-upgrade GSM revision'); bytes32[] memory beforeSnapshot = _getStorageSnapshot(); // Sanity check on select storage variable assertEq(uint256(beforeSnapshot[1]), uint160(TREASURY), 'GHO Treasury address not set'); // Perform the mock upgrade address gsmV2 = address( new MockGsmV2(address(GHO_TOKEN), address(USDC_TOKEN), address(GHO_GSM_FIXED_PRICE_STRATEGY)) ); bytes memory data = abi.encodeWithSelector(MockGsmV2.initialize.selector); vm.expectEmit(true, false, false, true, address(GHO_GSM)); emit Upgraded(gsmV2); vm.prank(SHORT_EXECUTOR); AdminUpgradeabilityProxy(payable(address(GHO_GSM))).upgradeToAndCall(gsmV2, data); assertEq(GHO_GSM.GSM_REVISION(), 2, 'Unexpected post-upgrade GSM revision'); bytes32[] memory afterSnapshot = _getStorageSnapshot(); // First storage item should be different, the rest the same post-upgrade assertTrue(afterSnapshot[0] != beforeSnapshot[0], 'Unexpected lastInitializedRevision'); for (uint8 i = 1; i < afterSnapshot.length; i++) { assertEq(afterSnapshot[i], beforeSnapshot[i], 'Unexpected storage value updated'); } } function _getStorageSnapshot() internal view returns (bytes32[] memory) { // Snapshot values for lastInitializedRevision (slot 1) and GSM local storage (54-58) bytes32[] memory data = new bytes32[](6); data[0] = vm.load(address(GHO_GSM), bytes32(uint256(1))); data[1] = vm.load(address(GHO_GSM), bytes32(uint256(54))); data[2] = vm.load(address(GHO_GSM), bytes32(uint256(55))); data[3] = vm.load(address(GHO_GSM), bytes32(uint256(56))); data[4] = vm.load(address(GHO_GSM), bytes32(uint256(57))); data[5] = vm.load(address(GHO_GSM), bytes32(uint256(58))); return data; } } ================================================ FILE: src/test/TestUiGhoDataProvider.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; import {UiGhoDataProvider, IUiGhoDataProvider} from '../contracts/facilitators/aave/misc/UiGhoDataProvider.sol'; contract TestUiGhoDataProvider is TestGhoBase { UiGhoDataProvider dataProvider; function setUp() public { dataProvider = new UiGhoDataProvider(IPool(POOL), GHO_TOKEN); } function testGhoReserveData() public { DataTypes.ReserveData memory baseData = POOL.getReserveData(address(GHO_TOKEN)); (uint256 bucketCapacity, uint256 bucketLevel) = GHO_TOKEN.getFacilitatorBucket( baseData.aTokenAddress ); IUiGhoDataProvider.GhoReserveData memory result = dataProvider.getGhoReserveData(); assertEq( result.ghoBaseVariableBorrowRate, baseData.currentVariableBorrowRate, 'Unexpected variable borrow rate' ); assertEq( result.ghoDiscountedPerToken, GHO_DISCOUNT_STRATEGY.GHO_DISCOUNTED_PER_DISCOUNT_TOKEN(), 'Unexpected discount per token' ); assertEq( result.ghoDiscountRate, GHO_DISCOUNT_STRATEGY.DISCOUNT_RATE(), 'Unexpected discount rate' ); assertEq( result.ghoMinDiscountTokenBalanceForDiscount, GHO_DISCOUNT_STRATEGY.MIN_DISCOUNT_TOKEN_BALANCE(), 'Unexpected minimum discount token balance' ); assertEq( result.ghoMinDebtTokenBalanceForDiscount, GHO_DISCOUNT_STRATEGY.MIN_DEBT_TOKEN_BALANCE(), 'Unexpected minimum debt token balance' ); assertEq( result.ghoReserveLastUpdateTimestamp, baseData.lastUpdateTimestamp, 'Unexpected last timestamp' ); assertEq(result.ghoCurrentBorrowIndex, baseData.variableBorrowIndex, 'Unexpected borrow index'); assertEq(result.aaveFacilitatorBucketLevel, bucketLevel, 'Unexpected facilitator bucket level'); assertEq( result.aaveFacilitatorBucketMaxCapacity, bucketCapacity, 'Unexpected facilitator bucket capacity' ); } function testGhoUserData() public { IUiGhoDataProvider.GhoUserData memory result = dataProvider.getGhoUserData(ALICE); assertEq( result.userGhoDiscountPercent, GHO_DEBT_TOKEN.getDiscountPercent(ALICE), 'Unexpected discount percent' ); assertEq( result.userDiscountTokenBalance, IERC20(GHO_DEBT_TOKEN.getDiscountToken()).balanceOf(ALICE), 'Unexpected discount token balance' ); assertEq( result.userPreviousGhoBorrowIndex, GHO_DEBT_TOKEN.getPreviousIndex(ALICE), 'Unexpected previous index' ); assertEq( result.userGhoScaledBorrowBalance, GHO_DEBT_TOKEN.scaledBalanceOf(ALICE), 'Unexpected scaled balance' ); } } ================================================ FILE: src/test/TestUpgradeableGhoToken.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; contract TestUpgradeableGhoTokenSetup is TestGhoBase { address internal PROXY_ADMIN = makeAddr('PROXY_ADMIN'); UpgradeableGhoToken internal ghoToken; function setUp() public virtual { UpgradeableGhoToken ghoTokenImple = new UpgradeableGhoToken(); // proxy deploy and init bytes memory ghoTokenImpleParams = abi.encodeWithSignature( 'initialize(address)', address(this) ); TransparentUpgradeableProxy ghoTokenProxy = new TransparentUpgradeableProxy( address(ghoTokenImple), PROXY_ADMIN, ghoTokenImpleParams ); ghoToken = UpgradeableGhoToken(address(ghoTokenProxy)); } } contract TestUpgradeableGhoToken is TestUpgradeableGhoTokenSetup { function setUp() public override { super.setUp(); // Grant ghoToken.grantRole(GHO_TOKEN_FACILITATOR_MANAGER_ROLE, address(this)); ghoToken.grantRole(GHO_TOKEN_BUCKET_MANAGER_ROLE, address(this)); // Add Aave as Facilitator ghoToken.addFacilitator(address(GHO_ATOKEN), 'Aave V3 Pool', DEFAULT_CAPACITY); // Add Faucet ad Facilitator ghoToken.addFacilitator(FAUCET, 'Faucet Facilitator', type(uint128).max); } function testInit() public { UpgradeableGhoToken ghoTokenImple = new UpgradeableGhoToken(); // proxy deploy and init bytes memory ghoTokenImpleParams = abi.encodeWithSignature( 'initialize(address)', address(this) ); vm.expectEmit(true, true, true, true); emit RoleGranted(DEFAULT_ADMIN_ROLE, address(this), address(this)); TransparentUpgradeableProxy ghoTokenProxy = new TransparentUpgradeableProxy( address(ghoTokenImple), PROXY_ADMIN, ghoTokenImpleParams ); // Implementation asserts assertEq(ghoTokenImple.decimals(), 18, 'Wrong default ERC20 decimals'); vm.expectRevert('Initializable: contract is already initialized'); ghoTokenImple.initialize(address(this)); // Proxy asserts UpgradeableGhoToken token = UpgradeableGhoToken(address(ghoTokenProxy)); assertEq(token.name(), 'Gho Token', 'Wrong default ERC20 name'); assertEq(token.symbol(), 'GHO', 'Wrong default ERC20 symbol'); assertEq(token.decimals(), 18, 'Wrong default ERC20 decimals'); assertEq(token.getFacilitatorsList().length, 0, 'Facilitator list not empty'); } function testGetFacilitatorData() public { IGhoToken.Facilitator memory data = ghoToken.getFacilitator(address(GHO_ATOKEN)); assertEq(data.label, 'Aave V3 Pool', 'Unexpected facilitator label'); assertEq(data.bucketCapacity, DEFAULT_CAPACITY, 'Unexpected bucket capacity'); assertEq(data.bucketLevel, 0, 'Unexpected bucket level'); } function testGetNonFacilitatorData() public { IGhoToken.Facilitator memory data = ghoToken.getFacilitator(ALICE); assertEq(data.label, '', 'Unexpected facilitator label'); assertEq(data.bucketCapacity, 0, 'Unexpected bucket capacity'); assertEq(data.bucketLevel, 0, 'Unexpected bucket level'); } function testGetFacilitatorBucket() public { (uint256 capacity, uint256 level) = ghoToken.getFacilitatorBucket(address(GHO_ATOKEN)); assertEq(capacity, DEFAULT_CAPACITY, 'Unexpected bucket capacity'); assertEq(level, 0, 'Unexpected bucket level'); } function testGetNonFacilitatorBucket() public { (uint256 capacity, uint256 level) = ghoToken.getFacilitatorBucket(ALICE); assertEq(capacity, 0, 'Unexpected bucket capacity'); assertEq(level, 0, 'Unexpected bucket level'); } function testGetPopulatedFacilitatorsList() public { address[] memory facilitatorList = ghoToken.getFacilitatorsList(); assertEq(facilitatorList.length, 2, 'Unexpected number of facilitators'); assertEq(facilitatorList[0], address(GHO_ATOKEN), 'Unexpected address for mock facilitator 1'); assertEq(facilitatorList[1], FAUCET, 'Unexpected address for mock facilitator 5'); } function testAddFacilitator() public { vm.expectEmit(true, true, false, true, address(ghoToken)); emit FacilitatorAdded(ALICE, keccak256(abi.encodePacked('Alice')), DEFAULT_CAPACITY); ghoToken.addFacilitator(ALICE, 'Alice', DEFAULT_CAPACITY); } function testAddFacilitatorWithRole() public { vm.expectEmit(true, true, true, true, address(ghoToken)); emit RoleGranted(GHO_TOKEN_FACILITATOR_MANAGER_ROLE, ALICE, address(this)); ghoToken.grantRole(GHO_TOKEN_FACILITATOR_MANAGER_ROLE, ALICE); vm.prank(ALICE); vm.expectEmit(true, true, false, true, address(ghoToken)); emit FacilitatorAdded(ALICE, keccak256(abi.encodePacked('Alice')), DEFAULT_CAPACITY); ghoToken.addFacilitator(ALICE, 'Alice', DEFAULT_CAPACITY); } function testRevertAddExistingFacilitator() public { vm.expectRevert('FACILITATOR_ALREADY_EXISTS'); ghoToken.addFacilitator(address(GHO_ATOKEN), 'Aave V3 Pool', DEFAULT_CAPACITY); } function testRevertAddFacilitatorNoLabel() public { vm.expectRevert('INVALID_LABEL'); ghoToken.addFacilitator(ALICE, '', DEFAULT_CAPACITY); } function testRevertAddFacilitatorNoRole() public { vm.expectRevert( AccessControlErrorsLib.MISSING_ROLE(GHO_TOKEN_FACILITATOR_MANAGER_ROLE, address(ALICE)) ); vm.prank(ALICE); ghoToken.addFacilitator(ALICE, 'Alice', DEFAULT_CAPACITY); } function testRevertSetBucketCapacityNonFacilitator() public { vm.expectRevert('FACILITATOR_DOES_NOT_EXIST'); ghoToken.setFacilitatorBucketCapacity(ALICE, DEFAULT_CAPACITY); } function testSetNewBucketCapacity() public { vm.expectEmit(true, false, false, true, address(ghoToken)); emit FacilitatorBucketCapacityUpdated(address(GHO_ATOKEN), DEFAULT_CAPACITY, 0); ghoToken.setFacilitatorBucketCapacity(address(GHO_ATOKEN), 0); } function testSetNewBucketCapacityAsManager() public { vm.expectEmit(true, true, true, true, address(ghoToken)); emit RoleGranted(GHO_TOKEN_BUCKET_MANAGER_ROLE, ALICE, address(this)); ghoToken.grantRole(GHO_TOKEN_BUCKET_MANAGER_ROLE, ALICE); vm.prank(ALICE); vm.expectEmit(true, false, false, true, address(ghoToken)); emit FacilitatorBucketCapacityUpdated(address(GHO_ATOKEN), DEFAULT_CAPACITY, 0); ghoToken.setFacilitatorBucketCapacity(address(GHO_ATOKEN), 0); } function testRevertSetNewBucketCapacityNoRole() public { vm.expectRevert( AccessControlErrorsLib.MISSING_ROLE(GHO_TOKEN_BUCKET_MANAGER_ROLE, address(ALICE)) ); vm.prank(ALICE); ghoToken.setFacilitatorBucketCapacity(address(GHO_ATOKEN), 0); } function testRevertRemoveNonFacilitator() public { vm.expectRevert('FACILITATOR_DOES_NOT_EXIST'); ghoToken.removeFacilitator(ALICE); } function testRevertRemoveFacilitatorNonZeroBucket() public { vm.prank(FAUCET); ghoToken.mint(ALICE, 1); vm.expectRevert('FACILITATOR_BUCKET_LEVEL_NOT_ZERO'); ghoToken.removeFacilitator(FAUCET); } function testRemoveFacilitator() public { vm.expectEmit(true, false, false, true, address(ghoToken)); emit FacilitatorRemoved(address(GHO_ATOKEN)); ghoToken.removeFacilitator(address(GHO_ATOKEN)); } function testRemoveFacilitatorWithRole() public { vm.expectEmit(true, true, true, true, address(ghoToken)); emit RoleGranted(GHO_TOKEN_FACILITATOR_MANAGER_ROLE, ALICE, address(this)); ghoToken.grantRole(GHO_TOKEN_FACILITATOR_MANAGER_ROLE, ALICE); vm.prank(ALICE); vm.expectEmit(true, false, false, true, address(ghoToken)); emit FacilitatorRemoved(address(GHO_ATOKEN)); ghoToken.removeFacilitator(address(GHO_ATOKEN)); } function testRevertRemoveFacilitatorNoRole() public { vm.expectRevert( AccessControlErrorsLib.MISSING_ROLE(GHO_TOKEN_FACILITATOR_MANAGER_ROLE, address(ALICE)) ); vm.prank(ALICE); ghoToken.removeFacilitator(address(GHO_ATOKEN)); } function testRevertMintBadFacilitator() public { vm.prank(ALICE); vm.expectRevert('FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); ghoToken.mint(ALICE, DEFAULT_BORROW_AMOUNT); } function testRevertMintExceedCapacity() public { vm.prank(address(GHO_ATOKEN)); vm.expectRevert('FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); ghoToken.mint(ALICE, DEFAULT_CAPACITY + 1); } function testMint() public { vm.prank(address(GHO_ATOKEN)); vm.expectEmit(true, true, false, true, address(ghoToken)); emit Transfer(address(0), ALICE, DEFAULT_CAPACITY); vm.expectEmit(true, false, false, true, address(ghoToken)); emit FacilitatorBucketLevelUpdated(address(GHO_ATOKEN), 0, DEFAULT_CAPACITY); ghoToken.mint(ALICE, DEFAULT_CAPACITY); } function testRevertZeroMint() public { vm.prank(address(GHO_ATOKEN)); vm.expectRevert('INVALID_MINT_AMOUNT'); ghoToken.mint(ALICE, 0); } function testRevertZeroBurn() public { vm.prank(address(GHO_ATOKEN)); vm.expectRevert('INVALID_BURN_AMOUNT'); ghoToken.burn(0); } function testRevertBurnMoreThanMinted() public { vm.prank(address(GHO_ATOKEN)); vm.expectEmit(true, false, false, true, address(ghoToken)); emit FacilitatorBucketLevelUpdated(address(GHO_ATOKEN), 0, DEFAULT_CAPACITY); ghoToken.mint(address(GHO_ATOKEN), DEFAULT_CAPACITY); vm.prank(address(GHO_ATOKEN)); vm.expectRevert(stdError.arithmeticError); ghoToken.burn(DEFAULT_CAPACITY + 1); } function testRevertBurnOthersTokens() public { vm.prank(address(GHO_ATOKEN)); vm.expectEmit(true, true, false, true, address(ghoToken)); emit Transfer(address(0), ALICE, DEFAULT_CAPACITY); vm.expectEmit(true, false, false, true, address(ghoToken)); emit FacilitatorBucketLevelUpdated(address(GHO_ATOKEN), 0, DEFAULT_CAPACITY); ghoToken.mint(ALICE, DEFAULT_CAPACITY); vm.prank(address(GHO_ATOKEN)); vm.expectRevert(stdError.arithmeticError); ghoToken.burn(DEFAULT_CAPACITY); } function testBurn() public { vm.prank(address(GHO_ATOKEN)); vm.expectEmit(true, true, false, true, address(ghoToken)); emit Transfer(address(0), address(GHO_ATOKEN), DEFAULT_CAPACITY); vm.expectEmit(true, false, false, true, address(ghoToken)); emit FacilitatorBucketLevelUpdated(address(GHO_ATOKEN), 0, DEFAULT_CAPACITY); ghoToken.mint(address(GHO_ATOKEN), DEFAULT_CAPACITY); vm.prank(address(GHO_ATOKEN)); vm.expectEmit(true, false, false, true, address(ghoToken)); emit FacilitatorBucketLevelUpdated( address(GHO_ATOKEN), DEFAULT_CAPACITY, DEFAULT_CAPACITY - DEFAULT_BORROW_AMOUNT ); ghoToken.burn(DEFAULT_BORROW_AMOUNT); } function testOffboardFacilitator() public { // Onboard facilitator vm.expectEmit(true, true, false, true, address(ghoToken)); emit FacilitatorAdded(ALICE, keccak256(abi.encodePacked('Alice')), DEFAULT_CAPACITY); ghoToken.addFacilitator(ALICE, 'Alice', DEFAULT_CAPACITY); // Facilitator mints half of its capacity vm.prank(ALICE); ghoToken.mint(ALICE, DEFAULT_CAPACITY / 2); (uint256 bucketCapacity, uint256 bucketLevel) = ghoToken.getFacilitatorBucket(ALICE); assertEq(bucketCapacity, DEFAULT_CAPACITY, 'Unexpected bucket capacity of facilitator'); assertEq(bucketLevel, DEFAULT_CAPACITY / 2, 'Unexpected bucket level of facilitator'); // Facilitator cannot be removed vm.expectRevert('FACILITATOR_BUCKET_LEVEL_NOT_ZERO'); ghoToken.removeFacilitator(ALICE); // Facilitator Bucket Capacity set to 0 ghoToken.setFacilitatorBucketCapacity(ALICE, 0); // Facilitator cannot mint more and is expected to burn remaining level vm.prank(ALICE); vm.expectRevert('FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); ghoToken.mint(ALICE, 1); vm.prank(ALICE); ghoToken.burn(bucketLevel); // Facilitator can be removed with 0 bucket level vm.expectEmit(true, false, false, true, address(ghoToken)); emit FacilitatorRemoved(address(ALICE)); ghoToken.removeFacilitator(address(ALICE)); } function testDomainSeparator() public { bytes32 EIP712_DOMAIN = keccak256( 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' ); bytes memory EIP712_REVISION = bytes('1'); bytes32 expected = keccak256( abi.encode( EIP712_DOMAIN, keccak256(bytes(ghoToken.name())), keccak256(EIP712_REVISION), block.chainid, address(ghoToken) ) ); bytes32 result = ghoToken.DOMAIN_SEPARATOR(); assertEq(result, expected, 'Unexpected domain separator'); } function testDomainSeparatorNewChain() public { vm.chainId(31338); bytes32 EIP712_DOMAIN = keccak256( 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' ); bytes memory EIP712_REVISION = bytes('1'); bytes32 expected = keccak256( abi.encode( EIP712_DOMAIN, keccak256(bytes(ghoToken.name())), keccak256(EIP712_REVISION), block.chainid, address(ghoToken) ) ); bytes32 result = ghoToken.DOMAIN_SEPARATOR(); assertEq(result, expected, 'Unexpected domain separator'); } function testPermitAndVerifyNonce() public { (address david, uint256 davidKey) = makeAddrAndKey('david'); ghoFaucet(david, 1e18); bytes32 PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; bytes32 innerHash = keccak256(abi.encode(PERMIT_TYPEHASH, david, BOB, 1e18, 0, 1 hours)); bytes32 outerHash = keccak256( abi.encodePacked('\x19\x01', ghoToken.DOMAIN_SEPARATOR(), innerHash) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(davidKey, outerHash); ghoToken.permit(david, BOB, 1e18, 1 hours, v, r, s); assertEq(ghoToken.allowance(david, BOB), 1e18, 'Unexpected allowance'); assertEq(ghoToken.nonces(david), 1, 'Unexpected nonce'); } function testRevertPermitInvalidSignature() public { (, uint256 davidKey) = makeAddrAndKey('david'); bytes32 PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; bytes32 innerHash = keccak256(abi.encode(PERMIT_TYPEHASH, ALICE, BOB, 1e18, 0, 1 hours)); bytes32 outerHash = keccak256( abi.encodePacked('\x19\x01', ghoToken.DOMAIN_SEPARATOR(), innerHash) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(davidKey, outerHash); vm.expectRevert(bytes('INVALID_SIGNER')); ghoToken.permit(ALICE, BOB, 1e18, 1 hours, v, r, s); } function testRevertPermitInvalidDeadline() public { vm.expectRevert(bytes('PERMIT_DEADLINE_EXPIRED')); ghoToken.permit(ALICE, BOB, 1e18, block.timestamp - 1, 0, 0, 0); } } contract TestUpgradeableGhoTokenUpgrade is TestUpgradeableGhoTokenSetup { function testInitialization() public { // Upgradeability // version is 1st slot uint256 version = uint8(uint256(vm.load(address(ghoToken), bytes32(uint256(0))))); assertEq(version, 1); vm.prank(PROXY_ADMIN); (bool ok, bytes memory result) = address(ghoToken).staticcall( abi.encodeWithSelector(TransparentUpgradeableProxy.admin.selector) ); assertTrue(ok, 'proxy admin fetch failed'); address decodedProxyAdmin = abi.decode(result, (address)); assertEq(decodedProxyAdmin, PROXY_ADMIN, 'proxy admin is wrong'); assertEq(decodedProxyAdmin, getProxyAdminAddress(address(ghoToken)), 'proxy admin is wrong'); // Implementation vm.prank(PROXY_ADMIN); (ok, result) = address(ghoToken).staticcall( abi.encodeWithSelector(TransparentUpgradeableProxy.implementation.selector) ); assertTrue(ok, 'proxy implementation fetch failed'); address decodedImple = abi.decode(result, (address)); assertEq( decodedImple, getProxyImplementationAddress(address(ghoToken)), 'proxy implementation is wrong' ); assertEq(UpgradeableGhoToken(decodedImple).decimals(), 18, 'Wrong default ERC20 decimals'); vm.expectRevert('Initializable: contract is already initialized'); UpgradeableGhoToken(decodedImple).initialize(address(this)); // Proxy assertEq(ghoToken.name(), 'Gho Token', 'Wrong default ERC20 name'); assertEq(ghoToken.symbol(), 'GHO', 'Wrong default ERC20 symbol'); assertEq(ghoToken.decimals(), 18, 'Wrong default ERC20 decimals'); assertEq(ghoToken.totalSupply(), 0, 'Wrong total supply'); assertEq(ghoToken.getFacilitatorsList().length, 0, 'Facilitator list not empty'); } function testUpgrade() public { MockUpgradeable newImpl = new MockUpgradeable(); bytes memory mockImpleParams = abi.encodeWithSignature('initialize()'); vm.prank(PROXY_ADMIN); TransparentUpgradeableProxy(payable(address(ghoToken))).upgradeToAndCall( address(newImpl), mockImpleParams ); // version is 1st slot uint256 version = uint8(uint256(vm.load(address(ghoToken), bytes32(uint256(0))))); assertEq(version, 2); } function testRevertUpgradeUnauthorized() public { vm.expectRevert(); TransparentUpgradeableProxy(payable(address(ghoToken))).upgradeToAndCall(address(0), bytes('')); vm.expectRevert(); TransparentUpgradeableProxy(payable(address(ghoToken))).upgradeTo(address(0)); } function testChangeAdmin() public { assertEq(getProxyAdminAddress(address(ghoToken)), PROXY_ADMIN); address newAdmin = makeAddr('newAdmin'); vm.prank(PROXY_ADMIN); TransparentUpgradeableProxy(payable(address(ghoToken))).changeAdmin(newAdmin); assertEq(getProxyAdminAddress(address(ghoToken)), newAdmin, 'Admin change failed'); } function testChangeAdminUnauthorized() public { assertEq(getProxyAdminAddress(address(ghoToken)), PROXY_ADMIN); address newAdmin = makeAddr('newAdmin'); vm.expectRevert(); TransparentUpgradeableProxy(payable(address(ghoToken))).changeAdmin(newAdmin); assertEq(getProxyAdminAddress(address(ghoToken)), PROXY_ADMIN, 'Unauthorized admin change'); } } ================================================ FILE: src/test/TestZeroDiscountRateStrategy.t.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import './TestGhoBase.t.sol'; import {ZeroDiscountRateStrategy} from '../contracts/facilitators/aave/interestStrategy/ZeroDiscountRateStrategy.sol'; contract TestZeroDiscountRateStrategy is TestGhoBase { ZeroDiscountRateStrategy emptyStrategy; function setUp() public { emptyStrategy = new ZeroDiscountRateStrategy(); } function testFuzzRateAlwaysZero(uint256 debtBalance, uint256 discountTokenBalance) public { uint256 result = emptyStrategy.calculateDiscountRate(debtBalance, discountTokenBalance); assertEq(result, 0, 'Unexpected discount rate'); } } ================================================ FILE: src/test/helpers/Constants.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Constants { // ERC1967 slots bytes32 internal constant ERC1967_IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; bytes32 internal constant ERC1967_ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; // addresses expected for BGD stkAave address constant SHORT_EXECUTOR = 0xEE56e2B3D491590B5b31738cC34d5232F378a8D5; address constant STKAAVE_PROXY_ADMIN = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; // default admin role bytes32 public constant DEFAULT_ADMIN_ROLE = bytes32(0); // admin roles for GhoToken bytes32 public constant GHO_TOKEN_FACILITATOR_MANAGER_ROLE = keccak256('FACILITATOR_MANAGER_ROLE'); bytes32 public constant GHO_TOKEN_BUCKET_MANAGER_ROLE = keccak256('BUCKET_MANAGER_ROLE'); // admin role for GSM bytes32 public constant GSM_CONFIGURATOR_ROLE = keccak256('CONFIGURATOR_ROLE'); bytes32 public constant GSM_TOKEN_RESCUER_ROLE = keccak256('TOKEN_RESCUER_ROLE'); bytes32 public constant GSM_SWAP_FREEZER_ROLE = keccak256('SWAP_FREEZER_ROLE'); bytes32 public constant GSM_LIQUIDATOR_ROLE = keccak256('LIQUIDATOR_ROLE'); // signature typehash for GSM bytes32 public constant GSM_BUY_ASSET_WITH_SIG_TYPEHASH = keccak256( 'BuyAssetWithSig(address originator,uint256 minAmount,address receiver,uint256 nonce,uint256 deadline)' ); bytes32 public constant GSM_SELL_ASSET_WITH_SIG_TYPEHASH = keccak256( 'SellAssetWithSig(address originator,uint256 maxAmount,address receiver,uint256 nonce,uint256 deadline)' ); // defaults used in test environment uint256 constant DEFAULT_FLASH_FEE = 0.0009e4; // 0.09% uint128 constant DEFAULT_CAPACITY = 100_000_000e18; uint256 constant DEFAULT_BORROW_AMOUNT = 200e18; int256 constant DEFAULT_GHO_PRICE = 1e8; uint8 constant DEFAULT_ORACLE_DECIMALS = 8; uint256 constant DEFAULT_FIXED_PRICE = 1e18; uint256 constant DEFAULT_GSM_BUY_FEE = 0.1e4; // 10% uint256 constant DEFAULT_GSM_SELL_FEE = 0.1e4; // 10% uint128 constant DEFAULT_GSM_USDC_EXPOSURE = 100_000_000e6; // 6 decimals for USDC uint128 constant DEFAULT_GSM_USDC_AMOUNT = 100e6; // 6 decimals for USDC uint128 constant DEFAULT_GSM_GHO_AMOUNT = 100e18; // Gho Stewards uint32 constant GHO_BORROW_RATE_CHANGE_MAX = 0.05e4; uint256 constant GSM_FEE_RATE_CHANGE_MAX = 0.0050e4; uint256 constant MINIMUM_DELAY_V2 = 1 days; uint256 constant FIXED_RATE_STRATEGY_FACTORY_REVISION = 1; // sample users used across unit tests address constant ALICE = address(0x1111); address constant BOB = address(0x1112); address constant CHARLES = address(0x1113); address constant FAUCET = address(0x10001); address constant TREASURY = address(0x10002); address constant RISK_COUNCIL = address(0x10003); } ================================================ FILE: src/test/helpers/DebtUtils.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {SafeCast} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/SafeCast.sol'; import {WadRayMath} from '@aave/core-v3/contracts/protocol/libraries/math/WadRayMath.sol'; import {PercentageMath} from '@aave/core-v3/contracts/protocol/libraries/math/PercentageMath.sol'; library DebtUtils { using WadRayMath for uint256; using SafeCast for uint256; using PercentageMath for uint256; function test_coverage_ignore() public { // Intentionally left blank. // Excludes contract from coverage. } function computeDebt( uint256 userPreviousIndex, uint256 index, uint256 previousScaledBalance, uint256 accumulatedDebtInterest, uint256 discountPercent ) external pure returns (uint256, uint256, uint128) { uint256 balanceIncrease = previousScaledBalance.rayMul(index) - previousScaledBalance.rayMul(userPreviousIndex); uint256 discountScaled = 0; if (balanceIncrease != 0 && discountPercent != 0) { uint256 discount = balanceIncrease.percentMul(discountPercent); discountScaled = discount.rayDiv(index); balanceIncrease = balanceIncrease - discount; } uint128 accumulatedDebt = (balanceIncrease + accumulatedDebtInterest).toUint128(); return (balanceIncrease, discountScaled, accumulatedDebt); } } ================================================ FILE: src/test/helpers/ErrorsLib.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import '@openzeppelin/contracts/utils/Strings.sol'; library AccessControlErrorsLib { function MISSING_ROLE(bytes32 role, address account) external pure returns (bytes memory) { return abi.encodePacked( 'AccessControl: account ', Strings.toHexString(account), ' is missing role ', Strings.toHexString(uint256(role), 32) ); } function test_coverage_ignore() public { // Intentionally left blank. // Excludes contract from coverage. } } library OwnableErrorsLib { function CALLER_NOT_OWNER() external pure returns (bytes memory) { return abi.encodePacked('Ownable: caller is not the owner'); } function test_coverage_ignore() public { // Intentionally left blank. // Excludes contract from coverage. } } ================================================ FILE: src/test/helpers/Events.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Events { // core token events event Mint( address indexed caller, address indexed onBehalfOf, uint256 value, uint256 balanceIncrease, uint256 index ); event Burn( address indexed from, address indexed target, uint256 value, uint256 balanceIncrease, uint256 index ); event Transfer(address indexed from, address indexed to, uint256 value); // setter/updater methods event ATokenSet(address indexed); event VariableDebtTokenSet(address indexed variableDebtToken); event GhoTreasuryUpdated(address indexed oldGhoTreasury, address indexed newGhoTreasury); event DiscountPercentUpdated( address indexed user, uint256 oldDiscountPercent, uint256 indexed newDiscountPercent ); event DiscountRateStrategyUpdated( address indexed oldDiscountRateStrategy, address indexed newDiscountRateStrategy ); event ReserveInterestRateStrategyChanged( address indexed asset, address oldStrategy, address newStrategy ); // flashmint-related events event FlashMint( address indexed receiver, address indexed initiator, address asset, uint256 indexed amount, uint256 fee ); event FeeUpdated(uint256 oldFee, uint256 newFee); // facilitator-related events event FacilitatorAdded( address indexed facilitatorAddress, bytes32 indexed label, uint256 bucketCapacity ); event FacilitatorRemoved(address indexed facilitatorAddress); event FacilitatorBucketCapacityUpdated( address indexed facilitatorAddress, uint256 oldCapacity, uint256 newCapacity ); event FacilitatorBucketLevelUpdated( address indexed facilitatorAddress, uint256 oldLevel, uint256 newLevel ); // GSM events event BuyAsset( address indexed originator, address indexed receiver, uint256 underlyingAmount, uint256 ghoAmount, uint256 fee ); event SellAsset( address indexed originator, address indexed receiver, uint256 underlyingAmount, uint256 ghoAmount, uint256 fee ); event SwapFreeze(address indexed freezer, bool enabled); event Seized( address indexed seizer, address indexed recipient, uint256 underlyingAmount, uint256 ghoOutstanding ); event BurnAfterSeize(address indexed burner, uint256 amount, uint256 ghoOutstanding); event BackingProvided( address indexed backer, address indexed asset, uint256 amount, uint256 ghoAmount, uint256 remainingLoss ); event FeeStrategyUpdated(address indexed oldFeeStrategy, address indexed newFeeStrategy); event ExposureCapUpdated(uint256 oldExposureCap, uint256 newExposureCap); event TokensRescued( address indexed tokenRescued, address indexed recipient, uint256 amountRescued ); // IGhoFacilitator events event FeesDistributedToTreasury( address indexed ghoTreasury, address indexed asset, uint256 amount ); // FixedRateStrategyFactory event RateStrategyCreated(address indexed strategy, uint256 indexed rate); // IGsmRegistry events event GsmAdded(address indexed gsmAddress); event GsmRemoved(address indexed gsmAddress); // AccessControl event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); // Ownable event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); // Upgrades event Upgraded(address indexed implementation); } ================================================ FILE: src/test/mocks/MockAclManager.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract MockAclManager { bool state; constructor() { state = true; } function test_coverage_ignore() public virtual { // Intentionally left blank. // Excludes contract from coverage. } function setState(bool value) public { state = value; } function isPoolAdmin(address) public view returns (bool) { return state; } function isFlashBorrower(address) public view returns (bool) { return state; } } ================================================ FILE: src/test/mocks/MockAddressesProvider.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract MockAddressesProvider { address immutable ACL_MANAGER; address POOL; address POOL_CONFIGURATOR; address PRICE_ORACLE; constructor(address aclManager) { ACL_MANAGER = aclManager; } function test_coverage_ignore() public virtual { // Intentionally left blank. // Excludes contract from coverage. } function setPool(address pool) public { POOL = pool; } function setConfigurator(address configurator) public { POOL_CONFIGURATOR = configurator; } function setPriceOracle(address priceOracle) public { PRICE_ORACLE = priceOracle; } function getACLManager() public view returns (address) { return ACL_MANAGER; } function getPool() public view returns (address) { return POOL; } function getPoolConfigurator() public view returns (address) { return POOL_CONFIGURATOR; } function getPriceOracle() public view returns (address) { return PRICE_ORACLE; } } ================================================ FILE: src/test/mocks/MockConfigurator.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; import {DataTypes} from '@aave/core-v3/contracts/protocol/libraries/types/DataTypes.sol'; import {ReserveConfiguration} from '@aave/core-v3/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; import {DefaultReserveInterestRateStrategyV2} from '../../contracts/misc/dependencies/AaveV3-1.sol'; import {IDefaultInterestRateStrategyV2} from '../../contracts/misc/dependencies/AaveV3-1.sol'; contract MockConfigurator { using ReserveConfiguration for DataTypes.ReserveConfigurationMap; IPool internal _pool; event ReserveInterestRateStrategyChanged( address indexed asset, address oldStrategy, address newStrategy ); event BorrowCapChanged(address indexed asset, uint256 oldBorrowCap, uint256 newBorrowCap); event SupplyCapChanged(address indexed asset, uint256 oldSupplyCap, uint256 newSupplyCap); constructor(IPool pool) { _pool = pool; } function test_coverage_ignore() public virtual { // Intentionally left blank. // Excludes contract from coverage. } function setReserveInterestRateStrategyAddress( address asset, address newRateStrategyAddress ) external { DataTypes.ReserveData memory reserve = _pool.getReserveData(asset); address oldRateStrategyAddress = reserve.interestRateStrategyAddress; _pool.setReserveInterestRateStrategyAddress(asset, newRateStrategyAddress); emit ReserveInterestRateStrategyChanged(asset, oldRateStrategyAddress, newRateStrategyAddress); } function setReserveInterestRateParams( address asset, IDefaultInterestRateStrategyV2.InterestRateData calldata rateParams ) external { DataTypes.ReserveData memory reserve = _pool.getReserveData(asset); address rateStrategyAddress = reserve.interestRateStrategyAddress; DefaultReserveInterestRateStrategyV2(rateStrategyAddress).setInterestRateParams( asset, rateParams ); } function setReserveInterestRateData(address asset, bytes calldata rateData) external { this.setReserveInterestRateParams( asset, abi.decode(rateData, (IDefaultInterestRateStrategyV2.InterestRateData)) ); } function setReserveInterestRateStrategyAddress( address asset, address rateStrategyAddress, bytes calldata rateData ) external { this.setReserveInterestRateStrategyAddress(asset, rateStrategyAddress); this.setReserveInterestRateParams( asset, abi.decode(rateData, (IDefaultInterestRateStrategyV2.InterestRateData)) ); } function setBorrowCap(address asset, uint256 newBorrowCap) external { DataTypes.ReserveConfigurationMap memory currentConfig = _pool.getConfiguration(asset); uint256 oldBorrowCap = currentConfig.getBorrowCap(); currentConfig.setBorrowCap(newBorrowCap); _pool.setConfiguration(asset, currentConfig); emit BorrowCapChanged(asset, oldBorrowCap, newBorrowCap); } function setSupplyCap(address asset, uint256 newSupplyCap) external { DataTypes.ReserveConfigurationMap memory currentConfig = _pool.getConfiguration(asset); uint256 oldSupplyCap = currentConfig.getSupplyCap(); currentConfig.setSupplyCap(newSupplyCap); _pool.setConfiguration(asset, currentConfig); emit SupplyCapChanged(asset, oldSupplyCap, newSupplyCap); } } ================================================ FILE: src/test/mocks/MockERC4626.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {ERC4626} from '@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol'; import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; contract MockERC4626 is ERC4626 { constructor( string memory name, string memory symbol, address asset ) ERC4626(IERC20(asset)) ERC20(name, symbol) {} } ================================================ FILE: src/test/mocks/MockFlashBorrower.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {IERC3156FlashBorrower} from '@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol'; import {IERC3156FlashLender} from '@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol'; import {IGhoToken} from '../../contracts/gho/interfaces/IGhoToken.sol'; /** * @title MockFlashBorrower * @author Aave * @dev This is purely an unsafe mock testing contract. Do not use in production. */ contract MockFlashBorrower is IERC3156FlashBorrower { enum Action { NORMAL, OTHER } IERC3156FlashLender private _lender; bool private _allowRepayment; bool private _allowCallback; constructor(IERC3156FlashLender lender) { _lender = lender; _allowRepayment = true; _allowCallback = true; } function test_coverage_ignore() public virtual { // Intentionally left blank. // Excludes contract from coverage. } /// @dev ERC-3156 Flash loan callback function onFlashLoan( address initiator, address token, uint256 amount, uint256 fee, bytes calldata data ) external override returns (bytes32) { require(msg.sender == address(_lender), 'FlashBorrower: Untrusted lender'); require(initiator == address(this), 'FlashBorrower: Untrusted loan initiator'); Action action = abi.decode(data, (Action)); if (action == Action.NORMAL) { // Intentionally left blank. } else if (action == Action.OTHER) { // Tests capacity change mid-flashmint. require( _lender.flashFee(token, type(uint128).max) == 0, 'FlashBorrower: Non-zero flashfee during capacity change test' ); (uint256 capacityBefore, ) = IGhoToken(token).getFacilitatorBucket(address(_lender)); require(capacityBefore != 0, 'FlashBorrower: Zero bucket capacity before setting'); IGhoToken(token).setFacilitatorBucketCapacity(address(_lender), 0); (uint256 capacityAfter, ) = IGhoToken(token).getFacilitatorBucket(address(_lender)); require(capacityAfter == 0, 'FlashBorrower: Non-zero bucket capacity after setting'); require( _lender.maxFlashLoan(token) == 0, 'FlashBorrower: Non-zero max flashloan at capacity < level' ); } // Repayment if (_allowRepayment) { IERC20(token).approve(address(_lender), amount + fee); } return _allowCallback ? keccak256('ERC3156FlashBorrower.onFlashLoan') : keccak256('arbitrary'); } /// @dev Initiate a flash loan function flashBorrow(address token, uint256 amount) public { bytes memory data = abi.encode(Action.NORMAL); _lender.flashLoan(this, token, amount, data); } function flashBorrowOtherActionMax(address token) public { bytes memory data = abi.encode(Action.OTHER); uint256 amount = _lender.maxFlashLoan(token); _lender.flashLoan(this, token, amount, data); } function setAllowRepayment(bool active) public { _allowRepayment = active; } function setAllowCallback(bool active) public { _allowCallback = active; } } ================================================ FILE: src/test/mocks/MockGsmV2.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {Gsm} from '../../contracts/facilitators/gsm/Gsm.sol'; /** * @dev Mock contract to test GSM upgrades, not to be used in production. */ contract MockGsmV2 is Gsm { /** * @dev Constructor * @param ghoToken The address of the GHO token contract * @param underlyingAsset The address of the collateral asset * @param priceStrategy The address of the price strategy */ constructor( address ghoToken, address underlyingAsset, address priceStrategy ) Gsm(ghoToken, underlyingAsset, priceStrategy) { // Intentionally left blank } function test_coverage_ignore() public virtual { // Intentionally left blank. // Excludes contract from coverage. } function initialize() external initializer { // Intentionally left blank } function GSM_REVISION() public pure virtual override returns (uint256) { return 2; } } ================================================ FILE: src/test/mocks/MockPool.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {GhoVariableDebtToken} from '../../contracts/facilitators/aave/tokens/GhoVariableDebtToken.sol'; import {GhoAToken} from '../../contracts/facilitators/aave/tokens/GhoAToken.sol'; import {IGhoToken} from '../../contracts/gho/interfaces/IGhoToken.sol'; import {GhoDiscountRateStrategy} from '../../contracts/facilitators/aave/interestStrategy/GhoDiscountRateStrategy.sol'; import {GhoInterestRateStrategy} from '../../contracts/facilitators/aave/interestStrategy/GhoInterestRateStrategy.sol'; import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; import {IAaveIncentivesController} from '@aave/core-v3/contracts/interfaces/IAaveIncentivesController.sol'; import {Pool} from '@aave/core-v3/contracts/protocol/pool/Pool.sol'; import {UserConfiguration} from '@aave/core-v3/contracts/protocol/libraries/configuration/UserConfiguration.sol'; import {ReserveConfiguration} from '@aave/core-v3/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; import {ReserveLogic} from '@aave/core-v3/contracts/protocol/libraries/logic/ReserveLogic.sol'; import {Helpers} from '@aave/core-v3/contracts/protocol/libraries/helpers/Helpers.sol'; import {DataTypes} from '@aave/core-v3/contracts/protocol/libraries/types/DataTypes.sol'; import {StableDebtToken} from '@aave/core-v3/contracts/protocol/tokenization/StableDebtToken.sol'; import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/ERC20.sol'; import {Errors} from '@aave/core-v3/contracts/protocol/libraries/helpers/Errors.sol'; /** * @dev MockPool removes assets and users validations from Pool contract. */ contract MockPool is Pool { using ReserveLogic for DataTypes.ReserveCache; using ReserveLogic for DataTypes.ReserveData; using UserConfiguration for DataTypes.UserConfigurationMap; using ReserveConfiguration for DataTypes.ReserveConfigurationMap; GhoVariableDebtToken public DEBT_TOKEN; GhoAToken public ATOKEN; address public GHO; constructor(IPoolAddressesProvider provider) Pool(provider) {} function test_coverage_ignore() public virtual { // Intentionally left blank. // Excludes contract from coverage. } function setGhoTokens(GhoVariableDebtToken ghoDebtToken, GhoAToken ghoAToken) external { DEBT_TOKEN = ghoDebtToken; ATOKEN = ghoAToken; GHO = ghoAToken.UNDERLYING_ASSET_ADDRESS(); _reserves[GHO].init( address(ATOKEN), address(new StableDebtToken(IPool(address(this)))), address(DEBT_TOKEN), address(new GhoInterestRateStrategy(address(0), 2e25)) ); } function supply( address asset, uint256 amount, address onBehalfOf, uint16 referralCode ) public override(Pool) {} function borrow( address, // asset uint256 amount, uint256, // interestRateMode uint16, // referralCode address onBehalfOf ) public override(Pool) { DataTypes.ReserveData storage reserve = _reserves[GHO]; DataTypes.ReserveCache memory reserveCache = reserve.cache(); reserve.updateState(reserveCache); DEBT_TOKEN.mint(msg.sender, onBehalfOf, amount, reserveCache.nextVariableBorrowIndex); reserve.updateInterestRates(reserveCache, GHO, 0, amount); ATOKEN.transferUnderlyingTo(onBehalfOf, amount); } function repay( address, // asset uint256 amount, uint256, // interestRateMode address onBehalfOf ) public override(Pool) returns (uint256) { DataTypes.ReserveData storage reserve = _reserves[GHO]; DataTypes.ReserveCache memory reserveCache = reserve.cache(); reserve.updateState(reserveCache); uint256 paybackAmount = DEBT_TOKEN.balanceOf(onBehalfOf); if (amount < paybackAmount) { paybackAmount = amount; } DEBT_TOKEN.burn(onBehalfOf, paybackAmount, reserveCache.nextVariableBorrowIndex); reserve.updateInterestRates(reserveCache, GHO, 0, amount); IERC20(GHO).transferFrom(msg.sender, reserveCache.aTokenAddress, paybackAmount); ATOKEN.handleRepayment(msg.sender, onBehalfOf, paybackAmount); return paybackAmount; } function setReserveInterestRateStrategyAddress( address asset, address rateStrategyAddress ) external override { require(asset != address(0), Errors.ZERO_ADDRESS_NOT_VALID); _reserves[asset].interestRateStrategyAddress = rateStrategyAddress; } function getReserveInterestRateStrategyAddress(address asset) public view returns (address) { return _reserves[asset].interestRateStrategyAddress; } function setConfiguration( address asset, DataTypes.ReserveConfigurationMap calldata configuration ) external override { require(asset != address(0), Errors.ZERO_ADDRESS_NOT_VALID); _reserves[asset].configuration = configuration; } } ================================================ FILE: src/test/mocks/MockPoolDataProvider.sol ================================================ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.0; import {IPoolDataProvider} from '@aave/core-v3/contracts/interfaces/IPoolDataProvider.sol'; import {IPoolAddressesProvider} from '@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol'; import {DataTypes} from '@aave/core-v3/contracts/protocol/libraries/types/DataTypes.sol'; import {ReserveConfiguration} from '@aave/core-v3/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; import {IPool} from '@aave/core-v3/contracts/interfaces/IPool.sol'; contract MockPoolDataProvider is IPoolDataProvider { using ReserveConfiguration for DataTypes.ReserveConfigurationMap; IPoolAddressesProvider public immutable POOL_ADDRESSES_PROVIDER; function ADDRESSES_PROVIDER() external view returns (IPoolAddressesProvider) { return POOL_ADDRESSES_PROVIDER; } constructor(address addressesProvider) { POOL_ADDRESSES_PROVIDER = IPoolAddressesProvider(addressesProvider); } function getInterestRateStrategyAddress(address asset) external view returns (address) { DataTypes.ReserveData memory reserveData = IPool( IPoolAddressesProvider(POOL_ADDRESSES_PROVIDER).getPool() ).getReserveData(asset); return reserveData.interestRateStrategyAddress; } function getATokenTotalSupply(address asset) external view returns (uint256) { return 0; } function getAllATokens() external view returns (TokenData[] memory) { return new TokenData[](0); } function getAllReservesTokens() external view returns (TokenData[] memory) { return new TokenData[](0); } function getDebtCeiling(address asset) external view returns (uint256) { return 0; } function getDebtCeilingDecimals() external pure returns (uint256) { return 0; } function getFlashLoanEnabled(address asset) external view returns (bool) { return false; } function getLiquidationProtocolFee(address asset) external view returns (uint256) { return 0; } function getPaused(address asset) external view returns (bool isPaused) { return false; } function getReserveCaps( address asset ) external view returns (uint256 borrowCap, uint256 supplyCap) { return (0, 0); } function getReserveConfigurationData( address asset ) external view returns ( uint256 decimals, uint256 ltv, uint256 liquidationThreshold, uint256 liquidationBonus, uint256 reserveFactor, bool usageAsCollateralEnabled, bool borrowingEnabled, bool stableBorrowRateEnabled, bool isActive, bool isFrozen ) { return (0, 0, 0, 0, 0, false, false, false, false, false); } function getReserveData( address asset ) external view returns ( uint256 unbacked, uint256 accruedToTreasuryScaled, uint256 totalAToken, uint256 totalStableDebt, uint256 totalVariableDebt, uint256 liquidityRate, uint256 variableBorrowRate, uint256 stableBorrowRate, uint256 averageStableBorrowRate, uint256 liquidityIndex, uint256 variableBorrowIndex, uint40 lastUpdateTimestamp ) { return (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); } function getReserveEModeCategory(address asset) external view returns (uint256) { return 0; } function getReserveTokensAddresses( address asset ) external view returns ( address aTokenAddress, address stableDebtTokenAddress, address variableDebtTokenAddress ) { return (address(0), address(0), address(0)); } function getSiloedBorrowing(address asset) external view returns (bool) { return false; } function getTotalDebt(address asset) external view returns (uint256) { return 0; } function getUnbackedMintCap(address asset) external view returns (uint256) { return 0; } function getUserReserveData( address asset, address user ) external view returns ( uint256 currentATokenBalance, uint256 currentStableDebt, uint256 currentVariableDebt, uint256 principalStableDebt, uint256 scaledVariableDebt, uint256 stableBorrowRate, uint256 liquidityRate, uint40 stableRateLastUpdated, bool usageAsCollateralEnabled ) { return (0, 0, 0, 0, 0, 0, 0, 0, false); } } ================================================ FILE: src/test/mocks/MockUpgradeable.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol'; /** * @dev Mock contract to test upgrades, not to be used in production. */ contract MockUpgradeable is Initializable { /** * @dev Constructor */ constructor() { // Intentionally left bank } /** * @dev Initializer */ function initialize() public reinitializer(2) { // Intentionally left bank } } ================================================ FILE: src/test/mocks/MockUpgradeableBurnMintTokenPool.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol'; import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; import {RateLimiter} from 'src/contracts/misc/dependencies/Ccip.sol'; import {IRouter} from 'src/contracts/misc/dependencies/Ccip.sol'; import {IARM} from 'src/contracts/misc/dependencies/AaveV3-1.sol'; contract MockUpgradeableBurnMintTokenPool is Initializable { using SafeERC20 for IERC20; using RateLimiter for RateLimiter.TokenBucket; error Unauthorized(address caller); error ZeroAddressNotAllowed(); event ChainConfigured( uint64 remoteChainSelector, RateLimiter.Config outboundRateLimiterConfig, RateLimiter.Config inboundRateLimiterConfig ); struct ChainUpdate { uint64 remoteChainSelector; bool allowed; RateLimiter.Config outboundRateLimiterConfig; RateLimiter.Config inboundRateLimiterConfig; } address internal _owner; bool internal immutable i_acceptLiquidity; address internal s_rateLimitAdmin; uint256 private s_bridgeLimit; address internal s_bridgeLimitAdmin; IERC20 internal immutable i_token; address internal immutable i_armProxy; bool internal immutable i_allowlistEnabled; EnumerableSet.AddressSet internal s_allowList; IRouter internal s_router; EnumerableSet.UintSet internal s_remoteChainSelectors; mapping(uint64 => RateLimiter.TokenBucket) internal s_outboundRateLimits; mapping(uint64 => RateLimiter.TokenBucket) internal s_inboundRateLimits; constructor(address token, address armProxy, bool allowlistEnabled, bool acceptLiquidity) { i_acceptLiquidity = acceptLiquidity; if (address(token) == address(0)) revert ZeroAddressNotAllowed(); i_token = IERC20(token); i_armProxy = armProxy; i_allowlistEnabled = allowlistEnabled; } function initialize( address owner, address[] memory allowlist, address router, uint256 bridgeLimit ) public virtual initializer { allowlist; if (owner == address(0)) revert ZeroAddressNotAllowed(); if (router == address(0)) revert ZeroAddressNotAllowed(); _transferOwnership(owner); s_router = IRouter(router); s_bridgeLimit = bridgeLimit; } function owner() public view returns (address) { return _owner; } function acceptOwnership() external {} function setRateLimitAdmin(address rateLimitAdmin) external { s_rateLimitAdmin = rateLimitAdmin; } function setBridgeLimit(uint256 newBridgeLimit) external { if (msg.sender != s_bridgeLimitAdmin && msg.sender != owner()) revert Unauthorized(msg.sender); s_bridgeLimit = newBridgeLimit; } function setBridgeLimitAdmin(address bridgeLimitAdmin) external { s_bridgeLimitAdmin = bridgeLimitAdmin; } function getBridgeLimit() external view virtual returns (uint256) { return s_bridgeLimit; } function getRateLimitAdmin() external view returns (address) { return s_rateLimitAdmin; } function getBridgeLimitAdmin() external view returns (address) { return s_bridgeLimitAdmin; } function setChainRateLimiterConfig( uint64 remoteChainSelector, RateLimiter.Config memory outboundConfig, RateLimiter.Config memory inboundConfig ) external { if (msg.sender != s_rateLimitAdmin && msg.sender != owner()) revert Unauthorized(msg.sender); _setRateLimitConfig(remoteChainSelector, outboundConfig, inboundConfig); } function _setRateLimitConfig( uint64 remoteChainSelector, RateLimiter.Config memory outboundConfig, RateLimiter.Config memory inboundConfig ) internal { RateLimiter._validateTokenBucketConfig(outboundConfig, false); s_outboundRateLimits[remoteChainSelector]._setTokenBucketConfig(outboundConfig); RateLimiter._validateTokenBucketConfig(inboundConfig, false); s_inboundRateLimits[remoteChainSelector]._setTokenBucketConfig(inboundConfig); emit ChainConfigured(remoteChainSelector, outboundConfig, inboundConfig); } function getCurrentOutboundRateLimiterState( uint64 remoteChainSelector ) external view returns (RateLimiter.TokenBucket memory) { return s_outboundRateLimits[remoteChainSelector]._currentTokenBucketState(); } function getCurrentInboundRateLimiterState( uint64 remoteChainSelector ) external view returns (RateLimiter.TokenBucket memory) { return s_inboundRateLimits[remoteChainSelector]._currentTokenBucketState(); } function applyChainUpdates(ChainUpdate[] calldata chains) external virtual { for (uint256 i = 0; i < chains.length; ++i) { ChainUpdate memory update = chains[i]; s_outboundRateLimits[update.remoteChainSelector] = RateLimiter.TokenBucket({ rate: update.outboundRateLimiterConfig.rate, capacity: update.outboundRateLimiterConfig.capacity, tokens: update.outboundRateLimiterConfig.capacity, lastUpdated: uint32(block.timestamp), isEnabled: update.outboundRateLimiterConfig.isEnabled }); s_inboundRateLimits[update.remoteChainSelector] = RateLimiter.TokenBucket({ rate: update.inboundRateLimiterConfig.rate, capacity: update.inboundRateLimiterConfig.capacity, tokens: update.inboundRateLimiterConfig.capacity, lastUpdated: uint32(block.timestamp), isEnabled: update.inboundRateLimiterConfig.isEnabled }); } } function _transferOwnership(address newOwner) internal { _owner = newOwner; } } ================================================ FILE: src/test/mocks/MockUpgradeableLockReleaseTokenPool.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol'; import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; import {RateLimiter} from 'src/contracts/misc/dependencies/Ccip.sol'; import {IRouter} from 'src/contracts/misc/dependencies/Ccip.sol'; import {IARM} from 'src/contracts/misc/dependencies/AaveV3-1.sol'; contract MockUpgradeableLockReleaseTokenPool is Initializable { using SafeERC20 for IERC20; using RateLimiter for RateLimiter.TokenBucket; error Unauthorized(address caller); error ZeroAddressNotAllowed(); event ChainConfigured( uint64 remoteChainSelector, RateLimiter.Config outboundRateLimiterConfig, RateLimiter.Config inboundRateLimiterConfig ); struct ChainUpdate { uint64 remoteChainSelector; bool allowed; RateLimiter.Config outboundRateLimiterConfig; RateLimiter.Config inboundRateLimiterConfig; } address internal _owner; bool internal immutable i_acceptLiquidity; address internal s_rateLimitAdmin; uint256 private s_bridgeLimit; address internal s_bridgeLimitAdmin; IERC20 internal immutable i_token; address internal immutable i_armProxy; bool internal immutable i_allowlistEnabled; EnumerableSet.AddressSet internal s_allowList; IRouter internal s_router; EnumerableSet.UintSet internal s_remoteChainSelectors; mapping(uint64 => RateLimiter.TokenBucket) internal s_outboundRateLimits; mapping(uint64 => RateLimiter.TokenBucket) internal s_inboundRateLimits; constructor(address token, address armProxy, bool allowlistEnabled, bool acceptLiquidity) { i_acceptLiquidity = acceptLiquidity; if (address(token) == address(0)) revert ZeroAddressNotAllowed(); i_token = IERC20(token); i_armProxy = armProxy; i_allowlistEnabled = allowlistEnabled; } function initialize( address owner, address[] memory allowlist, address router, uint256 bridgeLimit ) public virtual initializer { allowlist; if (owner == address(0)) revert ZeroAddressNotAllowed(); if (router == address(0)) revert ZeroAddressNotAllowed(); _transferOwnership(owner); s_router = IRouter(router); s_bridgeLimit = bridgeLimit; } function owner() public view returns (address) { return _owner; } function acceptOwnership() external {} function setRateLimitAdmin(address rateLimitAdmin) external { s_rateLimitAdmin = rateLimitAdmin; } function setBridgeLimit(uint256 newBridgeLimit) external { if (msg.sender != s_bridgeLimitAdmin && msg.sender != owner()) revert Unauthorized(msg.sender); s_bridgeLimit = newBridgeLimit; } function setBridgeLimitAdmin(address bridgeLimitAdmin) external { s_bridgeLimitAdmin = bridgeLimitAdmin; } function getBridgeLimit() external view virtual returns (uint256) { return s_bridgeLimit; } function getRateLimitAdmin() external view returns (address) { return s_rateLimitAdmin; } function getBridgeLimitAdmin() external view returns (address) { return s_bridgeLimitAdmin; } function setChainRateLimiterConfig( uint64 remoteChainSelector, RateLimiter.Config memory outboundConfig, RateLimiter.Config memory inboundConfig ) external { if (msg.sender != s_rateLimitAdmin && msg.sender != owner()) revert Unauthorized(msg.sender); _setRateLimitConfig(remoteChainSelector, outboundConfig, inboundConfig); } function _setRateLimitConfig( uint64 remoteChainSelector, RateLimiter.Config memory outboundConfig, RateLimiter.Config memory inboundConfig ) internal { RateLimiter._validateTokenBucketConfig(outboundConfig, false); s_outboundRateLimits[remoteChainSelector]._setTokenBucketConfig(outboundConfig); RateLimiter._validateTokenBucketConfig(inboundConfig, false); s_inboundRateLimits[remoteChainSelector]._setTokenBucketConfig(inboundConfig); emit ChainConfigured(remoteChainSelector, outboundConfig, inboundConfig); } function getCurrentOutboundRateLimiterState( uint64 remoteChainSelector ) external view returns (RateLimiter.TokenBucket memory) { return s_outboundRateLimits[remoteChainSelector]._currentTokenBucketState(); } function getCurrentInboundRateLimiterState( uint64 remoteChainSelector ) external view returns (RateLimiter.TokenBucket memory) { return s_inboundRateLimits[remoteChainSelector]._currentTokenBucketState(); } function applyChainUpdates(ChainUpdate[] calldata chains) external virtual { for (uint256 i = 0; i < chains.length; ++i) { ChainUpdate memory update = chains[i]; s_outboundRateLimits[update.remoteChainSelector] = RateLimiter.TokenBucket({ rate: update.outboundRateLimiterConfig.rate, capacity: update.outboundRateLimiterConfig.capacity, tokens: update.outboundRateLimiterConfig.capacity, lastUpdated: uint32(block.timestamp), isEnabled: update.outboundRateLimiterConfig.isEnabled }); s_inboundRateLimits[update.remoteChainSelector] = RateLimiter.TokenBucket({ rate: update.inboundRateLimiterConfig.rate, capacity: update.inboundRateLimiterConfig.capacity, tokens: update.inboundRateLimiterConfig.capacity, lastUpdated: uint32(block.timestamp), isEnabled: update.inboundRateLimiterConfig.isEnabled }); } } function _transferOwnership(address newOwner) internal { _owner = newOwner; } } ================================================ FILE: tasks/main/deploy-and-setup.ts ================================================ import { task } from 'hardhat/config'; task('deploy-and-setup', 'Deploy fresh 3.0.1 market instance and setup GHO').setAction( async (_, hre) => { console.log('BlockNumber:', (await hre.ethers.provider.getBlockNumber()).toString()); console.log('Network:', await hre.ethers.provider.getNetwork()); await hre.run('deploy', { tags: 'market,periphery-post,after-deploy,full_gho_deploy', noCompile: true, }); await hre.run('gho-testnet-setup'); } ); ================================================ FILE: tasks/main/gho-testnet-setup.ts ================================================ import { task } from 'hardhat/config'; /** NOTICE: This task covers the testnet deployment environment */ task('gho-testnet-setup', 'Deploy and Configure Gho').setAction(async (params, hre) => { /***************************************** * INITIALIZE RESERVE * ******************************************/ blankSpace(); await hre.run('initialize-gho-reserve'); /***************************************** * CONFIGURE RESERVE * * 1. enable borrowing * * 2. configure oracle * ******************************************/ blankSpace(); await hre.run('enable-gho-borrowing'); blankSpace(); await hre.run('set-gho-oracle'); /****************************************** * CONFIGURE GHO * * 1. Add aave as a GHO entity * * 2. Add flashminter as GHO entity * * 3. Set addresses in AToken and VDebt * ******************************************/ blankSpace(); blankSpace(); await hre.run('add-gho-as-entity'); blankSpace(); await hre.run('add-gho-flashminter-as-entity'); blankSpace(); await hre.run('set-gho-addresses'); /***************************************** * UPDATE StkAave * ******************************************/ blankSpace(); await hre.run('upgrade-stkAave'); blankSpace(); await hre.run('print-all-deployments'); }); const blankSpace = () => { console.log(); }; ================================================ FILE: tasks/misc/network-check.ts ================================================ import { formatEther } from 'ethers/lib/utils'; import { task } from 'hardhat/config'; task(`network-check`, `Check network block and deployment account`).setAction(async (_, hre) => { const [_deployer] = await hre.ethers.getSigners(); const deployerAddress = await _deployer.getAddress(); const deployerBalance = await hre.ethers.provider.getBalance(deployerAddress); console.log(`Network: ${hre.network.name}`); console.log(`Block Number: ${await hre.ethers.provider.getBlockNumber()}`); console.log(`Deployer: ${deployerAddress}`); console.log(`Balance: ${formatEther(deployerBalance)}`); }); ================================================ FILE: tasks/misc/print-all-deployments.ts ================================================ import { getWalletBalances } from '@aave/deploy-v3'; import { task } from 'hardhat/config'; task(`print-all-deployments`).setAction(async (_, { deployments, getNamedAccounts, ...hre }) => { const allDeployments = await deployments.all(); let formattedDeployments: { [k: string]: { address: string } } = {}; let mintableTokens: { [k: string]: { address: string } } = {}; console.log('\nAccounts after deployment'); console.log('========'); console.table(await getWalletBalances()); // Print deployed contracts console.log('\nDeployments'); console.log('==========='); Object.keys(allDeployments).forEach((key) => { if (!key.includes('Mintable')) { formattedDeployments[key] = { address: allDeployments[key].address, }; } }); console.table(formattedDeployments); // Print Mintable Reserves and Rewards Object.keys(allDeployments).forEach((key) => { if (key.includes('Mintable')) { mintableTokens[key] = { address: allDeployments[key].address, }; } }); mintableTokens['GhoToken'] = { address: allDeployments['GhoToken'].address }; console.log('Reserves'); console.table(mintableTokens); }); ================================================ FILE: tasks/roles/00_gho-transfer-ownership.ts ================================================ import { expect } from 'chai'; import { GhoToken } from './../../../types/src/contracts/gho/GhoToken'; import { task } from 'hardhat/config'; task('gho-transfer-ownership', 'Transfer Ownership of Gho') .addParam('newOwner') .setAction(async ({ newOwner }, hre) => { const DEFAULT_ADMIN_ROLE = hre.ethers.utils.hexZeroPad('0x00', 32); const gho = (await hre.ethers.getContract('GhoToken')) as GhoToken; const grantAdminRoleTx = await gho.grantRole(DEFAULT_ADMIN_ROLE, newOwner); await expect(grantAdminRoleTx).to.emit(gho, 'RoleGranted'); const removeAdminRoleTx = await gho.renounceRole(DEFAULT_ADMIN_ROLE, users[0].address); console.log(`GHO ownership transferred to: ${newOwner}`); }); ================================================ FILE: tasks/testnet-setup/00_initialize-gho-reserve.ts ================================================ import { task } from 'hardhat/config'; import { ghoTokenConfig } from '../../helpers/config'; import { getPoolConfiguratorProxy, INCENTIVES_PROXY_ID, TREASURY_PROXY_ID } from '@aave/deploy-v3'; import { ConfiguratorInputTypes } from '@aave/deploy-v3/dist/types/typechain/@aave/core-v3/contracts/interfaces/IPoolConfigurator'; const printReserveInfo = (initReserveEvent) => { console.log(`Gho Reserve Initialized`); console.log(`\tasset: ${initReserveEvent.args.asset}`); console.log(`\tghoAToken: ${initReserveEvent.args.aToken}`); console.log(`\tstableDebtToken ${initReserveEvent.args.stableDebtToken}`); console.log(`\tghoVariableDebtToken ${initReserveEvent.args.variableDebtToken}`); console.log( `\tinterestRateStrategyAddress ${initReserveEvent.args.interestRateStrategyAddress}` ); }; task('initialize-gho-reserve', 'Initialize Gho Reserve').setAction(async (_, hre) => { const { ethers } = hre; const [_deployer] = await hre.ethers.getSigners(); const ghoATokenImplementation = await ethers.getContract('GhoAToken'); const stableDebtTokenImplementation = await ethers.getContract('GhoStableDebtToken'); const ghoVariableDebtTokenImplementation = await ethers.getContract('GhoVariableDebtToken'); const ghoInterestRateStrategy = await ethers.getContract('GhoInterestRateStrategy'); const ghoToken = await ethers.getContract('GhoToken'); ghoToken.grantRole(ghoToken.FACILITATOR_MANAGER_ROLE(), _deployer.address); ghoToken.grantRole(ghoToken.BUCKET_MANAGER_ROLE(), _deployer.address); const poolConfigurator = await getPoolConfiguratorProxy(); const treasuryAddress = (await hre.deployments.get(TREASURY_PROXY_ID)).address; const incentivesControllerAddress = (await hre.deployments.get(INCENTIVES_PROXY_ID)).address; const reserveInput: ConfiguratorInputTypes.InitReserveInputStruct = { aTokenImpl: ghoATokenImplementation.address, stableDebtTokenImpl: stableDebtTokenImplementation.address, variableDebtTokenImpl: ghoVariableDebtTokenImplementation.address, underlyingAssetDecimals: ghoTokenConfig.TOKEN_DECIMALS, interestRateStrategyAddress: ghoInterestRateStrategy.address, underlyingAsset: ghoToken.address, treasury: treasuryAddress, incentivesController: incentivesControllerAddress, aTokenName: `Aave Ethereum GHO`, aTokenSymbol: `aEthGHO`, variableDebtTokenName: `Aave Variable Debt Ethereum GHO`, variableDebtTokenSymbol: `variableDebtEthGHO`, stableDebtTokenName: 'Aave Stable Debt Ethereum GHO', stableDebtTokenSymbol: 'stableDebtEthGHO', params: '0x10', }; // Init reserve const initReserveTx = await poolConfigurator.initReserves([reserveInput]); const initReserveTxReceipt = await initReserveTx.wait(); const initReserveEvent = initReserveTxReceipt.events?.find( (e) => e.event === 'ReserveInitialized' ); if (initReserveEvent) { printReserveInfo(initReserveEvent); } else { throw new Error( `Missing ReserveInitialized event at initReserves in GHO init, check tx: ${initReserveTxReceipt.transactionHash}` ); } }); ================================================ FILE: tasks/testnet-setup/01_enable-gho-borrowing.ts ================================================ import { task } from 'hardhat/config'; import { getPoolConfiguratorProxy } from '@aave/deploy-v3'; task('enable-gho-borrowing', 'Enable variable borrowing on GHO').setAction(async (_, hre) => { const { ethers } = hre; const gho = await ethers.getContract('GhoToken'); const poolConfigurator = await getPoolConfiguratorProxy(); const enableBorrowingTx = await poolConfigurator.setReserveBorrowing(gho.address, true); const enableBorrowingTxReceipt = await enableBorrowingTx.wait(); const borrowingEnabledEvent = enableBorrowingTxReceipt.events?.find( (e) => e.event === 'ReserveBorrowing' ); if (borrowingEnabledEvent?.args) { const { enabled, asset } = borrowingEnabledEvent.args; console.log(`Borrowing set to ${enabled} on asset: \n\t${asset}}`); } else { throw new Error( `Error at gho borrowing initialization. Check tx: ${enableBorrowingTxReceipt.transactionHash}` ); } }); ================================================ FILE: tasks/testnet-setup/02_set-gho-oracle.ts ================================================ import { task } from 'hardhat/config'; import { getAaveOracle } from '@aave/deploy-v3'; task('set-gho-oracle', 'Set oracle for gho in Aave Oracle').setAction(async (_, hre) => { const { ethers } = hre; const gho = await ethers.getContract('GhoToken'); const ghoOracle = await ethers.getContract('GhoOracle'); const aaveOracle = await getAaveOracle(); const setSourcesTx = await aaveOracle.setAssetSources([gho.address], [ghoOracle.address]); const setSourcesTxReceipt = await setSourcesTx.wait(); const assetSourceUpdate = setSourcesTxReceipt.events?.find( (e) => e.event === 'AssetSourceUpdated' ); if (assetSourceUpdate?.args) { const { source, asset } = assetSourceUpdate.args; console.log(`Source set to: ${source} for asset ${asset}`); } else { throw new Error(`Error at oracle setup, check tx: ${setSourcesTxReceipt.transactionHash}`); } }); ================================================ FILE: tasks/testnet-setup/03_add-gho-as-entity.ts ================================================ import { task } from 'hardhat/config'; import { ghoEntityConfig } from '../../helpers/config'; import { getAaveProtocolDataProvider } from '@aave/deploy-v3'; import { GhoToken } from '../../types'; task('add-gho-as-entity', 'Adds Aave as a gho entity').setAction(async (_, hre) => { const { ethers } = hre; const gho = (await ethers.getContract('GhoToken')) as GhoToken; const aaveDataProvider = await getAaveProtocolDataProvider(); const tokenProxyAddresses = await aaveDataProvider.getReserveTokensAddresses(gho.address); const [deployer] = await hre.ethers.getSigners(); const addEntityTx = await gho .connect(deployer) .addFacilitator( tokenProxyAddresses.aTokenAddress, ghoEntityConfig.label, ghoEntityConfig.mintLimit ); const addEntityTxReceipt = await addEntityTx.wait(); const newEntityEvents = addEntityTxReceipt.events?.find((e) => e.event === 'FacilitatorAdded'); if (newEntityEvents?.args) { console.log(`Address added as a facilitator: ${JSON.stringify(newEntityEvents.args[0])}`); } else { throw new Error( `Error when adding facilitator. Check tx ${addEntityTxReceipt.transactionHash}` ); } }); ================================================ FILE: tasks/testnet-setup/04_add-gho-flashminter-as-entity.ts ================================================ import { GhoFlashMinter } from '../../../types/src/contracts/facilitators/flashMinter/GhoFlashMinter'; import { GhoToken } from '../../../types/src/contracts/gho/GhoToken'; import { task } from 'hardhat/config'; import { ghoEntityConfig } from '../../helpers/config'; task('add-gho-flashminter-as-entity', 'Adds FlashMinter as a gho entity').setAction( async (_, hre) => { const { ethers } = hre; const gho = (await ethers.getContract('GhoToken')) as GhoToken; const ghoFlashMinter = (await ethers.getContract('GhoFlashMinter')) as GhoFlashMinter; const addEntityTx = await gho.addFacilitator( ghoFlashMinter.address, ghoEntityConfig.flashMinterLabel, ghoEntityConfig.flashMinterCapacity ); const addEntityTxReceipt = await addEntityTx.wait(); const newEntityEvents = addEntityTxReceipt.events?.find((e) => e.event === 'FacilitatorAdded'); if (newEntityEvents?.args) { console.log(`Address added as a facilitator: ${JSON.stringify(newEntityEvents.args[0])}`); } else { throw new Error(`Error at adding entity. Check tx: ${addEntityTx.hash}`); } } ); ================================================ FILE: tasks/testnet-setup/05_set-gho-addresses.ts ================================================ import { task } from 'hardhat/config'; import { STAKE_AAVE_PROXY, TREASURY_PROXY_ID, getAaveProtocolDataProvider, waitForTx, } from '@aave/deploy-v3'; import { GhoToken } from '../../../types/src/contracts/gho/GhoToken'; import { ghoReserveConfig } from '../../helpers/config'; import { getGhoAToken, getGhoVariableDebtToken, getGhoDiscountRateStrategy, } from '../../helpers/contract-getters'; task( 'set-gho-addresses', 'Set addresses as needed in GhoAToken and GhoVariableDebtToken' ).setAction(async (_, hre) => { const { ethers } = hre; const stkAave = await (await hre.deployments.get(STAKE_AAVE_PROXY)).address; const gho = (await ethers.getContract('GhoToken')) as GhoToken; const aaveDataProvider = await getAaveProtocolDataProvider(); const treasuryAddress = await (await hre.deployments.get(TREASURY_PROXY_ID)).address; const discountRateStrategy = await getGhoDiscountRateStrategy(); const tokenProxyAddresses = await aaveDataProvider.getReserveTokensAddresses(gho.address); const ghoAToken = await getGhoAToken(tokenProxyAddresses.aTokenAddress); const ghoVariableDebtToken = await getGhoVariableDebtToken( tokenProxyAddresses.variableDebtTokenAddress ); // Set treasury const setTreasuryTxReceipt = await waitForTx(await ghoAToken.updateGhoTreasury(treasuryAddress)); console.log( `GhoAToken treasury set to: ${treasuryAddress} in tx: ${setTreasuryTxReceipt.transactionHash}` ); // Set variable debt token const setVariableDebtTxReceipt = await waitForTx( await ghoAToken.setVariableDebtToken(tokenProxyAddresses.variableDebtTokenAddress) ); console.log( `GhoAToken variableDebtContract set to: ${tokenProxyAddresses.variableDebtTokenAddress} in tx: ${setVariableDebtTxReceipt.transactionHash}` ); // Set variable debt token const setATokenTxReceipt = await waitForTx( await ghoVariableDebtToken.setAToken(tokenProxyAddresses.aTokenAddress) ); console.log( `VariableDebtToken aToken set to: ${tokenProxyAddresses.aTokenAddress} in tx: ${setATokenTxReceipt.transactionHash}` ); // Set discount strategy const updateDiscountRateStrategyTxReceipt = await waitForTx( await ghoVariableDebtToken.updateDiscountRateStrategy(discountRateStrategy.address) ); console.log( `VariableDebtToken discount strategy set to: ${discountRateStrategy.address} in tx: ${updateDiscountRateStrategyTxReceipt.transactionHash}` ); // Set discount token const updateDiscountTokenTxReceipt = await waitForTx( await ghoVariableDebtToken.updateDiscountToken(stkAave) ); console.log( `VariableDebtToken discount token set to: ${stkAave} in tx: ${updateDiscountTokenTxReceipt.transactionHash}` ); }); ================================================ FILE: tasks/testnet-setup/06_upgrade-stkAave.ts ================================================ import { task } from 'hardhat/config'; import { getAaveProtocolDataProvider, getProxyAdminBySlot, STAKE_AAVE_PROXY, } from '@aave/deploy-v3'; import { getBaseImmutableAdminUpgradeabilityProxy } from '../../helpers/contract-getters'; import { impersonateAccountHardhat } from '../../helpers/misc-utils'; import { StakedAaveV3__factory } from '../../types'; task('upgrade-stkAave', 'Upgrade Staked Aave').setAction(async (_, hre) => { const { ethers } = hre; const signers = await hre.ethers.getSigners(); const shortExecutor = '0xee56e2b3d491590b5b31738cc34d5232f378a8d5'; const gho = await ethers.getContract('GhoToken'); const aaveDataProvider = await getAaveProtocolDataProvider(); const newStakedAaveImpl = await ethers.getContract('StakedAaveV3Impl'); const stkAave = (await hre.deployments.get(STAKE_AAVE_PROXY)).address; const admin = await getProxyAdminBySlot(stkAave); const signerAdmin = signers.find(({ address }) => address == admin); const [deploySigner] = signers; if (!signerAdmin) { throw `Error: Signers does not contain the stkAave Admin Address.\nDeployer ${signers[0].address}\nAdmin: ${admin}`; } const stkAaveProxy = (await getBaseImmutableAdminUpgradeabilityProxy(stkAave)).connect( signerAdmin ); const tokenProxyAddresses = await aaveDataProvider.getReserveTokensAddresses(gho.address); const ghoVariableDebtTokenAddress = tokenProxyAddresses.variableDebtTokenAddress; let instance = StakedAaveV3__factory.connect(stkAaveProxy.address, deploySigner); const stakedAaveEncodedInitialize = newStakedAaveImpl.interface.encodeFunctionData('initialize', [ signerAdmin.address, signerAdmin.address, signerAdmin.address, '0', await instance.COOLDOWN_SECONDS(), ]); const upgradeTx = await stkAaveProxy.upgradeToAndCall( newStakedAaveImpl.address, stakedAaveEncodedInitialize ); await upgradeTx.wait(); instance = await StakedAaveV3__factory.connect( stkAaveProxy.address, await impersonateAccountHardhat(shortExecutor) ); await instance.setGHODebtToken(ghoVariableDebtTokenAddress); console.log(`stkAave upgradeTx.hash: ${upgradeTx.hash}`); console.log(`StkAave implementation set to: ${newStakedAaveImpl.address}`); }); ================================================ FILE: test/__setup.test.ts ================================================ import rawBRE from 'hardhat'; import { initializeMakeSuite } from './helpers/make-suite'; import { config } from 'dotenv'; config(); before(async () => { const skipDeploy = process.env.SKIP_DEPLOY === 'true'; if (!skipDeploy) { await rawBRE.run('deploy-and-setup'); console.log('-> Gho deployed and configured'); } else { console.log('-> Testing Gho Market reusing deployments/ artifacts'); } console.log('-> Initializing test environment'); await initializeMakeSuite(); console.log('\n***************'); console.log('Setup and snapshot finished'); console.log('***************\n'); }); ================================================ FILE: test/basic-borrow.test.ts ================================================ import hre from 'hardhat'; import { expect } from 'chai'; import { BigNumber } from 'ethers'; import './helpers/math/wadraymath'; import { makeSuite, TestEnv } from './helpers/make-suite'; import { timeLatest, setBlocktime, mine } from '../helpers/misc-utils'; import { ONE_YEAR, MAX_UINT, ZERO_ADDRESS, oneRay } from '../helpers/constants'; import { ghoReserveConfig } from '../helpers/config'; import { calcCompoundedInterest } from './helpers/math/calculations'; import { getTxCostAndTimestamp } from './helpers/helpers'; makeSuite('Gho Basic Borrow Flow', (testEnv: TestEnv) => { let ethers; let collateralAmount; let borrowAmount; let startTime; let oneYearLater; let rcpt, tx; before(() => { ethers = hre.ethers; collateralAmount = ethers.utils.parseUnits('1000.0', 18); borrowAmount = ethers.utils.parseUnits('1000.0', 18); }); it('User 1: Deposit WETH and Borrow GHO', async function () { const { users, pool, weth, gho, variableDebtToken } = testEnv; await weth.connect(users[0].signer).approve(pool.address, collateralAmount); await pool .connect(users[0].signer) .deposit(weth.address, collateralAmount, users[0].address, 0); tx = await pool .connect(users[0].signer) .borrow(gho.address, borrowAmount, 2, 0, users[0].address); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(ZERO_ADDRESS, users[0].address, borrowAmount) .to.emit(variableDebtToken, 'Mint') .withArgs(users[0].address, users[0].address, borrowAmount, 0, oneRay) .to.not.emit(variableDebtToken, 'DiscountPercentUpdated'); expect(await gho.balanceOf(users[0].address)).to.be.equal(borrowAmount); expect(await variableDebtToken.totalSupply()).to.be.equal(borrowAmount); expect(await variableDebtToken.getBalanceFromInterest(users[0].address)).to.be.equal(0); expect(await variableDebtToken.balanceOf(users[0].address)).to.be.equal(borrowAmount); }); it('User 1: Increase time by 1 year and check interest accrued', async function () { const { users, gho, variableDebtToken, pool } = testEnv; const poolData = await pool.getReserveData(gho.address); startTime = BigNumber.from(poolData.lastUpdateTimestamp); const variableBorrowIndex = poolData.variableBorrowIndex; oneYearLater = startTime.add(BigNumber.from(ONE_YEAR)); await setBlocktime(oneYearLater.toNumber()); await mine(); // Mine block to increment time in underlying chain as well const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, await timeLatest(), startTime ); const expIndex = variableBorrowIndex.rayMul(multiplier); const user1ExpectedBalance = (await variableDebtToken.scaledBalanceOf(users[0].address)).rayMul( expIndex ); const user1Year1Debt = await variableDebtToken.balanceOf(users[0].address); expect(await gho.balanceOf(users[0].address)).to.be.equal(borrowAmount); expect(user1Year1Debt).to.be.eq(user1ExpectedBalance); expect(await variableDebtToken.totalSupply()).to.be.equal(user1ExpectedBalance); expect(await variableDebtToken.getBalanceFromInterest(users[0].address)).to.be.equal(0); }); it('User 2: After 1 year Deposit WETH and Borrow GHO', async function () { const { users, pool, weth, gho, variableDebtToken } = testEnv; const { lastUpdateTimestamp: ghoLastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); await weth.connect(users[1].signer).approve(pool.address, collateralAmount); await pool .connect(users[1].signer) .deposit(weth.address, collateralAmount, users[1].address, 0); tx = await pool .connect(users[1].signer) .borrow(gho.address, borrowAmount, 2, 0, users[1].address); rcpt = await tx.wait(); const { txTimestamp } = await getTxCostAndTimestamp(rcpt); const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, txTimestamp, BigNumber.from(ghoLastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(ZERO_ADDRESS, users[1].address, borrowAmount) .to.emit(variableDebtToken, 'Mint') .withArgs(users[1].address, users[1].address, borrowAmount, 0, expIndex) .to.not.emit(variableDebtToken, 'DiscountPercentUpdated'); expect(await gho.balanceOf(users[1].address)).to.be.equal(borrowAmount); expect(await variableDebtToken.getBalanceFromInterest(users[1].address)).to.be.equal(0); expect(await variableDebtToken.balanceOf(users[1].address)).to.be.equal(borrowAmount); }); it('User 1: Increase time by 1 more year and borrow more GHO', async function () { const { users, gho, variableDebtToken, pool } = testEnv; const { lastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); const user1ScaledBefore = await variableDebtToken.scaledBalanceOf(users[0].address); const user2ScaledBefore = await variableDebtToken.scaledBalanceOf(users[1].address); // Updating the timestamp for the borrow to be one year later oneYearLater = BigNumber.from(lastUpdateTimestamp).add(BigNumber.from(ONE_YEAR)); await setBlocktime(oneYearLater.toNumber()); tx = await pool .connect(users[0].signer) .borrow(gho.address, borrowAmount, 2, 0, users[0].address); rcpt = await tx.wait(); const { txTimestamp } = await getTxCostAndTimestamp(rcpt); const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, txTimestamp, BigNumber.from(lastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); const borrowedAmountScaled = borrowAmount.rayDiv(expIndex); const user1ExpectedBalance = user1ScaledBefore.add(borrowedAmountScaled).rayMul(expIndex); const user2ExpectedBalance = user2ScaledBefore.rayMul(expIndex); const amount = user1ExpectedBalance.sub(borrowAmount); const user1ExpectedBalanceIncrease = amount.sub(borrowAmount); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(ZERO_ADDRESS, users[0].address, amount) .to.emit(variableDebtToken, 'Mint') .withArgs(users[0].address, users[0].address, amount, user1ExpectedBalanceIncrease, expIndex) .to.not.emit(variableDebtToken, 'DiscountPercentUpdated'); const user1Debt = await variableDebtToken.balanceOf(users[0].address); const user2Debt = await variableDebtToken.balanceOf(users[1].address); expect(await gho.balanceOf(users[0].address)).to.be.equal(borrowAmount.add(borrowAmount)); expect(await gho.balanceOf(users[1].address)).to.be.equal(borrowAmount); expect(user1Debt).to.be.eq(user1ExpectedBalance); expect(user2Debt).to.be.eq(user2ExpectedBalance); const interestsSinceLastAction = user1Debt.sub(borrowAmount).sub(borrowAmount); expect(await variableDebtToken.getBalanceFromInterest(users[0].address)).to.be.equal( interestsSinceLastAction ); }); it('User 2: Receive GHO from User 1 and Repay Debt', async function () { const { users, gho, variableDebtToken, aToken, pool } = testEnv; await gho.connect(users[0].signer).transfer(users[1].address, borrowAmount); await gho.connect(users[1].signer).approve(pool.address, MAX_UINT); const { lastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); const user1ScaledBefore = await variableDebtToken.scaledBalanceOf(users[0].address); const user2ScaledBefore = await variableDebtToken.scaledBalanceOf(users[1].address); expect(await variableDebtToken.getBalanceFromInterest(users[1].address)).to.be.equal(0); tx = await pool.connect(users[1].signer).repay(gho.address, MAX_UINT, 2, users[1].address); rcpt = await tx.wait(); const { txTimestamp } = await getTxCostAndTimestamp(rcpt); const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, txTimestamp, BigNumber.from(lastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); const user1ExpectedBalance = user1ScaledBefore.rayMul(expIndex); const user2ExpectedBalance = user2ScaledBefore.rayMul(expIndex); const user2ExpectedInterest = user2ExpectedBalance.sub(borrowAmount); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(users[1].address, ZERO_ADDRESS, borrowAmount) .to.emit(variableDebtToken, 'Burn') .withArgs(users[1].address, ZERO_ADDRESS, borrowAmount, user2ExpectedInterest, expIndex) .to.not.emit(variableDebtToken, 'DiscountPercentUpdated'); const user1Debt = await variableDebtToken.balanceOf(users[0].address); const user2Debt = await variableDebtToken.balanceOf(users[1].address); expect(await gho.balanceOf(users[0].address)).to.be.equal(borrowAmount); expect(await gho.balanceOf(users[1].address)).to.be.equal( borrowAmount.mul(2).sub(user2ExpectedBalance) ); expect(user1Debt).to.be.eq(user1ExpectedBalance); expect(user2Debt).to.be.eq(0); expect(await gho.balanceOf(aToken.address)).to.be.equal(user2ExpectedInterest); expect(await variableDebtToken.getBalanceFromInterest(users[1].address)).to.be.equal(0); }); it('User 3: Deposit some ETH and borrow GHO', async function () { const { users, pool, weth, gho, variableDebtToken, treasuryAddress } = testEnv; const { lastUpdateTimestamp: ghoLastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); await weth.connect(users[2].signer).approve(pool.address, collateralAmount); await pool .connect(users[2].signer) .deposit(weth.address, collateralAmount, users[2].address, 0); tx = await pool .connect(users[2].signer) .borrow(gho.address, borrowAmount.mul(3), 2, 0, users[2].address); rcpt = await tx.wait(); const { txTimestamp } = await getTxCostAndTimestamp(rcpt); const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, txTimestamp, BigNumber.from(ghoLastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(ZERO_ADDRESS, users[2].address, borrowAmount.mul(3)) .to.emit(variableDebtToken, 'Mint') .withArgs(users[2].address, users[2].address, borrowAmount.mul(3), 0, expIndex) .to.not.emit(variableDebtToken, 'DiscountPercentUpdated'); expect(await gho.balanceOf(users[2].address)).to.be.equal(borrowAmount.mul(3)); expect(await variableDebtToken.getBalanceFromInterest(users[2].address)).to.be.equal(0); expect(await variableDebtToken.balanceOf(users[2].address)).to.be.equal(borrowAmount.mul(3)); }); it('User 1: Repay 100 wei of GHO Debt', async function () { const { users, gho, variableDebtToken, aToken, pool, treasuryAddress } = testEnv; const repayAmount = BigNumber.from('100'); // 100 wei await gho.connect(users[0].signer).approve(pool.address, MAX_UINT); const { lastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); const user1ScaledBefore = await variableDebtToken.scaledBalanceOf(users[0].address); const aTokenGhoBalanceBefore = await gho.balanceOf(aToken.address); const user1AccruedInterestBefore = await variableDebtToken.getBalanceFromInterest( users[0].address ); tx = await pool.connect(users[0].signer).repay(gho.address, repayAmount, 2, users[0].address); rcpt = await tx.wait(); const { txTimestamp } = await getTxCostAndTimestamp(rcpt); const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, txTimestamp, BigNumber.from(lastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); const user1ExpectedBalance = user1ScaledBefore.rayMul(expIndex); const user1ExpectedInterest = user1ExpectedBalance.sub(borrowAmount.mul(2)); const user1ExpectedBalanceIncrease = user1ExpectedInterest.sub(user1AccruedInterestBefore); const expectedATokenGhoBalance = aTokenGhoBalanceBefore.add(repayAmount); const amount = user1ExpectedBalanceIncrease.sub(repayAmount); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(ZERO_ADDRESS, users[0].address, amount) .to.emit(variableDebtToken, 'Mint') .withArgs(users[0].address, users[0].address, amount, user1ExpectedBalanceIncrease, expIndex) .to.not.emit(variableDebtToken, 'DiscountPercentUpdated'); expect(await variableDebtToken.balanceOf(users[0].address)).to.be.eq( user1ExpectedBalance.sub(repayAmount) ); expect(await variableDebtToken.getBalanceFromInterest(users[0].address)).to.be.equal( user1AccruedInterestBefore.add(user1ExpectedBalanceIncrease).sub(repayAmount) ); expect(await gho.balanceOf(aToken.address)).to.be.eq(expectedATokenGhoBalance); }); it('User 1: Receive some GHO from User 3 and Repay Debt', async function () { const { users, gho, variableDebtToken, aToken, pool, treasuryAddress } = testEnv; await gho.connect(users[2].signer).transfer(users[0].address, borrowAmount.mul(3)); await gho.connect(users[0].signer).approve(pool.address, MAX_UINT); const { lastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); const user1ScaledBefore = await variableDebtToken.scaledBalanceOf(users[0].address); const aTokenGhoBalanceBefore = await gho.balanceOf(aToken.address); const user1AccruedInterestBefore = await variableDebtToken.getBalanceFromInterest( users[0].address ); tx = await pool.connect(users[0].signer).repay(gho.address, MAX_UINT, 2, users[0].address); rcpt = await tx.wait(); const { txTimestamp } = await getTxCostAndTimestamp(rcpt); const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, txTimestamp, BigNumber.from(lastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); const user1ExpectedBalance = user1ScaledBefore.rayMul(expIndex); const user1ExpectedInterest = user1ExpectedBalance.sub(borrowAmount.mul(2)); const user1ExpectedBalanceIncrease = user1ExpectedInterest.sub(user1AccruedInterestBefore); const expectedATokenGhoBalance = aTokenGhoBalanceBefore.add(user1ExpectedInterest); const amount = user1ExpectedBalance.sub(user1ExpectedBalanceIncrease); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(users[0].address, ZERO_ADDRESS, amount) .to.emit(variableDebtToken, 'Burn') .withArgs(users[0].address, ZERO_ADDRESS, amount, user1ExpectedBalanceIncrease, expIndex) .to.not.emit(variableDebtToken, 'DiscountPercentUpdated'); expect(await variableDebtToken.balanceOf(users[0].address)).to.be.eq(0); expect(await variableDebtToken.getBalanceFromInterest(users[0].address)).to.be.equal(0); expect(await gho.balanceOf(aToken.address)).to.be.eq(expectedATokenGhoBalance); }); it('Distribute fees to treasury', async function () { const { aToken, gho, treasuryAddress } = testEnv; const aTokenBalance = await gho.balanceOf(aToken.address); expect(aTokenBalance).to.not.be.equal(0); expect(await gho.balanceOf(treasuryAddress)).to.be.equal(0); const tx = await aToken.distributeFeesToTreasury(); await expect(tx) .to.emit(aToken, 'FeesDistributedToTreasury') .withArgs(treasuryAddress, gho.address, aTokenBalance); expect(await gho.balanceOf(aToken.address)).to.be.equal(0); expect(await gho.balanceOf(treasuryAddress)).to.be.equal(aTokenBalance); }); }); ================================================ FILE: test/borrow-onBehalf.test.ts ================================================ import hre from 'hardhat'; import { expect } from 'chai'; import { BigNumber } from 'ethers'; import './helpers/math/wadraymath'; import { makeSuite, TestEnv } from './helpers/make-suite'; import { timeLatest, setBlocktime, mine } from '../helpers/misc-utils'; import { ONE_YEAR, MAX_UINT, ZERO_ADDRESS, oneRay } from '../helpers/constants'; import { ghoReserveConfig } from '../helpers/config'; import { calcCompoundedInterest } from './helpers/math/calculations'; import { getTxCostAndTimestamp } from './helpers/helpers'; makeSuite('Gho OnBehalf Borrow Flow', (testEnv: TestEnv) => { let ethers; let collateralAmount; let borrowAmount; let startTime; let oneYearLater; let rcpt, tx; before(() => { ethers = hre.ethers; collateralAmount = ethers.utils.parseUnits('1000.0', 18); borrowAmount = ethers.utils.parseUnits('1000.0', 18); }); it('User 1: Deposit WETH and delegate borrowing power to User 2', async function () { const { users, pool, weth, gho, variableDebtToken } = testEnv; await weth.connect(users[0].signer).approve(pool.address, collateralAmount); await pool .connect(users[0].signer) .deposit(weth.address, collateralAmount, users[0].address, 0); const tx = await variableDebtToken .connect(users[0].signer) .approveDelegation(users[1].address, borrowAmount); await expect(tx) .to.emit(variableDebtToken, 'BorrowAllowanceDelegated') .withArgs(users[0].address, users[1].address, gho.address, borrowAmount); }); it('User 2: Borrow GHO on behalf of User 1', async function () { const { users, pool, gho, variableDebtToken } = testEnv; tx = await pool .connect(users[1].signer) .borrow(gho.address, borrowAmount, 2, 0, users[0].address); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(ZERO_ADDRESS, users[0].address, borrowAmount) .to.emit(variableDebtToken, 'Mint') .withArgs(users[1].address, users[0].address, borrowAmount, 0, oneRay) .to.not.emit(variableDebtToken, 'DiscountPercentUpdated'); expect(await gho.balanceOf(users[1].address)).to.be.equal(borrowAmount); expect(await variableDebtToken.getBalanceFromInterest(users[0].address)).to.be.equal(0); expect(await variableDebtToken.balanceOf(users[0].address)).to.be.equal(borrowAmount); }); it('User 1: Increase time by 1 year and check interest accrued', async function () { const { users, gho, variableDebtToken, pool } = testEnv; const poolData = await pool.getReserveData(gho.address); startTime = BigNumber.from(poolData.lastUpdateTimestamp); const variableBorrowIndex = poolData.variableBorrowIndex; oneYearLater = startTime.add(BigNumber.from(ONE_YEAR)); await setBlocktime(oneYearLater.toNumber()); await mine(); // Mine block to increment time in underlying chain as well const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, await timeLatest(), startTime ); const expIndex = variableBorrowIndex.rayMul(multiplier); const user1ExpectedBalance = (await variableDebtToken.scaledBalanceOf(users[0].address)).rayMul( expIndex ); const user1Year1Debt = await variableDebtToken.balanceOf(users[0].address); expect(await gho.balanceOf(users[1].address)).to.be.equal(borrowAmount); expect(user1Year1Debt).to.be.eq(user1ExpectedBalance); expect(await variableDebtToken.getBalanceFromInterest(users[0].address)).to.be.equal(0); }); it('User 3: After 1 year Deposit WETH and Borrow GHO', async function () { const { users, pool, weth, gho, variableDebtToken } = testEnv; const { lastUpdateTimestamp: ghoLastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); await weth.connect(users[2].signer).approve(pool.address, collateralAmount); await pool .connect(users[2].signer) .deposit(weth.address, collateralAmount, users[2].address, 0); tx = await pool .connect(users[2].signer) .borrow(gho.address, borrowAmount, 2, 0, users[2].address); rcpt = await tx.wait(); const { txTimestamp } = await getTxCostAndTimestamp(rcpt); const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, txTimestamp, BigNumber.from(ghoLastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(ZERO_ADDRESS, users[2].address, borrowAmount) .to.emit(variableDebtToken, 'Mint') .withArgs(users[2].address, users[2].address, borrowAmount, 0, expIndex) .to.not.emit(variableDebtToken, 'DiscountPercentUpdated'); expect(await gho.balanceOf(users[2].address)).to.be.equal(borrowAmount); expect(await variableDebtToken.getBalanceFromInterest(users[2].address)).to.be.equal(0); expect(await variableDebtToken.balanceOf(users[2].address)).to.be.equal(borrowAmount); }); it('User 2: Receive GHO from User 3 and Repay Debt', async function () { const { users, gho, variableDebtToken, aToken, pool, treasuryAddress } = testEnv; await gho.connect(users[2].signer).transfer(users[1].address, borrowAmount); await gho.connect(users[1].signer).approve(pool.address, MAX_UINT); const { lastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); const user1ScaledBefore = await variableDebtToken.scaledBalanceOf(users[0].address); const user3ScaledBefore = await variableDebtToken.scaledBalanceOf(users[2].address); const currentTimestamp = await ( await hre.ethers.provider.getBlock(await hre.ethers.provider.getBlockNumber()) ).timestamp; const timestamp = currentTimestamp + 1; const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, hre.ethers.BigNumber.from(timestamp), BigNumber.from(lastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); const user1ExpectedBalance = user1ScaledBefore.rayMul(expIndex); const user3ExpectedBalance = user3ScaledBefore.rayMul(expIndex); const user1ExpectedInterest = user1ExpectedBalance.sub(borrowAmount); tx = await pool .connect(users[1].signer) .repay(gho.address, user1ExpectedBalance, 2, users[0].address); rcpt = await tx.wait(); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(users[0].address, ZERO_ADDRESS, borrowAmount) .to.emit(variableDebtToken, 'Burn') .withArgs(users[0].address, ZERO_ADDRESS, borrowAmount, user1ExpectedInterest, expIndex) .to.not.emit(variableDebtToken, 'DiscountPercentUpdated'); const user1Debt = await variableDebtToken.balanceOf(users[0].address); const user2Debt = await variableDebtToken.balanceOf(users[1].address); const user3Debt = await variableDebtToken.balanceOf(users[2].address); expect(await gho.balanceOf(users[0].address)).to.be.equal(0); expect(await gho.balanceOf(users[1].address)).to.be.equal( borrowAmount.mul(2).sub(user1ExpectedBalance) ); expect(await gho.balanceOf(users[2].address)).to.be.equal(0); expect(user1Debt).to.be.eq(0); expect(user2Debt).to.be.eq(0); expect(user3Debt).to.be.eq(user3ExpectedBalance); expect(await gho.balanceOf(aToken.address)).to.be.equal(user1ExpectedInterest); expect(await gho.balanceOf(treasuryAddress)).to.be.eq(0, '8'); expect(await variableDebtToken.getBalanceFromInterest(users[0].address)).to.be.equal(0); }); }); ================================================ FILE: test/discount-borrow.test.ts ================================================ import hre from 'hardhat'; import { expect } from 'chai'; import { BigNumber } from 'ethers'; import './helpers/math/wadraymath'; import { makeSuite, TestEnv } from './helpers/make-suite'; import { timeLatest, setBlocktime, mine, evmSnapshot, evmRevert } from '../helpers/misc-utils'; import { ONE_YEAR, MAX_UINT, ZERO_ADDRESS, oneRay, PERCENTAGE_FACTOR } from '../helpers/constants'; import { ghoReserveConfig } from '../helpers/config'; import { calcCompoundedInterest, calcDiscountRate } from './helpers/math/calculations'; import { getTxCostAndTimestamp } from './helpers/helpers'; import { printVariableDebtTokenEvents } from './helpers/tokenization-events'; makeSuite('Gho Discount Borrow Flow', (testEnv: TestEnv) => { let ethers; let collateralAmount; let borrowAmount; let startTime; let oneYearLater; let rcpt, tx; let discountRate, ghoDiscountedPerDiscountToken, minDiscountTokenBalance; before(async () => { ethers = hre.ethers; collateralAmount = ethers.utils.parseUnits('1000.0', 18); borrowAmount = ethers.utils.parseUnits('1000.0', 18); const { users, aaveToken, stakedAave, discountRateStrategy, faucetOwner } = testEnv; // Fetch discount rate strategy parameters [discountRate, ghoDiscountedPerDiscountToken, minDiscountTokenBalance] = await Promise.all([ discountRateStrategy.DISCOUNT_RATE(), discountRateStrategy.GHO_DISCOUNTED_PER_DISCOUNT_TOKEN(), discountRateStrategy.MIN_DISCOUNT_TOKEN_BALANCE(), ]); // Transfers 10 stkAave (discountToken) to User 2 const stkAaveAmount = ethers.utils.parseUnits('10.0', 18); const approveAaveAmount = ethers.utils.parseUnits('1000.0', 18); await aaveToken.connect(users[1].signer).approve(stakedAave.address, approveAaveAmount); await stakedAave.connect(users[1].signer).stake(users[1].address, stkAaveAmount); }); it('User 1: Deposit WETH and Borrow GHO', async function () { const { users, pool, weth, gho, variableDebtToken } = testEnv; await weth.connect(users[0].signer).approve(pool.address, collateralAmount); await pool .connect(users[0].signer) .deposit(weth.address, collateralAmount, users[0].address, 0); tx = await pool .connect(users[0].signer) .borrow(gho.address, borrowAmount, 2, 0, users[0].address); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(ZERO_ADDRESS, users[0].address, borrowAmount) .to.emit(variableDebtToken, 'Mint') .withArgs(users[0].address, users[0].address, borrowAmount, 0, oneRay); expect(await variableDebtToken.getDiscountPercent(users[0].address)).to.be.eq(0); expect(await gho.balanceOf(users[0].address)).to.be.equal(borrowAmount); expect(await variableDebtToken.getBalanceFromInterest(users[0].address)).to.be.equal(0); expect(await variableDebtToken.balanceOf(users[0].address)).to.be.equal(borrowAmount); }); it('User 1: Increase time by 1 year and check interest accrued', async function () { const { users, gho, variableDebtToken, pool } = testEnv; const poolData = await pool.getReserveData(gho.address); startTime = BigNumber.from(poolData.lastUpdateTimestamp); const variableBorrowIndex = poolData.variableBorrowIndex; oneYearLater = startTime.add(BigNumber.from(ONE_YEAR)); await setBlocktime(oneYearLater.toNumber()); await mine(); // Mine block to increment time in underlying chain as well const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, await timeLatest(), startTime ); const expIndex = variableBorrowIndex.rayMul(multiplier); const user1ExpectedBalance = (await variableDebtToken.scaledBalanceOf(users[0].address)).rayMul( expIndex ); const user1Year1Debt = await variableDebtToken.balanceOf(users[0].address); expect(await gho.balanceOf(users[0].address)).to.be.equal(borrowAmount); expect(user1Year1Debt).to.be.eq(user1ExpectedBalance); expect(await variableDebtToken.getBalanceFromInterest(users[0].address)).to.be.equal(0); }); it('User 2: After 1 year Deposit WETH and Borrow GHO', async function () { const { users, pool, weth, gho, variableDebtToken, stakedAave } = testEnv; const { lastUpdateTimestamp: ghoLastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); const discountPercentBefore = await variableDebtToken.getDiscountPercent(users[1].address); await weth.connect(users[1].signer).approve(pool.address, collateralAmount); await pool .connect(users[1].signer) .deposit(weth.address, collateralAmount, users[1].address, 0); tx = await pool .connect(users[1].signer) .borrow(gho.address, borrowAmount, 2, 0, users[1].address); rcpt = await tx.wait(); const { txTimestamp } = await getTxCostAndTimestamp(rcpt); const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, txTimestamp, BigNumber.from(ghoLastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); const discountTokenBalance = await stakedAave.balanceOf(users[1].address); const discountPercent = calcDiscountRate( discountRate, ghoDiscountedPerDiscountToken, minDiscountTokenBalance, borrowAmount, discountTokenBalance ); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(ZERO_ADDRESS, users[1].address, borrowAmount) .to.emit(variableDebtToken, 'Mint') .withArgs(users[1].address, users[1].address, borrowAmount, 0, expIndex) .to.emit(variableDebtToken, 'DiscountPercentUpdated') .withArgs(users[1].address, discountPercentBefore, discountPercent); expect(await variableDebtToken.getDiscountPercent(users[1].address)).to.be.eq(discountPercent); expect(await gho.balanceOf(users[1].address)).to.be.equal(borrowAmount); expect(await variableDebtToken.getBalanceFromInterest(users[1].address)).to.be.equal(0); expect(await variableDebtToken.balanceOf(users[1].address)).to.be.equal(borrowAmount); }); it('User 2: Wait 1 more year and borrow less GHO than discount accrued', async function () { const snapId = await evmSnapshot(); const { users, pool, weth, gho, variableDebtToken, stakedAave } = testEnv; const debtBalanceBeforeTimeskip = await variableDebtToken.balanceOf(users[1].address); const { lastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); const twoYearsLater = startTime.add(BigNumber.from(ONE_YEAR).mul(2)); await setBlocktime(twoYearsLater.toNumber()); await mine(); // Mine block to increment time in underlying chain as well const balanceBeforeBorrow = await gho.balanceOf(users[1].address); const debtBalanceAfterTimeskip = await variableDebtToken.balanceOf(users[1].address); const debtIncrease = debtBalanceAfterTimeskip.sub(debtBalanceBeforeTimeskip); const discountPercent = calcDiscountRate( discountRate, ghoDiscountedPerDiscountToken, minDiscountTokenBalance, borrowAmount, await stakedAave.balanceOf(users[1].address) ); const expectedDiscount = debtIncrease.mul(discountPercent).div(PERCENTAGE_FACTOR); expect(expectedDiscount).to.be.gt(1); await expect(pool.connect(users[1].signer).borrow(gho.address, 1, 2, 0, users[1].address)).to .not.be.reverted; const balanceAfterBorrow = await gho.balanceOf(users[1].address); expect(balanceAfterBorrow).to.eq(balanceBeforeBorrow.add(1)); await evmRevert(snapId); }); it('User 1: Increase time by 1 more year and borrow more GHO', async function () { const { users, gho, variableDebtToken, pool, stakedAave } = testEnv; const user1BeforeDebt = await variableDebtToken.scaledBalanceOf(users[0].address); const { lastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); const user1ScaledBefore = await variableDebtToken.scaledBalanceOf(users[0].address); const user2ScaledBefore = await variableDebtToken.scaledBalanceOf(users[1].address); // Updating the timestamp for the borrow to be one year later oneYearLater = BigNumber.from(lastUpdateTimestamp).add(BigNumber.from(ONE_YEAR)); await setBlocktime(oneYearLater.toNumber()); tx = await pool .connect(users[0].signer) .borrow(gho.address, borrowAmount, 2, 0, users[0].address); rcpt = await tx.wait(); const { txTimestamp } = await getTxCostAndTimestamp(rcpt); const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, txTimestamp, BigNumber.from(lastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); const borrowedAmountScaled = borrowAmount.rayDiv(expIndex); const user1ExpectedBalance = user1ScaledBefore.add(borrowedAmountScaled).rayMul(expIndex); const amount = user1ExpectedBalance.sub(borrowAmount); const user1BalanceIncrease = amount.sub(borrowAmount); const user2ExpectedBalanceNoDiscount = user2ScaledBefore.rayMul(expIndex); const user2BalanceIncrease = user2ExpectedBalanceNoDiscount.sub(borrowAmount); const user2DiscountTokenBalance = await stakedAave.balanceOf(users[1].address); const user2DiscountPercent = calcDiscountRate( discountRate, ghoDiscountedPerDiscountToken, minDiscountTokenBalance, borrowAmount, user2DiscountTokenBalance ); const user2ExpectedDiscount = user2BalanceIncrease .mul(user2DiscountPercent) .div(PERCENTAGE_FACTOR); const user2ExpectedBalance = user2ExpectedBalanceNoDiscount.sub(user2ExpectedDiscount); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(ZERO_ADDRESS, users[0].address, amount) .to.emit(variableDebtToken, 'Mint') .withArgs(users[0].address, users[0].address, amount, user1BalanceIncrease, expIndex) .to.not.emit(variableDebtToken, 'DiscountPercentUpdated'); const user1Debt = await variableDebtToken.balanceOf(users[0].address); const user2Debt = await variableDebtToken.balanceOf(users[1].address); expect(await variableDebtToken.getDiscountPercent(users[0].address)).to.be.eq(0); expect(await variableDebtToken.getDiscountPercent(users[1].address)).to.be.eq( user2DiscountPercent ); expect(await gho.balanceOf(users[0].address)).to.be.equal(borrowAmount.add(borrowAmount)); expect(await gho.balanceOf(users[1].address)).to.be.equal(borrowAmount); expect(user1Debt).to.be.eq(user1ExpectedBalance); expect(user2Debt).to.be.closeTo(user2ExpectedBalance, 1); const balanceIncrease = user1Debt.sub(borrowAmount).sub(user1BeforeDebt); expect(await variableDebtToken.getBalanceFromInterest(users[0].address)).to.be.equal( balanceIncrease ); }); it('User 2: Receive GHO from User 1 and Repay Debt', async function () { const { users, gho, variableDebtToken, aToken, pool, stakedAave, treasuryAddress } = testEnv; await gho.connect(users[0].signer).transfer(users[1].address, borrowAmount); await gho.connect(users[1].signer).approve(pool.address, MAX_UINT); const { lastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); const user1ScaledBefore = await variableDebtToken.scaledBalanceOf(users[0].address); const user2ScaledBefore = await variableDebtToken.scaledBalanceOf(users[1].address); const user2DiscountPercentBefore = await variableDebtToken.getDiscountPercent(users[1].address); expect(await variableDebtToken.getBalanceFromInterest(users[1].address)).to.be.equal(0); tx = await pool.connect(users[1].signer).repay(gho.address, MAX_UINT, 2, users[1].address); rcpt = await tx.wait(); const { txTimestamp } = await getTxCostAndTimestamp(rcpt); const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, txTimestamp, BigNumber.from(lastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); const user1ExpectedBalance = user1ScaledBefore.rayMul(expIndex); const user2ExpectedBalanceNoDiscount = user2ScaledBefore.rayMul(expIndex); const user2BalanceIncrease = user2ExpectedBalanceNoDiscount.sub(borrowAmount); const user2DiscountTokenBalance = await stakedAave.balanceOf(users[1].address); const user2ExpectedDiscount = user2BalanceIncrease .mul(user2DiscountPercentBefore) .div(PERCENTAGE_FACTOR); const user2ExpectedBalance = user2ExpectedBalanceNoDiscount.sub(user2ExpectedDiscount); const user2ExpectedInterest = user2BalanceIncrease.sub(user2ExpectedDiscount); expect(await variableDebtToken.getBalanceFromInterest(users[1].address)).to.be.equal(0); const user2DiscountPercent = calcDiscountRate( discountRate, ghoDiscountedPerDiscountToken, minDiscountTokenBalance, BigNumber.from(0), user2DiscountTokenBalance ); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(users[1].address, ZERO_ADDRESS, borrowAmount) .to.emit(variableDebtToken, 'Burn') .withArgs(users[1].address, ZERO_ADDRESS, borrowAmount, user2ExpectedInterest, expIndex) .to.emit(variableDebtToken, 'DiscountPercentUpdated') .withArgs(users[1].address, user2DiscountPercentBefore, user2DiscountPercent); const user1Debt = await variableDebtToken.balanceOf(users[0].address); const user2Debt = await variableDebtToken.balanceOf(users[1].address); expect(await variableDebtToken.getDiscountPercent(users[1].address)).to.be.eq( user2DiscountPercent ); expect(await gho.balanceOf(users[0].address)).to.be.equal(borrowAmount); expect(await gho.balanceOf(users[1].address)).to.be.equal( borrowAmount.mul(2).sub(user2ExpectedBalance) ); expect(user1Debt).to.be.eq(user1ExpectedBalance); // TODO: update to zero expect(user2Debt).to.be.eq(0); expect(await gho.balanceOf(aToken.address)).to.be.eq(user2ExpectedInterest); expect(await variableDebtToken.getBalanceFromInterest(users[1].address)).to.be.equal(0); }); it('User 3: Deposit some ETH and borrow GHO', async function () { const { users, pool, weth, gho, variableDebtToken } = testEnv; const { lastUpdateTimestamp: ghoLastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); await weth.connect(users[2].signer).approve(pool.address, collateralAmount); await pool .connect(users[2].signer) .deposit(weth.address, collateralAmount, users[2].address, 0); tx = await pool .connect(users[2].signer) .borrow(gho.address, borrowAmount.mul(3), 2, 0, users[2].address); rcpt = await tx.wait(); const { txTimestamp } = await getTxCostAndTimestamp(rcpt); const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, txTimestamp, BigNumber.from(ghoLastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(ZERO_ADDRESS, users[2].address, borrowAmount.mul(3)) .to.emit(variableDebtToken, 'Mint') .withArgs(users[2].address, users[2].address, borrowAmount.mul(3), 0, expIndex) .to.not.emit(variableDebtToken, 'DiscountPercentUpdated'); expect(await variableDebtToken.getDiscountPercent(users[2].address)).to.be.eq(0); expect(await gho.balanceOf(users[2].address)).to.be.equal(borrowAmount.mul(3)); expect(await variableDebtToken.getBalanceFromInterest(users[2].address)).to.be.equal(0); expect(await variableDebtToken.balanceOf(users[2].address)).to.be.equal(borrowAmount.mul(3)); }); it('User 1: Repay 100 wei of GHO Debt', async function () { const { users, gho, variableDebtToken, aToken, pool, treasuryAddress } = testEnv; const repayAmount = BigNumber.from('100'); // 100 wei await gho.connect(users[0].signer).approve(pool.address, MAX_UINT); const { lastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); const user1ScaledBefore = await variableDebtToken.scaledBalanceOf(users[0].address); const aTokenGhoBalanceBefore = await gho.balanceOf(aToken.address); const user1AccruedInterestBefore = await variableDebtToken.getBalanceFromInterest( users[0].address ); tx = await pool.connect(users[0].signer).repay(gho.address, repayAmount, 2, users[0].address); rcpt = await tx.wait(); const { txTimestamp } = await getTxCostAndTimestamp(rcpt); const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, txTimestamp, BigNumber.from(lastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); const user1ExpectedBalance = user1ScaledBefore.rayMul(expIndex); const user1ExpectedInterest = user1ExpectedBalance.sub(borrowAmount.mul(2)); const user1ExpectedBalanceIncrease = user1ExpectedInterest.sub(user1AccruedInterestBefore); const expectedATokenGhoBalance = aTokenGhoBalanceBefore.add(repayAmount); const amount = user1ExpectedBalanceIncrease.sub(repayAmount); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(ZERO_ADDRESS, users[0].address, amount) .to.emit(variableDebtToken, 'Mint') .withArgs(users[0].address, users[0].address, amount, user1ExpectedBalanceIncrease, expIndex) .to.not.emit(variableDebtToken, 'DiscountPercentUpdated'); expect(await variableDebtToken.getDiscountPercent(users[0].address)).to.be.eq(0); expect(await variableDebtToken.balanceOf(users[0].address)).to.be.eq( user1ExpectedBalance.sub(repayAmount) ); expect(await variableDebtToken.getBalanceFromInterest(users[0].address)).to.be.equal( user1AccruedInterestBefore.add(user1ExpectedBalanceIncrease).sub(repayAmount) ); expect(await gho.balanceOf(aToken.address)).to.be.eq(expectedATokenGhoBalance); }); it('User 1: Receive some GHO from User 3 and Repay Debt', async function () { const { users, gho, variableDebtToken, aToken, pool, treasuryAddress } = testEnv; await gho.connect(users[2].signer).transfer(users[0].address, borrowAmount.mul(3)); await gho.connect(users[0].signer).approve(pool.address, MAX_UINT); const { lastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); const user1ScaledBefore = await variableDebtToken.scaledBalanceOf(users[0].address); const aTokenGhoBalanceBefore = await gho.balanceOf(aToken.address); const user1AccruedInterestBefore = await variableDebtToken.getBalanceFromInterest( users[0].address ); tx = await pool.connect(users[0].signer).repay(gho.address, MAX_UINT, 2, users[0].address); rcpt = await tx.wait(); const { txTimestamp } = await getTxCostAndTimestamp(rcpt); const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, txTimestamp, BigNumber.from(lastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); const user1ExpectedBalance = user1ScaledBefore.rayMul(expIndex); const user1ExpectedInterest = user1ExpectedBalance.sub(borrowAmount.mul(2)); const user1ExpectedBalanceIncrease = user1ExpectedInterest.sub(user1AccruedInterestBefore); const expectedATokenGhoBalance = aTokenGhoBalanceBefore.add(user1ExpectedInterest); const amount = user1ExpectedBalance.sub(user1ExpectedBalanceIncrease); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(users[0].address, ZERO_ADDRESS, amount) .to.emit(variableDebtToken, 'Burn') .withArgs(users[0].address, ZERO_ADDRESS, amount, user1ExpectedBalanceIncrease, expIndex) .to.not.emit(variableDebtToken, 'DiscountPercentUpdated'); expect(await variableDebtToken.getDiscountPercent(users[0].address)).to.be.eq(0); expect(await variableDebtToken.balanceOf(users[0].address)).to.be.eq(0); expect(await variableDebtToken.getBalanceFromInterest(users[0].address)).to.be.equal(0); expect(await gho.balanceOf(aToken.address)).to.be.eq(expectedATokenGhoBalance); }); it('Distribute fees to treasury', async function () { const { aToken, gho, treasuryAddress } = testEnv; const aTokenBalance = await gho.balanceOf(aToken.address); expect(aTokenBalance).to.not.be.equal(0); expect(await gho.balanceOf(treasuryAddress)).to.be.equal(0); const tx = await aToken.distributeFeesToTreasury(); await expect(tx) .to.emit(aToken, 'FeesDistributedToTreasury') .withArgs(treasuryAddress, gho.address, aTokenBalance); expect(await gho.balanceOf(aToken.address)).to.be.equal(0); expect(await gho.balanceOf(treasuryAddress)).to.be.equal(aTokenBalance); }); }); ================================================ FILE: test/discount-rebalance.test.ts ================================================ import hre from 'hardhat'; import { expect } from 'chai'; import { BigNumber } from 'ethers'; import './helpers/math/wadraymath'; import { makeSuite, TestEnv } from './helpers/make-suite'; import { advanceTimeAndBlock } from '../helpers/misc-utils'; import { ZERO_ADDRESS, oneRay } from '../helpers/constants'; import { ghoReserveConfig } from '../helpers/config'; import { calcCompoundedInterest, calcDiscountRate } from './helpers/math/calculations'; import { getTxCostAndTimestamp } from './helpers/helpers'; import { ZeroDiscountRateStrategy__factory } from '../types'; makeSuite('Gho Discount Rebalance Flow', (testEnv: TestEnv) => { let ethers; let collateralAmount; let borrowAmount; let rcpt, tx; let discountRate, ghoDiscountedPerDiscountToken, minDiscountTokenBalance; before(async () => { ethers = hre.ethers; collateralAmount = ethers.utils.parseUnits('1000.0', 18); borrowAmount = ethers.utils.parseUnits('1000.0', 18); const { users, aaveToken, stakedAave, discountRateStrategy } = testEnv; // Fetch discount rate strategy parameters [discountRate, ghoDiscountedPerDiscountToken, minDiscountTokenBalance] = await Promise.all([ discountRateStrategy.DISCOUNT_RATE(), discountRateStrategy.GHO_DISCOUNTED_PER_DISCOUNT_TOKEN(), discountRateStrategy.MIN_DISCOUNT_TOKEN_BALANCE(), ]); // Transfers 10 stkAave (discountToken) to User 1 const stkAaveAmount = ethers.utils.parseUnits('10.0', 18); await aaveToken.connect(users[2].signer).approve(stakedAave.address, stkAaveAmount); await stakedAave.connect(users[2].signer).stake(users[0].address, stkAaveAmount); }); it('User 1: Deposit WETH and Borrow GHO', async function () { const { users, pool, weth, gho, variableDebtToken, stakedAave } = testEnv; const discountPercentBefore = await variableDebtToken.getDiscountPercent(users[0].address); await weth.connect(users[0].signer).approve(pool.address, collateralAmount); await pool .connect(users[0].signer) .deposit(weth.address, collateralAmount, users[0].address, 0); tx = await pool .connect(users[0].signer) .borrow(gho.address, borrowAmount, 2, 0, users[0].address); rcpt = await tx.wait(); const discountTokenBalance = await stakedAave.balanceOf(users[0].address); const discountPercent = calcDiscountRate( discountRate, ghoDiscountedPerDiscountToken, minDiscountTokenBalance, borrowAmount, discountTokenBalance ); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(ZERO_ADDRESS, users[0].address, borrowAmount) .to.emit(variableDebtToken, 'Mint') .withArgs(users[0].address, users[0].address, borrowAmount, 0, oneRay) .to.emit(variableDebtToken, 'DiscountPercentUpdated') .withArgs(users[0].address, discountPercentBefore, discountPercent); expect(await variableDebtToken.getDiscountPercent(users[0].address)).to.be.eq(discountPercent); expect(await gho.balanceOf(users[0].address)).to.be.equal(borrowAmount); expect(await variableDebtToken.getBalanceFromInterest(users[0].address)).to.be.equal(0); expect(await variableDebtToken.balanceOf(users[0].address)).to.be.equal(borrowAmount); }); it('User 2: Deposit WETH and Borrow GHO', async function () { const { users, pool, weth, gho, variableDebtToken, stakedAave } = testEnv; const { lastUpdateTimestamp: ghoLastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); await weth.connect(users[1].signer).approve(pool.address, collateralAmount); await pool .connect(users[1].signer) .deposit(weth.address, collateralAmount, users[1].address, 0); tx = await pool .connect(users[1].signer) .borrow(gho.address, borrowAmount, 2, 0, users[1].address); rcpt = await tx.wait(); const { txTimestamp } = await getTxCostAndTimestamp(rcpt); const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, txTimestamp, BigNumber.from(ghoLastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); const discountTokenBalance = await stakedAave.balanceOf(users[1].address); const discountPercent = calcDiscountRate( discountRate, ghoDiscountedPerDiscountToken, minDiscountTokenBalance, borrowAmount, discountTokenBalance ); await expect(tx) .to.emit(variableDebtToken, 'Transfer') .withArgs(ZERO_ADDRESS, users[1].address, borrowAmount) .to.emit(variableDebtToken, 'Mint') .withArgs(users[1].address, users[1].address, borrowAmount, 0, expIndex) .to.not.emit(variableDebtToken, 'DiscountPercentUpdated'); expect(await variableDebtToken.getDiscountPercent(users[1].address)).to.be.eq(discountPercent); expect(await gho.balanceOf(users[1].address)).to.be.equal(borrowAmount); expect(await variableDebtToken.getBalanceFromInterest(users[1].address)).to.be.equal(0); expect(await variableDebtToken.balanceOf(users[1].address)).to.be.equal(borrowAmount); }); it('Time flies - variable debt index increases', async function () { await advanceTimeAndBlock(10000000000); }); it('User 3 rebalances User 1 discount percent - discount percent is adjusted to current debt', async function () { const { users, pool, variableDebtToken, stakedAave, gho } = testEnv; const { lastUpdateTimestamp: ghoLastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); const user1ScaledBefore = await variableDebtToken.scaledBalanceOf(users[0].address); const discountPercentBefore = await variableDebtToken.getDiscountPercent(users[0].address); tx = await variableDebtToken .connect(users[2].signer) .rebalanceUserDiscountPercent(users[0].address); rcpt = await tx.wait(); const { txTimestamp } = await getTxCostAndTimestamp(rcpt); const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, txTimestamp, BigNumber.from(ghoLastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); const user1ExpectedBalanceNoDiscount = user1ScaledBefore.rayMul(expIndex); const user1BalanceIncrease = user1ExpectedBalanceNoDiscount.sub(borrowAmount); const user1ExpectedDiscount = user1BalanceIncrease.percentMul(discountPercentBefore); const user1BalanceIncreaseWithDiscount = user1BalanceIncrease.sub(user1ExpectedDiscount); const user1ExpectedDiscountScaled = user1ExpectedDiscount.rayDiv(expIndex); const user1ExpectedScaledBalanceWithDiscount = user1ScaledBefore.sub( user1ExpectedDiscountScaled ); const user1ExpectedBalance = user1ExpectedScaledBalanceWithDiscount.rayMul(expIndex); const user1DiscountTokenBalance = await stakedAave.balanceOf(users[0].address); const expectedUser1DiscountPercent = calcDiscountRate( discountRate, ghoDiscountedPerDiscountToken, minDiscountTokenBalance, user1ExpectedBalance, user1DiscountTokenBalance ); await expect(tx) .to.emit(variableDebtToken, 'DiscountPercentUpdated') .withArgs(users[0].address, discountPercentBefore, expectedUser1DiscountPercent) .to.emit(variableDebtToken, 'Transfer') .withArgs(ZERO_ADDRESS, users[0].address, user1BalanceIncreaseWithDiscount) .to.emit(variableDebtToken, 'Mint') .withArgs( ZERO_ADDRESS, users[0].address, user1BalanceIncreaseWithDiscount, user1BalanceIncreaseWithDiscount, expIndex ); expect(await variableDebtToken.getDiscountPercent(users[0].address)).to.be.eq( expectedUser1DiscountPercent ); }); it('Time flies - variable debt index increases', async function () { await advanceTimeAndBlock(10000000000); }); it('Governance changes the discount rate strategy', async function () { const { variableDebtToken, poolAdmin } = testEnv; const oldDiscountRateStrategyAddress = await variableDebtToken.getDiscountRateStrategy(); const emptyStrategy = await new ZeroDiscountRateStrategy__factory(poolAdmin.signer).deploy(); await expect( variableDebtToken.connect(poolAdmin.signer).updateDiscountRateStrategy(emptyStrategy.address) ) .to.emit(variableDebtToken, 'DiscountRateStrategyUpdated') .withArgs(oldDiscountRateStrategyAddress, emptyStrategy.address); }); it('User 3 rebalances User 1 discount percent - discount percent changes', async function () { const { users, variableDebtToken } = testEnv; const discountPercentBefore = await variableDebtToken.getDiscountPercent(users[0].address); await expect( variableDebtToken.connect(users[2].signer).rebalanceUserDiscountPercent(users[0].address) ) .to.emit(variableDebtToken, 'DiscountPercentUpdated') .withArgs(users[0].address, discountPercentBefore, 0); expect(await variableDebtToken.getDiscountPercent(users[0].address)).to.be.not.eq( discountPercentBefore ); expect(await variableDebtToken.getDiscountPercent(users[0].address)).to.be.eq(0); }); it('Time flies - variable debt index increases', async function () { await advanceTimeAndBlock(10000000000); }); it('User 3 rebalances User 1 discount percent - discount percent is the same', async function () { const { users, variableDebtToken } = testEnv; const discountPercentBefore = await variableDebtToken.getDiscountPercent(users[0].address); await expect( variableDebtToken.connect(users[2].signer).rebalanceUserDiscountPercent(users[0].address) ).to.not.emit(variableDebtToken, 'DiscountPercentUpdated'); expect(await variableDebtToken.getDiscountPercent(users[0].address)).to.be.eq( discountPercentBefore ); }); }); ================================================ FILE: test/flashmint.test.ts ================================================ import hre from 'hardhat'; import { expect } from 'chai'; import { PANIC_CODES } from '@nomicfoundation/hardhat-chai-matchers/panic'; import { makeSuite, TestEnv } from './helpers/make-suite'; import { MockFlashBorrower__factory, GhoFlashMinter__factory, MockFlashBorrower } from '../types'; import { ONE_ADDRESS, ZERO_ADDRESS } from '../helpers/constants'; import { ghoEntityConfig } from '../helpers/config'; import { mintErc20 } from './helpers/user-setup'; import './helpers/math/wadraymath'; import { evmRevert, evmSnapshot } from '../helpers/misc-utils'; makeSuite('Gho FlashMinter', (testEnv: TestEnv) => { let ethers; let flashBorrower: MockFlashBorrower; let flashFee; let tx; before(async () => { ethers = hre.ethers; const { deployer, flashMinter } = testEnv; const flashBorrowerFactory = new MockFlashBorrower__factory(deployer.signer); flashBorrower = await flashBorrowerFactory.deploy(flashMinter.address); flashFee = ghoEntityConfig.flashMinterFee; }); it('Check flashmint percentage fee', async function () { const { flashMinter } = testEnv; expect(await flashMinter.getFee()).to.be.equal(100); }); it('Check flashmint fee for unsupported token (revert expected)', async function () { const { flashMinter, usdc } = testEnv; await expect(flashMinter.flashFee(usdc.address, 1)).to.be.revertedWith( 'FlashMinter: Unsupported currency' ); }); it('Check flashmint fee', async function () { const { flashMinter, gho } = testEnv; const borrowAmount = ethers.utils.parseUnits('1000.0', 18); const expectedFeeAmount = borrowAmount.percentMul(flashFee); expect(await flashMinter.flashFee(gho.address, borrowAmount)).to.be.equal(expectedFeeAmount); }); it('Fund FlashBorrower To Repay FlashMint Fees', async function () { const { users, pool, weth, gho } = testEnv; const collateralAmount = ethers.utils.parseUnits('1000.0', 18); const borrowAmount = ethers.utils.parseUnits('1000.0', 18); const expectedFee = borrowAmount.percentMul(flashFee); await weth.connect(users[0].signer).approve(pool.address, collateralAmount); await pool .connect(users[0].signer) .deposit(weth.address, collateralAmount, users[0].address, 0); tx = await pool .connect(users[0].signer) .borrow(gho.address, borrowAmount, 2, 0, users[0].address); await gho.connect(users[0].signer).transfer(flashBorrower.address, expectedFee); expect(await gho.balanceOf(flashBorrower.address)).to.be.equal(expectedFee); }); it('Flashmint of unsupported token (revert expected)', async function () { const { flashMinter, usdc } = testEnv; const randomAddress = ONE_ADDRESS; const borrowAmount = 1; await expect( flashMinter.flashLoan(randomAddress, usdc.address, borrowAmount, '0x00') ).to.be.revertedWith('FlashMinter: Unsupported currency'); }); it('Flashmint of GHO with an EOA as receiver (revert expected)', async function () { const { flashMinter, gho } = testEnv; const randomAddress = ONE_ADDRESS; const borrowAmount = 1; await expect(flashMinter.flashLoan(randomAddress, gho.address, borrowAmount, '0x00')).to.be .reverted; }); it('Flashmint of GHO with non-complaint receiver (revert expected)', async function () { const { gho } = testEnv; const borrowAmount = 1; await flashBorrower.setAllowCallback(false); await expect(flashBorrower.flashBorrow(gho.address, borrowAmount)).to.be.revertedWith( 'FlashMinter: Callback failed' ); await flashBorrower.setAllowCallback(true); }); it('Flashmint 1000 GHO', async function () { const { flashMinter, gho } = testEnv; const borrowAmount = ethers.utils.parseUnits('1000.0', 18); const expectedFeeAmount = borrowAmount.percentMul(flashFee); tx = await flashBorrower.flashBorrow(gho.address, borrowAmount); await expect(tx) .to.emit(flashMinter, 'FlashMint') .withArgs( flashBorrower.address, flashBorrower.address, gho.address, borrowAmount, expectedFeeAmount ); expect(await gho.balanceOf(flashBorrower.address)).to.be.equal(0); expect(await gho.balanceOf(flashMinter.address)).to.be.equal(expectedFeeAmount); }); it('Flashmint 1000 GHO As Approved FlashBorrower', async function () { const { flashMinter, gho, aclAdmin, aclManager } = testEnv; expect(await aclManager.isFlashBorrower(flashBorrower.address)).to.be.false; await aclManager.connect(aclAdmin.signer).addFlashBorrower(flashBorrower.address); expect(await aclManager.isFlashBorrower(flashBorrower.address)).to.be.true; // fee should be zero since msg.sender will be an approved FlashBorrower const expectedFee = 0; const initialFlashMinterBalance = await gho.balanceOf(flashMinter.address); const borrowAmount = ethers.utils.parseUnits('1000.0', 18); tx = await flashBorrower.flashBorrow(gho.address, borrowAmount); await expect(tx) .to.emit(flashMinter, 'FlashMint') .withArgs( flashBorrower.address, flashBorrower.address, gho.address, borrowAmount, expectedFee ); expect(await gho.balanceOf(flashBorrower.address)).to.be.equal(0); expect(await gho.balanceOf(flashMinter.address)).to.be.equal(initialFlashMinterBalance); // remove approved FlashBorrower role for the rest of the tests await aclManager.connect(aclAdmin.signer).removeFlashBorrower(flashBorrower.address); expect(await aclManager.isFlashBorrower(flashBorrower.address)).to.be.false; }); it('Flashmint and change capacity mid-execution as approved FlashBorrower', async function () { const snapId = await evmSnapshot(); const { flashMinter, gho, ghoOwner, aclAdmin, aclManager, users } = testEnv; expect(await aclManager.isFlashBorrower(flashBorrower.address)).to.be.false; await aclManager.connect(aclAdmin.signer).addFlashBorrower(flashBorrower.address); expect(await aclManager.isFlashBorrower(flashBorrower.address)).to.be.true; const BUCKET_MANAGER_ROLE = ethers.utils.id('BUCKET_MANAGER_ROLE'); await expect(gho.connect(ghoOwner.signer).grantRole(BUCKET_MANAGER_ROLE, flashBorrower.address)) .to.not.be.reverted; expect((await gho.getFacilitatorBucket(flashMinter.address))[0]).to.not.eq(0); await expect(flashBorrower.flashBorrowOtherActionMax(gho.address)).to.not.be.reverted; expect((await gho.getFacilitatorBucket(flashMinter.address))[0]).to.eq(0); await evmRevert(snapId); }); it('Flashmint 1 Billion GHO (revert expected)', async function () { const { gho } = testEnv; const oneBillion = ethers.utils.parseUnits('1000000000', 18); await expect(flashBorrower.flashBorrow(gho.address, oneBillion)).to.be.revertedWith( 'FACILITATOR_BUCKET_CAPACITY_EXCEEDED' ); }); it('Fund FlashBorrower To Repay FlashMint Fees (Maximum Bucket Capacity Minus 1)', async function () { const { users, flashMinter, pool, weth, gho, faucetOwner, aaveOracle } = testEnv; const flashMinterFacilitator = await gho.getFacilitator(flashMinter.address); const capacityMinusOne = flashMinterFacilitator.bucketCapacity.sub(1); const expectedFee = capacityMinusOne.percentMul(flashFee); const ghoPrice = await aaveOracle.getAssetPrice(gho.address); const wethPrice = await aaveOracle.getAssetPrice(weth.address); const expectedRequiredCollateral = expectedFee.mul(ghoPrice).div(wethPrice).mul(2); await mintErc20(faucetOwner, weth.address, [users[0].address], expectedRequiredCollateral); await weth.connect(users[0].signer).approve(pool.address, expectedRequiredCollateral); await pool .connect(users[0].signer) .deposit(weth.address, expectedRequiredCollateral, users[0].address, 0); tx = await pool .connect(users[0].signer) .borrow(gho.address, expectedFee, 2, 0, users[0].address); await gho.connect(users[0].signer).transfer(flashBorrower.address, expectedFee); expect(await gho.balanceOf(flashBorrower.address)).to.be.equal(expectedFee); }); it('Flashmint Maximum Bucket Capacity Minus 1', async function () { const { flashMinter, gho } = testEnv; const flashMinterFacilitator = await gho.getFacilitator(flashMinter.address); const capacityMinusOne = flashMinterFacilitator.bucketCapacity.sub(1); const expectedFee = capacityMinusOne.percentMul(flashFee); const initialFlashMinterBalance = await gho.balanceOf(flashMinter.address); tx = await flashBorrower.flashBorrow(gho.address, capacityMinusOne); await expect(tx) .to.emit(flashMinter, 'FlashMint') .withArgs( flashBorrower.address, flashBorrower.address, gho.address, capacityMinusOne, expectedFee ); expect(await gho.balanceOf(flashBorrower.address)).to.be.equal(0); expect(await gho.balanceOf(flashMinter.address)).to.be.equal( initialFlashMinterBalance.add(expectedFee) ); }); it('Fund FlashBorrower To Repay FlashMint Fees (Maximum Bucket Capacity)', async function () { const { users, flashMinter, pool, weth, gho, faucetOwner, aaveOracle } = testEnv; const flashMinterFacilitator = await gho.getFacilitator(flashMinter.address); const capacity = flashMinterFacilitator.bucketCapacity; const expectedFee = capacity.percentMul(flashFee); const ghoPrice = await aaveOracle.getAssetPrice(gho.address); const wethPrice = await aaveOracle.getAssetPrice(weth.address); const expectedRequiredCollateral = expectedFee.mul(ghoPrice).div(wethPrice).mul(2); await mintErc20(faucetOwner, weth.address, [users[0].address], expectedRequiredCollateral); await weth.connect(users[0].signer).approve(pool.address, expectedRequiredCollateral); await pool .connect(users[0].signer) .deposit(weth.address, expectedRequiredCollateral, users[0].address, 0); tx = await pool .connect(users[0].signer) .borrow(gho.address, expectedFee, 2, 0, users[0].address); await gho.connect(users[0].signer).transfer(flashBorrower.address, expectedFee); expect(await gho.balanceOf(flashBorrower.address)).to.be.equal(expectedFee); }); it('Flashmint Maximum Bucket Capacity', async function () { const { flashMinter, gho } = testEnv; const flashMinterFacilitator = await gho.getFacilitator(flashMinter.address); const capacity = flashMinterFacilitator.bucketCapacity; const expectedFee = capacity.percentMul(flashFee); const initialFlashMinterBalance = await gho.balanceOf(flashMinter.address); tx = await flashBorrower.flashBorrow(gho.address, capacity); await expect(tx) .to.emit(flashMinter, 'FlashMint') .withArgs(flashBorrower.address, flashBorrower.address, gho.address, capacity, expectedFee); expect(await gho.balanceOf(flashBorrower.address)).to.be.equal(0); expect(await gho.balanceOf(flashMinter.address)).to.be.equal( initialFlashMinterBalance.add(expectedFee) ); }); it('Flashmint maximum bucket capacity + 1 (revert expected)', async function () { const { flashMinter, gho } = testEnv; const flashMinterFacilitator = await gho.getFacilitator(flashMinter.address); await expect( flashBorrower.flashBorrow(gho.address, flashMinterFacilitator.bucketCapacity.add(1)) ).to.be.revertedWith('FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); }); it('MaxFlashLoan', async function () { const { flashMinter, gho } = testEnv; expect(await flashMinter.maxFlashLoan(gho.address)).to.be.equal( ghoEntityConfig.flashMinterCapacity ); }); it('Change Flashmint Facilitator Max Capacity', async function () { const { flashMinter, gho, ghoOwner } = testEnv; const oldCapacity = ghoEntityConfig.flashMinterCapacity; const reducedCapacity = oldCapacity.div(5); const tx = await gho .connect(ghoOwner.signer) .setFacilitatorBucketCapacity(flashMinter.address, reducedCapacity); await expect(tx).to.not.be.reverted; await expect(tx) .to.emit(gho, 'FacilitatorBucketCapacityUpdated') .withArgs(flashMinter.address, oldCapacity, reducedCapacity); const flashMinterFacilitator = await gho.getFacilitator(flashMinter.address); const updatedCapacity = flashMinterFacilitator.bucketCapacity; expect(updatedCapacity).to.be.equal(reducedCapacity); }); it('Fund FlashBorrower To Repay FlashMint Fees (New Maximum Bucket Capacity)', async function () { const { users, flashMinter, pool, weth, gho, faucetOwner, aaveOracle } = testEnv; const flashMinterFacilitator = await gho.getFacilitator(flashMinter.address); const capacity = flashMinterFacilitator.bucketCapacity; expect(capacity).to.be.lt(ghoEntityConfig.flashMinterCapacity); const expectedFee = capacity.percentMul(flashFee); const ghoPrice = await aaveOracle.getAssetPrice(gho.address); const wethPrice = await aaveOracle.getAssetPrice(weth.address); const expectedRequiredCollateral = expectedFee.mul(ghoPrice).div(wethPrice).mul(2); await mintErc20(faucetOwner, weth.address, [users[0].address], expectedRequiredCollateral); await weth.connect(users[0].signer).approve(pool.address, expectedRequiredCollateral); await pool .connect(users[0].signer) .deposit(weth.address, expectedRequiredCollateral, users[0].address, 0); tx = await pool .connect(users[0].signer) .borrow(gho.address, expectedFee, 2, 0, users[0].address); await gho.connect(users[0].signer).transfer(flashBorrower.address, expectedFee); expect(await gho.balanceOf(flashBorrower.address)).to.be.equal(expectedFee); }); it('Flashmint New Maximum Bucket Capacity', async function () { const { flashMinter, gho } = testEnv; const flashMinterFacilitator = await gho.getFacilitator(flashMinter.address); const capacity = flashMinterFacilitator.bucketCapacity; const expectedFee = capacity.percentMul(flashFee); const initialFlashMinterBalance = await gho.balanceOf(flashMinter.address); tx = await flashBorrower.flashBorrow(gho.address, capacity); await expect(tx) .to.emit(flashMinter, 'FlashMint') .withArgs(flashBorrower.address, flashBorrower.address, gho.address, capacity, expectedFee); expect(await gho.balanceOf(flashBorrower.address)).to.be.equal(0); expect(await gho.balanceOf(flashMinter.address)).to.be.equal( initialFlashMinterBalance.add(expectedFee) ); }); it('Flashmint maximum bucket capacity + 1 (revert expected)', async function () { const { flashMinter, gho } = testEnv; const flashMinterFacilitator = await gho.getFacilitator(flashMinter.address); await expect( flashBorrower.flashBorrow(gho.address, flashMinterFacilitator.bucketCapacity.add(1)) ).to.be.revertedWith('FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); }); it('FlashMint from a borrower that does not approve the transfer for repayment', async function () { const { gho } = testEnv; const borrowAmount = ethers.utils.parseUnits('1000.0', 18); await flashBorrower.setAllowRepayment(false); // revert expected in transfer from `allowed - amount` will cause an error await expect(flashBorrower.flashBorrow(gho.address, borrowAmount)).to.be.revertedWithPanic( PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW ); await flashBorrower.setAllowRepayment(true); }); it('Update Fee - not permissionned (revert expected)', async function () { const { flashMinter, users } = testEnv; await expect(flashMinter.connect(users[0].signer).updateFee(200)).to.be.revertedWith( 'CALLER_NOT_POOL_ADMIN' ); }); it('Distribute fees to treasury', async function () { const { flashMinter, gho, treasuryAddress } = testEnv; const flashMinterBalance = await gho.balanceOf(flashMinter.address); expect(flashMinterBalance).to.not.be.equal(0); expect(await gho.balanceOf(treasuryAddress)).to.be.equal(0); const tx = await flashMinter.distributeFeesToTreasury(); await expect(tx) .to.emit(flashMinter, 'FeesDistributedToTreasury') .withArgs(treasuryAddress, gho.address, flashMinterBalance); expect(await gho.balanceOf(treasuryAddress)).to.be.equal(flashMinterBalance); expect(await gho.balanceOf(flashMinter.address)).to.be.equal(0); }); it('Update Fee', async function () { const { flashMinter, poolAdmin } = testEnv; const newFlashFee = 200; tx = await flashMinter.connect(poolAdmin.signer).updateFee(newFlashFee); await expect(tx).to.emit(flashMinter, 'FeeUpdated').withArgs(flashFee, newFlashFee); }); it('Check MaxFee amount', async function () { const { flashMinter } = testEnv; const expectedMaxFee = ghoEntityConfig.flashMinterMaxFee; expect(await flashMinter.MAX_FEE()).to.be.equal(expectedMaxFee); }); it('Update Fee to an invalid amount', async function () { const { flashMinter, poolAdmin } = testEnv; const maxFee = await flashMinter.MAX_FEE(); await expect( flashMinter.connect(poolAdmin.signer).updateFee(maxFee.add(1000)) ).to.be.revertedWith('FlashMinter: Fee out of range'); }); it('Deploy GhoFlashMinter with an invalid amount', async function () { const { gho, poolAdmin, pool, treasuryAddress } = testEnv; const addressesProvider = await pool.ADDRESSES_PROVIDER(); const largeFee = ghoEntityConfig.flashMinterMaxFee.add(100); const flashMinterFactory = new GhoFlashMinter__factory(poolAdmin.signer); await expect( flashMinterFactory.deploy(gho.address, treasuryAddress, largeFee, addressesProvider) ).to.be.revertedWith('FlashMinter: Fee out of range'); }); it('Get GhoTreasury', async function () { const { flashMinter, treasuryAddress } = testEnv; expect(await flashMinter.getGhoTreasury()).to.be.equal(treasuryAddress); }); it('Update GhoTreasury - not permissionned (revert expected)', async function () { const { flashMinter, users } = testEnv; await expect( flashMinter.connect(users[0].signer).updateGhoTreasury(ZERO_ADDRESS) ).to.be.revertedWith('CALLER_NOT_POOL_ADMIN'); }); it('Update GhoTreasury', async function () { const { flashMinter, poolAdmin, treasuryAddress } = testEnv; expect(await flashMinter.getGhoTreasury()).to.be.not.eq(ZERO_ADDRESS); await expect(flashMinter.connect(poolAdmin.signer).updateGhoTreasury(ZERO_ADDRESS)) .to.emit(flashMinter, 'GhoTreasuryUpdated') .withArgs(treasuryAddress, ZERO_ADDRESS); expect(await flashMinter.getGhoTreasury()).to.be.equal(ZERO_ADDRESS); }); it('MaxFlashLoan - Address That Is Not GHO', async function () { const { flashMinter } = testEnv; expect(await flashMinter.maxFlashLoan(ONE_ADDRESS)).to.be.equal(0); }); }); ================================================ FILE: test/gho-atoken.test.ts ================================================ import hre from 'hardhat'; import { expect } from 'chai'; import { impersonateAccountHardhat } from '../helpers/misc-utils'; import { makeSuite, TestEnv } from './helpers/make-suite'; import { ONE_ADDRESS, ZERO_ADDRESS } from '../helpers/constants'; import { GhoAToken__factory } from '../types'; import { INITIALIZED, ZERO_ADDRESS_NOT_VALID } from './helpers/constants'; import { ProtocolErrors } from '@aave/core-v3'; makeSuite('Gho AToken End-To-End', (testEnv: TestEnv) => { let ethers; const testAddressOne = '0x2acAb3DEa77832C09420663b0E1cB386031bA17B'; const testAddressTwo = '0x6fC355D4e0EE44b292E50878F49798ff755A5bbC'; let poolSigner; before(async () => { ethers = hre.ethers; const { pool } = testEnv; poolSigner = await impersonateAccountHardhat(pool.address); }); it('Initialize when already initialized (revert expected)', async function () { const { aToken } = testEnv; await expect( aToken.initialize( ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, 0, 'test', 'test', [] ) ).to.be.revertedWith(INITIALIZED); }); it('Initialize with incorrect pool (revert expected)', async function () { const { deployer, pool } = testEnv; const aToken = await new GhoAToken__factory(deployer.signer).deploy(pool.address); await expect( aToken.initialize( ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, 0, 'test', 'test', [] ) ).to.be.revertedWith(ProtocolErrors.POOL_ADDRESSES_DO_NOT_MATCH); }); it('Checks initial parameters', async function () { const { aToken, gho } = testEnv; expect(await aToken.UNDERLYING_ASSET_ADDRESS()).to.be.equal(gho.address); expect(await aToken.ATOKEN_REVISION()).to.be.equal(1); }); it('Checks the domain separator', async () => { const { aToken } = testEnv; const EIP712_REVISION = '1'; const domain = { name: await aToken.name(), version: EIP712_REVISION, chainId: hre.network.config.chainId, verifyingContract: aToken.address, }; const domainSeparator = ethers.utils._TypedDataEncoder.hashDomain(domain); expect(await aToken.DOMAIN_SEPARATOR()).to.be.equal(domainSeparator); }); it('Check permission of onlyPool modified functions (revert expected)', async () => { const { aToken, users } = testEnv; const nonPoolAdmin = users[2]; const randomAddress = ONE_ADDRESS; const randomNumber = '0'; const calls = [ { fn: 'mint', args: [randomAddress, randomAddress, randomNumber, randomNumber] }, { fn: 'burn', args: [randomAddress, randomAddress, randomNumber, randomNumber] }, { fn: 'mintToTreasury', args: [randomNumber, randomNumber] }, { fn: 'transferOnLiquidation', args: [randomAddress, randomAddress, randomNumber] }, { fn: 'transferUnderlyingTo', args: [randomAddress, randomNumber] }, { fn: 'handleRepayment', args: [randomAddress, randomAddress, randomNumber] }, ]; for (const call of calls) { await expect(aToken.connect(nonPoolAdmin.signer)[call.fn](...call.args)).to.be.revertedWith( ProtocolErrors.CALLER_MUST_BE_POOL ); } }); it('Check permission of onlyPoolAdmin modified functions (revert expected)', async () => { const { aToken, users } = testEnv; const nonPoolAdmin = users[2]; const randomAddress = ONE_ADDRESS; const randomNumber = '0'; const calls = [ { fn: 'rescueTokens', args: [randomAddress, randomAddress, randomNumber] }, { fn: 'setVariableDebtToken', args: [randomAddress] }, { fn: 'updateGhoTreasury', args: [randomAddress] }, ]; for (const call of calls) { await expect(aToken.connect(nonPoolAdmin.signer)[call.fn](...call.args)).to.be.revertedWith( ProtocolErrors.CALLER_NOT_POOL_ADMIN ); } }); it('Check operations not permitted (revert expected)', async () => { const { aToken } = testEnv; const randomAddress = ONE_ADDRESS; const randomNumber = '0'; const calls = [ { fn: 'mint', args: [randomAddress, randomAddress, randomNumber, randomNumber] }, { fn: 'burn', args: [randomAddress, randomAddress, randomNumber, randomNumber] }, { fn: 'mintToTreasury', args: [randomNumber, randomNumber] }, { fn: 'transferOnLiquidation', args: [randomAddress, randomAddress, randomNumber] }, { fn: 'transfer', args: [randomAddress, 0] }, { fn: 'permit', args: [ randomAddress, randomAddress, randomNumber, randomNumber, randomNumber, ethers.constants.HashZero, ethers.constants.HashZero, ], }, ]; for (const call of calls) { await expect(aToken.connect(poolSigner)[call.fn](...call.args)).to.be.revertedWith( ProtocolErrors.OPERATION_NOT_SUPPORTED ); } }); it('Get VariableDebtToken', async function () { const { aToken, variableDebtToken } = testEnv; const variableDebtTokenAddress = await aToken.getVariableDebtToken(); expect(variableDebtTokenAddress).to.be.equal(variableDebtToken.address); }); it('Get Treasury', async function () { const { aToken, treasuryAddress } = testEnv; const aTokenTreasuryAddress = await aToken.getGhoTreasury(); expect(aTokenTreasuryAddress).to.be.equal(treasuryAddress); }); it('Burn AToken - not permissioned (revert expected)', async function () { const { aToken, users } = testEnv; await expect( aToken.connect(users[5].signer).burn(testAddressOne, testAddressOne, 1000, 1) ).to.be.revertedWith(ProtocolErrors.CALLER_MUST_BE_POOL); }); it('Get VariableDebtToken', async function () { const { aToken, variableDebtToken } = testEnv; const variableDebtTokenAddress = await aToken.getVariableDebtToken(); expect(variableDebtTokenAddress).to.be.equal(variableDebtToken.address); }); it('Set Treasury', async function () { const { aToken, deployer, treasuryAddress } = testEnv; await expect(aToken.connect(deployer.signer).updateGhoTreasury(testAddressTwo)) .to.emit(aToken, 'GhoTreasuryUpdated') .withArgs(treasuryAddress, testAddressTwo); }); it('Get Treasury', async function () { const { aToken } = testEnv; const ghoTreasury = await aToken.getGhoTreasury(); expect(ghoTreasury).to.be.equal(testAddressTwo); }); it('Set VariableDebtToken - already set (revert expected)', async function () { const { aToken, poolAdmin } = testEnv; await expect( aToken.connect(poolAdmin.signer).setVariableDebtToken(testAddressTwo) ).to.be.revertedWith('VARIABLE_DEBT_TOKEN_ALREADY_SET'); }); it('Set ZERO address as VariableDebtToken (revert expected)', async function () { const { users: [user1], pool, poolAdmin, } = testEnv; const newGhoAToken = await new GhoAToken__factory(user1.signer).deploy(pool.address); await expect( newGhoAToken.connect(poolAdmin.signer).setVariableDebtToken(ZERO_ADDRESS) ).to.be.revertedWith(ZERO_ADDRESS_NOT_VALID); }); it('Set ZERO address as Treasury (revert expected)', async function () { const { aToken, poolAdmin } = testEnv; await expect( aToken.connect(poolAdmin.signer).updateGhoTreasury(ZERO_ADDRESS) ).to.be.revertedWith(ZERO_ADDRESS_NOT_VALID); }); it('Set ZERO address as VariableDebtToken (revert expected)', async function () { const { users: [user1], pool, poolAdmin, } = testEnv; const newGhoAToken = await new GhoAToken__factory(user1.signer).deploy(pool.address); await expect( newGhoAToken.connect(poolAdmin.signer).setVariableDebtToken(ZERO_ADDRESS) ).to.be.revertedWith(ZERO_ADDRESS_NOT_VALID); }); it('Set ZERO address as Treasury (revert expected)', async function () { const { aToken, poolAdmin } = testEnv; await expect( aToken.connect(poolAdmin.signer).updateGhoTreasury(ZERO_ADDRESS) ).to.be.revertedWith(ZERO_ADDRESS_NOT_VALID); }); it('Total Supply - always zero', async function () { const { aToken } = testEnv; await expect(await aToken.totalSupply()).to.be.equal(0); }); it('User balanceOf - always zero', async function () { const { aToken, users } = testEnv; for (const user of users) { await expect(await aToken.balanceOf(user.address)).to.be.eq(0); } }); it('User nonces - always zero', async function () { const { aToken, users } = testEnv; for (const user of users) { await expect(await aToken.nonces(user.address)).to.be.eq(0); } }); it('PoolAdmin rescue tokens from AToken', async () => { const { poolAdmin, pool, gho, usdc, aToken, users: [locker], } = testEnv; const amountToLock = 2; // Lock GHO const aTokenGhoBalanceBefore = await gho.balanceOf(aToken.address); const aTokenSigner = await impersonateAccountHardhat(aToken.address); expect(await gho.connect(aTokenSigner).mint(aToken.address, amountToLock)); expect(await gho.balanceOf(aToken.address)).to.be.eq(aTokenGhoBalanceBefore.add(amountToLock)); // Underlying cannot be rescued await expect( aToken.connect(poolAdmin.signer).rescueTokens(gho.address, locker.address, 2) ).to.be.revertedWith(ProtocolErrors.UNDERLYING_CANNOT_BE_RESCUED); expect(await gho.balanceOf(aToken.address)).to.be.eq(aTokenGhoBalanceBefore.add(amountToLock)); // Lock USDC const aTokenUsdcBalanceBefore = await usdc.balanceOf(aToken.address); expect(await usdc.connect(locker.signer).transfer(aToken.address, amountToLock)); expect(await usdc.balanceOf(aToken.address)).to.be.eq( aTokenUsdcBalanceBefore.add(amountToLock) ); // Rescue expect( await aToken .connect(poolAdmin.signer) .rescueTokens(usdc.address, locker.address, amountToLock) ); }); }); ================================================ FILE: test/gho-oracle.test.ts ================================================ import hre from 'hardhat'; import { expect } from 'chai'; import { makeSuite, TestEnv } from './helpers/make-suite'; import './helpers/math/wadraymath'; makeSuite('AaveOracle', (testEnv: TestEnv) => { let ethers; const GHO_ORACLE_DECIMALS = 8; let ghoPrice; before(async () => { ethers = hre.ethers; ghoPrice = ethers.utils.parseUnits('1', 8); }); it('Check initial config params of GHO oracle', async () => { const { ghoOracle } = testEnv; expect(await ghoOracle.decimals()).to.equal(GHO_ORACLE_DECIMALS); }); it('Check price of GHO via GHO oracle', async () => { const { ghoOracle } = testEnv; expect(await ghoOracle.latestAnswer()).to.equal(ghoPrice); }); it('Check price of GHO via AaveOracle', async () => { const { aaveOracle, gho } = testEnv; expect(await aaveOracle.getAssetPrice(gho.address)).to.equal(ghoPrice); }); }); ================================================ FILE: test/gho-stable-debt.test.ts ================================================ import hre from 'hardhat'; import { expect } from 'chai'; import { ProtocolErrors } from '@aave/core-v3'; import { impersonateAccountHardhat } from '../helpers/misc-utils'; import { makeSuite, TestEnv } from './helpers/make-suite'; import { ONE_ADDRESS, ZERO_ADDRESS } from '../helpers/constants'; import { GhoStableDebtToken__factory } from '../types'; import { INITIALIZED } from './helpers/constants'; import { evmRevert, evmSnapshot, getPoolConfiguratorProxy } from '@aave/deploy-v3'; makeSuite('Gho StableDebtToken End-To-End', (testEnv: TestEnv) => { let ethers; let poolSigner; before(async () => { ethers = hre.ethers; const { pool } = testEnv; poolSigner = await impersonateAccountHardhat(pool.address); }); it('Initialize when already initialized (revert expected)', async function () { const { stableDebtToken } = testEnv; await expect( stableDebtToken.initialize(ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, 0, 'test', 'test', []) ).to.be.revertedWith(INITIALIZED); }); it('Initialize with incorrect pool (revert expected)', async function () { const { deployer, pool } = testEnv; const stableDebtToken = await new GhoStableDebtToken__factory(deployer.signer).deploy( pool.address ); await expect( stableDebtToken.initialize(ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, 0, 'test', 'test', []) ).to.be.revertedWith(ProtocolErrors.POOL_ADDRESSES_DO_NOT_MATCH); }); it('Checks initial parameters', async function () { const { stableDebtToken, gho } = testEnv; expect(await stableDebtToken.UNDERLYING_ASSET_ADDRESS()).to.be.equal(gho.address); expect(await stableDebtToken.DEBT_TOKEN_REVISION()).to.be.equal(1); }); it('Check permission of onlyPool modified functions (revert expected)', async () => { const { stableDebtToken, users } = testEnv; const nonPoolAdmin = users[2]; const randomAddress = ONE_ADDRESS; const randomNumber = '0'; const calls = [ { fn: 'mint', args: [randomAddress, randomAddress, randomNumber, randomNumber] }, { fn: 'burn', args: [randomAddress, randomNumber] }, ]; for (const call of calls) { await expect( stableDebtToken.connect(nonPoolAdmin.signer)[call.fn](...call.args) ).to.be.revertedWith(ProtocolErrors.CALLER_MUST_BE_POOL); } }); it('Check operations not permitted (revert expected)', async () => { const { stableDebtToken } = testEnv; const randomAddress = ONE_ADDRESS; const randomNumber = '0'; const calls = [ { fn: 'mint', args: [randomAddress, randomAddress, randomNumber, randomNumber] }, { fn: 'burn', args: [randomAddress, randomNumber] }, { fn: 'transfer', args: [randomAddress, randomNumber] }, { fn: 'allowance', args: [randomAddress, randomAddress] }, { fn: 'approve', args: [randomAddress, randomNumber] }, { fn: 'transferFrom', args: [randomAddress, randomAddress, randomNumber] }, { fn: 'increaseAllowance', args: [randomAddress, randomNumber] }, { fn: 'decreaseAllowance', args: [randomAddress, randomNumber] }, ]; for (const call of calls) { await expect(stableDebtToken.connect(poolSigner)[call.fn](...call.args)).to.be.revertedWith( ProtocolErrors.OPERATION_NOT_SUPPORTED ); } }); it('User nonces - always zero', async function () { const { stableDebtToken, users } = testEnv; for (const user of users) { await expect(await stableDebtToken.nonces(user.address)).to.be.eq(0); } }); it('User tries to borrow GHO in stable mode with stable borrowing disabled (revert expected)', async function () { const { users, pool, weth, gho, stableDebtToken } = testEnv; const collateralAmount = ethers.utils.parseUnits('1000.0', 18); const borrowAmount = ethers.utils.parseUnits('1.0', 18); await weth.connect(users[0].signer).approve(pool.address, collateralAmount); await pool .connect(users[0].signer) .deposit(weth.address, collateralAmount, users[0].address, 0); await expect( pool.connect(users[0].signer).borrow(gho.address, borrowAmount, 1, 0, users[0].address) ).to.be.revertedWith(ProtocolErrors.STABLE_BORROWING_NOT_ENABLED); expect(await gho.balanceOf(users[0].address)).to.be.equal(0); expect(await stableDebtToken.totalSupply()).to.be.equal(0); expect(await stableDebtToken.balanceOf(users[0].address)).to.be.equal(0); }); it('User tries to borrow GHO in stable mode with borrowing enabled (revert expected)', async function () { const { users, pool, poolAdmin, weth, gho, stableDebtToken } = testEnv; const collateralAmount = ethers.utils.parseUnits('1000.0', 18); const borrowAmount = ethers.utils.parseUnits('1.0', 18); const snapId = await evmSnapshot(); // PoolAdmin enables stable borrowing for GHO const poolConfigurator = await getPoolConfiguratorProxy(); await expect( poolConfigurator.connect(poolAdmin.signer).setReserveStableRateBorrowing(gho.address, true) ) .to.emit(poolConfigurator, 'ReserveStableRateBorrowing') .withArgs(gho.address, true); // User tries to borrow in stable mode and stable size validation reverts due to zero available liquidity await weth.connect(users[3].signer).approve(pool.address, collateralAmount); await pool .connect(users[3].signer) .deposit(weth.address, collateralAmount, users[3].address, 0); await expect( pool.connect(users[3].signer).borrow(gho.address, borrowAmount, 1, 0, users[3].address) ).to.be.revertedWith(ProtocolErrors.AMOUNT_BIGGER_THAN_MAX_LOAN_SIZE_STABLE); expect(await gho.balanceOf(users[3].address)).to.be.equal(0); expect(await stableDebtToken.totalSupply()).to.be.equal(0); expect(await stableDebtToken.balanceOf(users[0].address)).to.be.equal(0); await evmRevert(snapId); }); }); ================================================ FILE: test/gho-token-permit.test.ts ================================================ import hre from 'hardhat'; import { expect } from 'chai'; import { SignerWithAddress } from './helpers/make-suite'; import { GhoToken__factory, IGhoToken } from '../types'; import { BigNumber } from 'ethers'; import { HARDHAT_CHAINID, MAX_UINT_AMOUNT, ZERO_ADDRESS } from './../helpers/constants'; import { buildPermitParams, getSignatureFromTypedData } from './helpers/helpers'; describe('GhoToken Unit Test', () => { let ethers; let ghoTokenFactory: GhoToken__factory; let users: SignerWithAddress[] = []; let facilitator1: SignerWithAddress; let facilitator1Label: string; let facilitator1Cap: BigNumber; let facilitator1Config: IGhoToken.FacilitatorStruct; let facilitator2: SignerWithAddress; let ghoToken; const EIP712_REVISION = '1'; before(async () => { ethers = hre.ethers; const signers = await ethers.getSigners(); for (const signer of signers) { users.push({ signer, address: await signer.getAddress(), }); } // setup facilitator1 facilitator1 = users[1]; facilitator1Label = 'Alice_Facilitator'; facilitator1Cap = ethers.utils.parseUnits('100000000', 18); facilitator1Config = { bucketCapacity: facilitator1Cap, bucketLevel: 0, label: facilitator1Label, }; // setup facilitator2 facilitator2 = users[2]; ghoTokenFactory = new GhoToken__factory(users[0].signer); }); it('Deploys GHO and adds the first facilitator', async function () { ghoToken = await ghoTokenFactory.deploy(users[0].address); const FACILITATOR_MANAGER_ROLE = ethers.utils.id('FACILITATOR_MANAGER_ROLE'); const BUCKET_MANAGER_ROLE = ethers.utils.id('BUCKET_MANAGER_ROLE'); const grantFacilitatorRoleTx = await ghoToken .connect(users[0].signer) .grantRole(FACILITATOR_MANAGER_ROLE, users[0].address); const grantBucketRoleTx = await ghoToken .connect(users[0].signer) .grantRole(BUCKET_MANAGER_ROLE, users[0].address); await expect(grantFacilitatorRoleTx).to.emit(ghoToken, 'RoleGranted'); await expect(grantBucketRoleTx).to.emit(ghoToken, 'RoleGranted'); const labelHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(facilitator1Label)); await expect( ghoToken .connect(users[0].signer) .addFacilitator( facilitator1.address, facilitator1Config.label, facilitator1Config.bucketCapacity ) ) .to.emit(ghoToken, 'FacilitatorAdded') .withArgs(facilitator1.address, labelHash, facilitator1Cap); }); it('Mint from facilitator 1', async function () { const mintAmount = ethers.utils.parseUnits('250000.0', 18); await expect(ghoToken.connect(facilitator1.signer).mint(facilitator1.address, mintAmount)) .to.emit(ghoToken, 'Transfer') .withArgs(ZERO_ADDRESS, facilitator1.address, mintAmount) .to.emit(ghoToken, 'FacilitatorBucketLevelUpdated') .withArgs(facilitator1.address, 0, mintAmount); const [, level] = await ghoToken.getFacilitatorBucket(facilitator1.address); expect(level).to.be.equal(mintAmount); }); it('Checks the domain separator', async () => { const separator = await ghoToken.connect(facilitator1.signer).DOMAIN_SEPARATOR(); const domain = { name: await ghoToken.name(), version: EIP712_REVISION, chainId: hre.network.config.chainId, verifyingContract: ghoToken.address, }; const domainSeparator = ethers.utils._TypedDataEncoder.hashDomain(domain); expect(separator).to.be.equal(domainSeparator); }); it('Submits a permit with 0 expiration (revert expected)', async () => { const owner = await ethers.Wallet.createRandom(); const spender = facilitator2; const tokenName = await ghoToken.name(); const chainId = hre.network.config.chainId || HARDHAT_CHAINID; const expiration = 0; const nonce = (await ghoToken.nonces(owner.address)).toNumber(); const permitAmount = ethers.utils.parseEther('2').toString(); const msgParams = buildPermitParams( chainId, ghoToken.address, EIP712_REVISION, tokenName, owner.address, spender.address, nonce, permitAmount, expiration.toFixed() ); expect((await ghoToken.allowance(owner.address, spender.address)).toString()).to.be.equal( '0', 'INVALID_ALLOWANCE_BEFORE_PERMIT' ); const { v, r, s } = getSignatureFromTypedData(owner.privateKey, msgParams); await expect( ghoToken .connect(spender.signer) .permit(owner.address, spender.address, permitAmount, expiration, v, r, s) ).to.be.revertedWith('PERMIT_DEADLINE_EXPIRED'); expect((await ghoToken.allowance(owner.address, spender.address)).toString()).to.be.equal('0'); }); it('Submits a permit with maximum expiration length', async () => { const owner = await ethers.Wallet.createRandom(); const spender = facilitator2; const chainId = hre.network.config.chainId || HARDHAT_CHAINID; const deadline = MAX_UINT_AMOUNT; const nonce = (await ghoToken.nonces(owner.address)).toNumber(); const permitAmount = ethers.utils.parseEther('2').toString(); const msgParams = buildPermitParams( chainId, ghoToken.address, EIP712_REVISION, await ghoToken.name(), owner.address, spender.address, nonce, deadline, permitAmount ); expect((await ghoToken.allowance(owner.address, spender.address)).toString()).to.be.equal( '0', 'INVALID_ALLOWANCE_BEFORE_PERMIT' ); const { v, r, s } = getSignatureFromTypedData(owner.privateKey, msgParams); expect( await ghoToken .connect(spender.signer) .permit(owner.address, spender.address, permitAmount, deadline, v, r, s) ); expect((await ghoToken.nonces(owner.address)).toNumber()).to.be.equal(1); }); it('Cancels the previous permit', async () => { const owner = await ethers.Wallet.createRandom(); const spender = facilitator2; const chainId = hre.network.config.chainId || HARDHAT_CHAINID; const deadline = MAX_UINT_AMOUNT; const permitAmount = ethers.utils.parseEther('2').toString(); const msgParams = buildPermitParams( chainId, ghoToken.address, EIP712_REVISION, await ghoToken.name(), owner.address, spender.address, (await ghoToken.nonces(owner.address)).toNumber(), deadline, permitAmount ); const { v, r, s } = getSignatureFromTypedData(owner.privateKey, msgParams); expect( await ghoToken .connect(spender.signer) .permit(owner.address, spender.address, permitAmount, deadline, v, r, s) ); expect((await ghoToken.allowance(owner.address, spender.address)).toString()).to.be.equal( ethers.utils.parseEther('2') ); const newPermitAmount = '0'; const newMsgParams = buildPermitParams( chainId, ghoToken.address, EIP712_REVISION, await ghoToken.name(), owner.address, spender.address, (await ghoToken.nonces(owner.address)).toNumber(), deadline, newPermitAmount ); const { v: newV, r: newR, s: newS } = getSignatureFromTypedData(owner.privateKey, newMsgParams); expect( await ghoToken .connect(spender.signer) .permit(owner.address, spender.address, newPermitAmount, deadline, newV, newR, newS) ); expect((await ghoToken.allowance(owner.address, spender.address)).toString()).to.be.equal( newPermitAmount, 'INVALID_ALLOWANCE_AFTER_PERMIT' ); expect((await ghoToken.nonces(owner.address)).toNumber()).to.be.equal(2); }); it('Tries to submit a permit with invalid nonce (revert expected)', async () => { const owner = await ethers.Wallet.createRandom(); const spender = facilitator2; const chainId = hre.network.config.chainId || HARDHAT_CHAINID; const deadline = MAX_UINT_AMOUNT; const nonce = 1000; const permitAmount = '0'; const msgParams = buildPermitParams( chainId, ghoToken.address, EIP712_REVISION, await ghoToken.name(), owner.address, spender.address, nonce, deadline, permitAmount ); const { v, r, s } = getSignatureFromTypedData(owner.privateKey, msgParams); await expect( ghoToken .connect(spender.signer) .permit(owner.address, spender.address, permitAmount, deadline, v, r, s) ).to.be.revertedWith('INVALID_SIGNER'); }); it('Tries to submit a permit with invalid expiration (previous to the current block) (revert expected)', async () => { const owner = await ethers.Wallet.createRandom(); const spender = facilitator2; const chainId = hre.network.config.chainId || HARDHAT_CHAINID; const expiration = '1'; const nonce = (await ghoToken.nonces(owner.address)).toNumber(); const permitAmount = '0'; const msgParams = buildPermitParams( chainId, ghoToken.address, EIP712_REVISION, await ghoToken.name(), owner.address, spender.address, nonce, expiration, permitAmount ); const { v, r, s } = getSignatureFromTypedData(owner.privateKey, msgParams); await expect( ghoToken .connect(spender.signer) .permit(owner.address, spender.address, expiration, permitAmount, v, r, s) ).to.be.revertedWith('PERMIT_DEADLINE_EXPIRED'); }); it('Tries to submit a permit with invalid signature (revert expected)', async () => { const owner = await ethers.Wallet.createRandom(); const spender = facilitator2; const chainId = hre.network.config.chainId || HARDHAT_CHAINID; const deadline = MAX_UINT_AMOUNT; const nonce = (await ghoToken.nonces(owner.address)).toNumber(); const permitAmount = '0'; const msgParams = buildPermitParams( chainId, ghoToken.address, EIP712_REVISION, await ghoToken.name(), owner.address, spender.address, nonce, deadline, permitAmount ); const { v, r, s } = getSignatureFromTypedData(owner.privateKey, msgParams); await expect( ghoToken .connect(spender.signer) .permit(owner.address, ZERO_ADDRESS, permitAmount, deadline, v, r, s) ).to.be.revertedWith('INVALID_SIGNER'); }); it('Tries to submit a permit with invalid owner (revert expected)', async () => { const owner = await ethers.Wallet.createRandom(); const spender = facilitator2; const chainId = hre.network.config.chainId || HARDHAT_CHAINID; const deadline = MAX_UINT_AMOUNT; const nonce = (await ghoToken.nonces(owner.address)).toNumber(); const permitAmount = '0'; const msgParams = buildPermitParams( chainId, ghoToken.address, EIP712_REVISION, await ghoToken.name(), owner.address, spender.address, nonce, deadline, permitAmount ); const { v, r, s } = getSignatureFromTypedData(owner.privateKey, msgParams); await expect( ghoToken .connect(spender.signer) .permit(ZERO_ADDRESS, spender.address, permitAmount, deadline, v, r, s) ).to.be.revertedWith('INVALID_SIGNER'); }); }); ================================================ FILE: test/gho-token-unit.test.ts ================================================ import hre from 'hardhat'; import { expect } from 'chai'; import { PANIC_CODES } from '@nomicfoundation/hardhat-chai-matchers/panic'; import { SignerWithAddress } from './helpers/make-suite'; import { ghoTokenConfig } from '../helpers/config'; import { GhoToken__factory, IGhoToken } from '../types'; import { HardhatEthersHelpers } from '@nomiclabs/hardhat-ethers/types'; import { BigNumber } from 'ethers'; import { ZERO_ADDRESS } from '../helpers/constants'; import { keccak256, toUtf8Bytes } from 'ethers/lib/utils'; describe('GhoToken Unit Test', () => { let ethers: typeof import('ethers/lib/ethers') & HardhatEthersHelpers; let ghoTokenFactory: GhoToken__factory; let users: SignerWithAddress[] = []; let facilitator1: SignerWithAddress; let facilitator1Label: string; let facilitator1Cap: BigNumber; let facilitator1UpdatedCap: BigNumber; let facilitator1Config: IGhoToken.FacilitatorStruct; let facilitator2: SignerWithAddress; let facilitator2Label: string; let facilitator2Cap: BigNumber; let facilitator2Config: IGhoToken.FacilitatorStruct; let facilitator3: SignerWithAddress; let facilitator3Label: string; let facilitator3Cap: BigNumber; let facilitator3Config: IGhoToken.FacilitatorStruct; let facilitator4: SignerWithAddress; let facilitator4Label: string; let facilitator4Cap: BigNumber; let facilitator4Config: IGhoToken.FacilitatorStruct; let facilitator5: SignerWithAddress; let facilitator5Label: string; let facilitator5Cap: BigNumber; let facilitator5Config: IGhoToken.FacilitatorStruct; let ghoToken; let BUCKET_MANAGER_ROLE: string; let FACILITATOR_MANAGER_ROLE: string; before(async () => { ethers = hre.ethers; BUCKET_MANAGER_ROLE = ethers.utils.id('BUCKET_MANAGER_ROLE'); FACILITATOR_MANAGER_ROLE = ethers.utils.id('FACILITATOR_MANAGER_ROLE'); const signers = await ethers.getSigners(); for (const signer of signers) { users.push({ signer, address: await signer.getAddress(), }); } // setup facilitator1 facilitator1 = users[1]; facilitator1Label = 'Alice_Facilitator'; facilitator1Cap = ethers.utils.parseUnits('100000000', 18); facilitator1UpdatedCap = ethers.utils.parseUnits('900000000', 18); facilitator1Config = { bucketCapacity: facilitator1Cap, bucketLevel: 0, label: facilitator1Label, }; // setup facilitator2 facilitator2 = users[2]; facilitator2Label = 'Bob_Facilitator'; facilitator2Cap = ethers.utils.parseUnits('200000000', 18); facilitator2Config = { bucketCapacity: facilitator2Cap, bucketLevel: 0, label: facilitator2Label, }; // setup facilitator3 facilitator3 = users[3]; facilitator3Label = 'Cat_Facilitator'; facilitator3Cap = ethers.utils.parseUnits('300000000', 18); facilitator3Config = { bucketCapacity: facilitator3Cap, bucketLevel: 0, label: facilitator3Label, }; // setup facilitator3 facilitator4 = users[4]; facilitator4Label = 'Dom_Facilitator'; facilitator4Cap = ethers.utils.parseUnits('400000000', 18); facilitator4Config = { bucketCapacity: facilitator4Cap, bucketLevel: 0, label: facilitator4Label, }; // setup facilitator3 facilitator5 = users[5]; facilitator5Label = 'Ed_Facilitator'; facilitator5Cap = ethers.utils.parseUnits('500000000', 18); facilitator5Config = { bucketCapacity: facilitator5Cap, bucketLevel: 0, label: facilitator5Label, }; ghoTokenFactory = new GhoToken__factory(users[0].signer); }); it('Deploy GhoToken without facilitators', async function () { const tempGhoToken = await ghoTokenFactory.deploy(users[0].address); const { TOKEN_DECIMALS, TOKEN_NAME, TOKEN_SYMBOL } = ghoTokenConfig; expect(await tempGhoToken.decimals()).to.be.equal(TOKEN_DECIMALS); expect(await tempGhoToken.name()).to.be.equal(TOKEN_NAME); expect(await tempGhoToken.symbol()).to.be.equal(TOKEN_SYMBOL); expect((await tempGhoToken.getFacilitatorsList()).length).to.be.equal(0); }); it('Deploys GHO and adds the first facilitator', async function () { ghoToken = await ghoTokenFactory.deploy(users[0].address); const deploymentReceipt = await ethers.provider.getTransactionReceipt( ghoToken.deployTransaction.hash ); expect(deploymentReceipt.logs.length).to.be.equal(1); const ownershipEvent = ghoToken.interface.parseLog(deploymentReceipt.logs[0]); const DEFAULT_ADMIN_ROLE = ethers.constants.HashZero; expect(ownershipEvent.name).to.equal('RoleGranted'); expect(ownershipEvent.args.role).to.equal(DEFAULT_ADMIN_ROLE); expect(ownershipEvent.args.account).to.equal(users[0].address); const grantFacilitatorRoleTx = await ghoToken .connect(users[0].signer) .grantRole(FACILITATOR_MANAGER_ROLE, users[0].address); const grantBucketRoleTx = await ghoToken .connect(users[0].signer) .grantRole(BUCKET_MANAGER_ROLE, users[0].address); await expect(grantFacilitatorRoleTx).to.emit(ghoToken, 'RoleGranted'); await expect(grantBucketRoleTx).to.emit(ghoToken, 'RoleGranted'); const labelHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(facilitator1Label)); const addFacilitatorTx = await ghoToken .connect(users[0].signer) .addFacilitator( facilitator1.address, facilitator1Config.label, facilitator1Config.bucketCapacity ); await expect(addFacilitatorTx) .to.emit(ghoToken, 'FacilitatorAdded') .withArgs(facilitator1.address, labelHash, facilitator1Cap); const { TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS } = ghoTokenConfig; expect(await ghoToken.decimals()).to.be.equal(TOKEN_DECIMALS); expect(await ghoToken.name()).to.be.equal(TOKEN_NAME); expect(await ghoToken.symbol()).to.be.equal(TOKEN_SYMBOL); const facilitatorList = await ghoToken.getFacilitatorsList(); expect(facilitatorList.length).to.be.equal(1); let facilitatorAddr = facilitatorList[0]; let facilitator = await ghoToken.getFacilitator(facilitatorAddr); expect(facilitator.label).to.be.equal(facilitator1Label); expect(facilitator.bucketLevel).to.be.equal(0); expect(facilitator.bucketCapacity).to.be.equal(facilitator1Cap); }); it('Adds a second facilitator', async function () { const addFacilitatorTx = await ghoToken .connect(users[0].signer) .addFacilitator( facilitator2.address, facilitator2Config.label, facilitator2Config.bucketCapacity ); const facilitatorList = await ghoToken.getFacilitatorsList(); expect(facilitatorList.length).to.be.equal(2); let facilitatorAddr = facilitatorList[1]; let facilitator = await ghoToken.getFacilitator(facilitatorAddr); expect(facilitator.label).to.be.equal(facilitator2Label); expect(facilitator.bucketLevel).to.be.equal(0); // level should be 0 expect(facilitator.bucketCapacity).to.be.equal(facilitator2Cap); }); it('Mint from facilitator 1', async function () { const mintAmount = ethers.utils.parseUnits('250000.0', 18); await expect(ghoToken.connect(facilitator1.signer).mint(facilitator1.address, mintAmount)) .to.emit(ghoToken, 'Transfer') .withArgs(ZERO_ADDRESS, facilitator1.address, mintAmount) .to.emit(ghoToken, 'FacilitatorBucketLevelUpdated') .withArgs(facilitator1.address, 0, mintAmount); const [, level] = await ghoToken.getFacilitatorBucket(facilitator1.address); expect(level).to.be.equal(mintAmount); }); it('Mint from facilitator 2', async function () { const mintAmount = ethers.utils.parseUnits('500000.0', 18); await expect(ghoToken.connect(facilitator2.signer).mint(facilitator2.address, mintAmount)) .to.emit(ghoToken, 'Transfer') .withArgs(ZERO_ADDRESS, facilitator2.address, mintAmount) .to.emit(ghoToken, 'FacilitatorBucketLevelUpdated') .withArgs(facilitator2.address, 0, mintAmount); const [, level] = await ghoToken.getFacilitatorBucket(facilitator2.address); expect(level).to.be.equal(mintAmount); }); it('Mint from non-facilitator - (revert expected)', async function () { const mintAmount = ethers.utils.parseUnits('500000.0', 18); await expect( ghoToken.connect(users[0].signer).mint(users[0].address, mintAmount) ).to.be.revertedWith('FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); }); it('Mint exceeding bucket capacity - (revert expected)', async function () { await expect( ghoToken.connect(facilitator1.signer).mint(facilitator1.address, facilitator1Cap) ).to.be.revertedWith('FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); }); it('Burn from facilitator 1', async function () { const previouslyMinted = ethers.utils.parseUnits('250000.0', 18); const burnAmount = ethers.utils.parseUnits('250000.0', 18); await expect(ghoToken.connect(facilitator1.signer).burn(burnAmount)) .to.emit(ghoToken, 'Transfer') .withArgs(facilitator1.address, ZERO_ADDRESS, burnAmount) .to.emit(ghoToken, 'FacilitatorBucketLevelUpdated') .withArgs(facilitator1.address, previouslyMinted, previouslyMinted.sub(burnAmount)); const [, level] = await ghoToken.getFacilitatorBucket(facilitator1.address); expect(level).to.be.equal(previouslyMinted.sub(burnAmount)); }); it('Burn from facilitator 2', async function () { const previouslyMinted = ethers.utils.parseUnits('500000.0', 18); const burnAmount = ethers.utils.parseUnits('250000.0', 18); await expect(ghoToken.connect(facilitator2.signer).burn(burnAmount)) .to.emit(ghoToken, 'Transfer') .withArgs(facilitator2.address, ZERO_ADDRESS, burnAmount) .to.emit(ghoToken, 'FacilitatorBucketLevelUpdated') .withArgs(facilitator2.address, previouslyMinted, previouslyMinted.sub(burnAmount)); const [, level] = await ghoToken.getFacilitatorBucket(facilitator2.address); expect(level).to.be.equal(previouslyMinted.sub(burnAmount)); }); it('Burn more than minted facilitator 1 - (revert expected)', async function () { const burnAmount = ethers.utils.parseUnits('250000.0', 18); await expect(ghoToken.connect(facilitator1.signer).burn(burnAmount)).to.be.revertedWithPanic( PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW ); }); it('Burn from a non-facilitator - (revert expected)', async function () { const burnAmount = ethers.utils.parseUnits('250000.0', 18); await expect(ghoToken.connect(users[0].signer).burn(burnAmount)).to.be.revertedWithPanic( PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW ); }); it('Update facilitator1 capacity', async function () { await expect( ghoToken.setFacilitatorBucketCapacity(facilitator1.address, facilitator1UpdatedCap) ) .to.emit(ghoToken, 'FacilitatorBucketCapacityUpdated') .withArgs(facilitator1.address, facilitator1Cap, facilitator1UpdatedCap); const [capacity] = await ghoToken.getFacilitatorBucket(facilitator1.address); expect(capacity).to.be.equal(facilitator1UpdatedCap); }); it('Update facilitator1 capacity from non-owner - (revert expected)', async function () { await expect( ghoToken .connect(facilitator1.signer) .setFacilitatorBucketCapacity(facilitator1.address, facilitator1UpdatedCap) ).to.be.revertedWith( 'AccessControl: account 0x' + BigInt(facilitator1.address).toString(16) + ' is missing role ' + BUCKET_MANAGER_ROLE ); }); it('Update capacity of a non-existent facilitator - (revert expected)', async function () { await expect( ghoToken.setFacilitatorBucketCapacity(users[0].address, facilitator1UpdatedCap) ).to.be.revertedWith('FACILITATOR_DOES_NOT_EXIST'); }); it('Mint after facilitator1 capacity increase', async function () { const mintAmount = facilitator1Cap; await expect(ghoToken.connect(facilitator1.signer).mint(facilitator1.address, mintAmount)) .to.emit(ghoToken, 'Transfer') .withArgs(ZERO_ADDRESS, facilitator1.address, mintAmount) .to.emit(ghoToken, 'FacilitatorBucketLevelUpdated') .withArgs(facilitator1.address, 0, mintAmount); const [, level] = await ghoToken.getFacilitatorBucket(facilitator1.address); expect(level).to.be.equal(mintAmount); }); // adding facilitators it('Add one facilitator', async function () { const labelHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(facilitator3Label)); await expect( ghoToken.addFacilitator( facilitator3.address, facilitator3Config.label, facilitator3Config.bucketCapacity ) ) .to.emit(ghoToken, 'FacilitatorAdded') .withArgs(facilitator3.address, labelHash, facilitator3Cap); const facilitatorList = await ghoToken.getFacilitatorsList(); expect(facilitatorList.length).to.be.equal(3); }); it('Add facilitator from non-owner - (revert expected)', async function () { await expect( ghoToken .connect(facilitator1.signer) .addFacilitator( facilitator4.address, facilitator4Config.label, facilitator4Config.bucketCapacity ) ).to.be.revertedWith( 'AccessControl: account 0x' + BigInt(facilitator1.address).toString(16) + ' is missing role ' + FACILITATOR_MANAGER_ROLE ); }); it('Add facilitator already added - (revert expected)', async function () { await expect( ghoToken.addFacilitator( facilitator1.address, facilitator1Config.label, facilitator1Config.bucketCapacity ) ).to.be.revertedWith('FACILITATOR_ALREADY_EXISTS'); }); it('Add facilitator with invalid label - (revert expected)', async function () { facilitator4Config.label = ''; await expect( ghoToken.addFacilitator( facilitator4.address, facilitator4Config.label, facilitator4Config.bucketCapacity ) ).to.be.revertedWith('INVALID_LABEL'); // reset facilitator 4 label facilitator4Config.label = facilitator4Label; }); it('Add two facilitator', async function () { const label4Hash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(facilitator4Label)); const label5Hash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(facilitator5Label)); await expect( ghoToken.addFacilitator( facilitator4.address, facilitator4Config.label, facilitator4Config.bucketCapacity ) ) .to.emit(ghoToken, 'FacilitatorAdded') .withArgs(facilitator4.address, label4Hash, facilitator4Cap); await expect( ghoToken.addFacilitator( facilitator5.address, facilitator5Config.label, facilitator5Config.bucketCapacity ) ) .to.emit(ghoToken, 'FacilitatorAdded') .withArgs(facilitator5.address, label5Hash, facilitator5Cap); const facilitatorList = await ghoToken.getFacilitatorsList(); expect(facilitatorList.length).to.be.equal(5); }); // remove facilitators it('Remove facilitator from non-owner - (revert expected)', async function () { await expect( ghoToken.connect(facilitator1.signer).removeFacilitator(facilitator3.address) ).to.be.revertedWith( 'AccessControl: account 0x' + BigInt(facilitator1.address).toString(16) + ' is missing role ' + FACILITATOR_MANAGER_ROLE ); }); it('Remove facilitator3', async function () { await expect(ghoToken.removeFacilitator(facilitator3.address)) .to.emit(ghoToken, 'FacilitatorRemoved') .withArgs(facilitator3.address); const facilitatorList = await ghoToken.getFacilitatorsList(); expect(facilitatorList.length).to.be.equal(4); expect(facilitatorList[0]).to.be.equal(facilitator1.address); expect(facilitatorList[1]).to.be.equal(facilitator2.address); expect(facilitatorList[2]).to.be.equal(facilitator5.address); expect(facilitatorList[3]).to.be.equal(facilitator4.address); }); it('Remove facilitator3 that does not exist - (revert expected)', async function () { await expect(ghoToken.removeFacilitator(facilitator3.address)).to.be.revertedWith( 'FACILITATOR_DOES_NOT_EXIST' ); const facilitatorList = await ghoToken.getFacilitatorsList(); expect(facilitatorList.length).to.be.equal(4); expect(facilitatorList[0]).to.be.equal(facilitator1.address); expect(facilitatorList[1]).to.be.equal(facilitator2.address); expect(facilitatorList[2]).to.be.equal(facilitator5.address); expect(facilitatorList[3]).to.be.equal(facilitator4.address); }); it('Remove facilitator2 - (revert expected)', async function () { await expect(ghoToken.removeFacilitator(facilitator2.address)).to.be.revertedWith( 'FACILITATOR_BUCKET_LEVEL_NOT_ZERO' ); }); it('Attempt empty burn', async function () { await expect(ghoToken.connect(users[6].signer).burn(0)).to.be.revertedWith( 'INVALID_BURN_AMOUNT' ); }); }); ================================================ FILE: test/gho-variable-debt.test.ts ================================================ import hre from 'hardhat'; import { expect } from 'chai'; import { makeSuite, TestEnv } from './helpers/make-suite'; import { impersonateAccountHardhat } from '../helpers/misc-utils'; import { ONE_ADDRESS, ZERO_ADDRESS } from '../helpers/constants'; import { GhoVariableDebtToken__factory } from '../types'; import { ProtocolErrors } from '@aave/core-v3'; import { INITIALIZED, CALLER_NOT_DISCOUNT_TOKEN, CALLER_NOT_A_TOKEN, ZERO_ADDRESS_NOT_VALID, } from './helpers/constants'; makeSuite('Gho VariableDebtToken End-To-End', (testEnv: TestEnv) => { let ethers; let poolSigner; const testAddressOne = '0x2acAb3DEa77832C09420663b0E1cB386031bA17B'; const testAddressTwo = '0x6fC355D4e0EE44b292E50878F49798ff755A5bbC'; before(async () => { ethers = hre.ethers; const { pool } = testEnv; poolSigner = await impersonateAccountHardhat(pool.address); }); it('Initialize when already initialized (revert expected)', async function () { const { variableDebtToken } = testEnv; await expect( variableDebtToken.initialize(ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, 0, 'test', 'test', []) ).to.be.revertedWith(INITIALIZED); }); it('Initialize with incorrect pool (revert expected)', async function () { const { deployer, pool } = testEnv; const variableDebtToken = await new GhoVariableDebtToken__factory(deployer.signer).deploy( pool.address ); await expect( variableDebtToken.initialize(ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, 0, 'test', 'test', []) ).to.be.revertedWith(ProtocolErrors.POOL_ADDRESSES_DO_NOT_MATCH); }); it('Update discount distribution - not permissioned (revert expected)', async function () { const { variableDebtToken } = testEnv; const randomSigner = await impersonateAccountHardhat(ONE_ADDRESS); const randomAddress = ONE_ADDRESS; const randomNumber = '0'; await expect( variableDebtToken .connect(randomSigner) .updateDiscountDistribution( randomAddress, randomAddress, randomNumber, randomNumber, randomNumber ) ).to.be.revertedWith(CALLER_NOT_DISCOUNT_TOKEN); }); it('Decrease Balance from Interest - not permissioned (revert expected)', async function () { const { variableDebtToken } = testEnv; const randomSigner = await impersonateAccountHardhat(ONE_ADDRESS); const randomAddress = ONE_ADDRESS; const randomNumber = '0'; await expect( variableDebtToken .connect(randomSigner) .decreaseBalanceFromInterest(randomAddress, randomNumber) ).to.be.revertedWith(CALLER_NOT_A_TOKEN); }); it('Check operations not permitted (revert expected)', async () => { const { variableDebtToken } = testEnv; const randomAddress = ONE_ADDRESS; const randomNumber = '0'; const calls = [ { fn: 'transfer', args: [randomAddress, randomNumber] }, { fn: 'allowance', args: [randomAddress, randomAddress] }, { fn: 'approve', args: [randomAddress, randomNumber] }, { fn: 'transferFrom', args: [randomAddress, randomAddress, randomNumber] }, { fn: 'increaseAllowance', args: [randomAddress, randomNumber] }, { fn: 'decreaseAllowance', args: [randomAddress, randomNumber] }, ]; for (const call of calls) { await expect(variableDebtToken.connect(poolSigner)[call.fn](...call.args)).to.be.revertedWith( ProtocolErrors.OPERATION_NOT_SUPPORTED ); } }); it('Check permission of onlyPool modified functions (revert expected)', async () => { const { variableDebtToken, users } = testEnv; const nonPoolAdmin = users[2]; const randomAddress = ONE_ADDRESS; const randomNumber = '0'; const calls = [ { fn: 'mint', args: [randomAddress, randomAddress, randomNumber, randomNumber] }, { fn: 'burn', args: [randomAddress, randomNumber, randomNumber] }, ]; for (const call of calls) { await expect( variableDebtToken.connect(nonPoolAdmin.signer)[call.fn](...call.args) ).to.be.revertedWith(ProtocolErrors.CALLER_MUST_BE_POOL); } }); it('Check permission of onlyPoolAdmin modified functions (revert expected)', async () => { const { variableDebtToken, users } = testEnv; const nonPoolAdmin = users[2]; const randomAddress = ONE_ADDRESS; const randomNumber = '0'; const calls = [ { fn: 'setAToken', args: [randomAddress] }, { fn: 'updateDiscountRateStrategy', args: [randomAddress] }, { fn: 'updateDiscountToken', args: [randomAddress] }, ]; for (const call of calls) { await expect( variableDebtToken.connect(nonPoolAdmin.signer)[call.fn](...call.args) ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_POOL_ADMIN); } }); it('Get AToken', async function () { const { variableDebtToken, aToken } = testEnv; const aTokenAddress = await variableDebtToken.getAToken(); expect(aTokenAddress).to.be.equal(aToken.address); }); it('Get Discount Rate Strategy', async function () { const { variableDebtToken, discountRateStrategy } = testEnv; const discountToken = await variableDebtToken.getDiscountRateStrategy(); expect(discountToken).to.be.equal(discountRateStrategy.address); }); it('Set ZERO address as AToken (revert expected)', async function () { const { users: [user1], pool, poolAdmin, } = testEnv; const newGhoAToken = await new GhoVariableDebtToken__factory(user1.signer).deploy(pool.address); await expect(newGhoAToken.connect(poolAdmin.signer).setAToken(ZERO_ADDRESS)).to.be.revertedWith( ZERO_ADDRESS_NOT_VALID ); }); it('Set AToken - already set (revert expected)', async function () { const { variableDebtToken, poolAdmin } = testEnv; await expect( variableDebtToken.connect(poolAdmin.signer).setAToken(ONE_ADDRESS) ).to.be.revertedWith('ATOKEN_ALREADY_SET'); }); it('Set Discount Strategy', async function () { const { variableDebtToken, deployer, discountRateStrategy } = testEnv; await expect(variableDebtToken.connect(deployer.signer).updateDiscountRateStrategy(ONE_ADDRESS)) .to.emit(variableDebtToken, 'DiscountRateStrategyUpdated') .withArgs(discountRateStrategy.address, ONE_ADDRESS); }); it('Get Discount Strategy - after setting', async function () { const { variableDebtToken } = testEnv; expect(await variableDebtToken.getDiscountRateStrategy()).to.be.equal(ONE_ADDRESS); }); it('Set ZERO address as Discount Strategy (revert expected)', async function () { const { variableDebtToken, deployer } = testEnv; await expect( variableDebtToken.connect(deployer.signer).updateDiscountRateStrategy(ZERO_ADDRESS) ).to.be.revertedWith(ZERO_ADDRESS_NOT_VALID); }); it('Set Discount Strategy - not permissioned (revert expected)', async function () { const { variableDebtToken } = testEnv; const randomSigner = await impersonateAccountHardhat(testAddressTwo); await expect( variableDebtToken.connect(randomSigner).updateDiscountRateStrategy(ONE_ADDRESS) ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_POOL_ADMIN); }); it('Set ZERO address as Discount Token (revert expected)', async function () { const { variableDebtToken, deployer } = testEnv; await expect( variableDebtToken.connect(deployer.signer).updateDiscountToken(ZERO_ADDRESS) ).to.be.revertedWith(ZERO_ADDRESS_NOT_VALID); }); it('Get Discount Token - before setting', async function () { const { variableDebtToken, stakedAave } = testEnv; expect(await variableDebtToken.getDiscountToken()).to.be.equal(stakedAave.address); }); it('Set Discount Token', async function () { const { variableDebtToken, stakedAave, deployer } = testEnv; await expect(variableDebtToken.connect(deployer.signer).updateDiscountToken(testAddressOne)) .to.emit(variableDebtToken, 'DiscountTokenUpdated') .withArgs(stakedAave.address, testAddressOne); }); it('Get Discount Token - after setting', async function () { const { variableDebtToken } = testEnv; expect(await variableDebtToken.getDiscountToken()).to.be.equal(testAddressOne); }); it('Set Discount Token - not permissioned (revert expected)', async function () { const { variableDebtToken } = testEnv; const randomSigner = await impersonateAccountHardhat(testAddressTwo); await expect( variableDebtToken.connect(randomSigner).updateDiscountToken(ONE_ADDRESS) ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_POOL_ADMIN); }); }); ================================================ FILE: test/helpers/constants.ts ================================================ export const INITIALIZED = 'Contract instance has already been initialized'; export const ZERO_ADDRESS_NOT_VALID = 'ZERO_ADDRESS_NOT_VALID'; export const CALLER_NOT_DISCOUNT_TOKEN = 'CALLER_NOT_DISCOUNT_TOKEN'; export const CALLER_NOT_A_TOKEN = 'CALLER_NOT_A_TOKEN'; ================================================ FILE: test/helpers/helpers.ts ================================================ import { BigNumber, ContractReceipt } from 'ethers'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { signTypedData_v4 } from 'eth-sig-util'; import { fromRpcSig, ECDSASignature } from 'ethereumjs-util'; import { tEthereumAddress, tStringTokenSmallUnits } from '../../helpers/types'; declare var hre: HardhatRuntimeEnvironment; export const getTxCostAndTimestamp = async (tx: ContractReceipt) => { if (!tx.blockNumber || !tx.transactionHash || !tx.cumulativeGasUsed) { throw new Error('No tx blocknumber'); } const txTimestamp = BigNumber.from( (await hre.ethers.provider.getBlock(tx.blockNumber)).timestamp ); const txInfo = await hre.ethers.provider.getTransaction(tx.transactionHash); const gasPrice = txInfo.gasPrice ? txInfo.gasPrice : tx.effectiveGasPrice; const txCost = BigNumber.from(tx.cumulativeGasUsed).mul(gasPrice); return { txCost, txTimestamp }; }; export const buildPermitParams = ( chainId: number, token: tEthereumAddress, revision: string, tokenName: string, owner: tEthereumAddress, spender: tEthereumAddress, nonce: number, deadline: string, value: tStringTokenSmallUnits ) => ({ types: { EIP712Domain: [ { name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' }, ], Permit: [ { name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'deadline', type: 'uint256' }, ], }, primaryType: 'Permit' as const, domain: { name: tokenName, version: revision, chainId: chainId, verifyingContract: token, }, message: { owner, spender, value, nonce, deadline, }, }); export const getSignatureFromTypedData = ( privateKey: string, typedData: any // TODO: should be TypedData, from eth-sig-utils, but TS doesn't accept it ): ECDSASignature => { const signature = signTypedData_v4(Buffer.from(privateKey.substring(2, 66), 'hex'), { data: typedData, }); return fromRpcSig(signature); }; ================================================ FILE: test/helpers/make-suite.ts ================================================ import { Signer } from 'ethers'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { tEthereumAddress } from '../../helpers/types'; import { evmSnapshot, evmRevert } from '../../helpers/misc-utils'; import { mintErc20 } from './user-setup'; import { AaveOracle, AaveProtocolDataProvider, GhoAToken, GhoDiscountRateStrategy, GhoInterestRateStrategy, GhoToken, GhoOracle, GhoVariableDebtToken, GhoStableDebtToken, Pool, IERC20, StakedAaveV3, MintableERC20, GhoFlashMinter, } from '../../types'; import { getGhoDiscountRateStrategy, getGhoInterestRateStrategy, getGhoOracle, getGhoToken, getGhoAToken, getGhoVariableDebtToken, getStakedAave, getMintableErc20, getGhoFlashMinter, getGhoStableDebtToken, } from '../../helpers/contract-getters'; import { getPool, getAaveProtocolDataProvider, getAaveOracle, getACLManager, ACLManager, Faucet, getFaucet, getMintableERC20, getTestnetReserveAddressFromSymbol, STAKE_AAVE_PROXY, TREASURY_PROXY_ID, } from '@aave/deploy-v3'; declare var hre: HardhatRuntimeEnvironment; export interface SignerWithAddress { signer: Signer; address: tEthereumAddress; } export interface TestEnv { deployer: SignerWithAddress; poolAdmin: SignerWithAddress; emergencyAdmin: SignerWithAddress; riskAdmin: SignerWithAddress; stkAaveWhale: SignerWithAddress; aclAdmin: SignerWithAddress; users: SignerWithAddress[]; gho: GhoToken; ghoOwner: SignerWithAddress; ghoOracle: GhoOracle; aToken: GhoAToken; stableDebtToken: GhoStableDebtToken; variableDebtToken: GhoVariableDebtToken; aTokenImplementation: GhoAToken; stableDebtTokenImplementation: GhoStableDebtToken; variableDebtTokenImplementation: GhoVariableDebtToken; interestRateStrategy: GhoInterestRateStrategy; discountRateStrategy: GhoDiscountRateStrategy; pool: Pool; aclManager: ACLManager; stakedAave: StakedAaveV3; aaveDataProvider: AaveProtocolDataProvider; aaveOracle: AaveOracle; treasuryAddress: tEthereumAddress; weth: MintableERC20; usdc: MintableERC20; aaveToken: IERC20; flashMinter: GhoFlashMinter; faucetOwner: Faucet; } let HardhatSnapshotId: string = '0x1'; const setHardhatSnapshotId = (id: string) => { HardhatSnapshotId = id; }; const testEnv: TestEnv = { deployer: {} as SignerWithAddress, poolAdmin: {} as SignerWithAddress, emergencyAdmin: {} as SignerWithAddress, riskAdmin: {} as SignerWithAddress, stkAaveWhale: {} as SignerWithAddress, aclAdmin: {} as SignerWithAddress, users: [] as SignerWithAddress[], gho: {} as GhoToken, ghoOwner: {} as SignerWithAddress, ghoOracle: {} as GhoOracle, aToken: {} as GhoAToken, stableDebtToken: {} as GhoStableDebtToken, variableDebtToken: {} as GhoVariableDebtToken, aTokenImplementation: {} as GhoAToken, stableDebtTokenImplementation: {} as GhoStableDebtToken, variableDebtTokenImplementation: {} as GhoVariableDebtToken, interestRateStrategy: {} as GhoInterestRateStrategy, discountRateStrategy: {} as GhoDiscountRateStrategy, pool: {} as Pool, aclManager: {} as ACLManager, stakedAave: {} as StakedAaveV3, aaveDataProvider: {} as AaveProtocolDataProvider, aaveOracle: {} as AaveOracle, treasuryAddress: {} as tEthereumAddress, weth: {} as MintableERC20, usdc: {} as MintableERC20, aaveToken: {} as IERC20, flashMinter: {} as GhoFlashMinter, faucetOwner: {} as Faucet, } as TestEnv; export async function initializeMakeSuite() { const [_deployer, ...restSigners] = await hre.ethers.getSigners(); console.log('Network:', hre.network.name); const deployer: SignerWithAddress = { address: await _deployer.getAddress(), signer: _deployer, }; for (const signer of restSigners) { testEnv.users.push({ signer, address: await signer.getAddress(), }); } testEnv.deployer = deployer; testEnv.poolAdmin = deployer; testEnv.aclAdmin = deployer; testEnv.ghoOwner = deployer; // get contracts from gho deployment testEnv.gho = await getGhoToken(); testEnv.ghoOracle = await getGhoOracle(); testEnv.pool = await getPool(); testEnv.aaveDataProvider = await getAaveProtocolDataProvider(); testEnv.aclManager = await getACLManager(); const tokenProxyAddresses = await testEnv.aaveDataProvider.getReserveTokensAddresses( testEnv.gho.address ); testEnv.aToken = await getGhoAToken(tokenProxyAddresses.aTokenAddress); testEnv.stableDebtToken = await getGhoStableDebtToken(tokenProxyAddresses.stableDebtTokenAddress); testEnv.variableDebtToken = await getGhoVariableDebtToken( tokenProxyAddresses.variableDebtTokenAddress ); testEnv.aTokenImplementation = await getGhoAToken(); testEnv.stableDebtTokenImplementation = await getGhoStableDebtToken(); testEnv.variableDebtTokenImplementation = await getGhoVariableDebtToken(); testEnv.interestRateStrategy = await getGhoInterestRateStrategy(); testEnv.discountRateStrategy = await getGhoDiscountRateStrategy(); testEnv.aaveOracle = await getAaveOracle(); testEnv.treasuryAddress = (await hre.deployments.get(TREASURY_PROXY_ID)).address; testEnv.faucetOwner = await getFaucet(); testEnv.weth = await getMintableERC20(await getTestnetReserveAddressFromSymbol('WETH')); testEnv.usdc = await getMintableERC20(await getTestnetReserveAddressFromSymbol('USDC')); testEnv.aaveToken = await getMintableErc20(await getTestnetReserveAddressFromSymbol('AAVE')); const userAddresses = testEnv.users.map((u) => u.address); await mintErc20( testEnv.faucetOwner, testEnv.weth.address, userAddresses, hre.ethers.utils.parseUnits('1000.0', 18) ); await mintErc20( testEnv.faucetOwner, testEnv.usdc.address, userAddresses, hre.ethers.utils.parseUnits('100000.0', 18) ); await mintErc20( testEnv.faucetOwner, testEnv.aaveToken.address, userAddresses, hre.ethers.utils.parseUnits('10.0', 18) ); testEnv.stakedAave = await getStakedAave( await ( await hre.deployments.get(STAKE_AAVE_PROXY) ).address ); testEnv.flashMinter = await getGhoFlashMinter(); } const setSnapshot = async () => { setHardhatSnapshotId(await evmSnapshot()); }; const revertHead = async () => { await evmRevert(HardhatSnapshotId); }; export function makeSuite(name: string, tests: (testEnv: TestEnv) => void) { describe(name, () => { before(async () => { await setSnapshot(); }); tests(testEnv); after(async () => { await revertHead(); }); }); } ================================================ FILE: test/helpers/math/calculations.ts ================================================ import { BigNumber } from 'ethers'; import './wadraymath'; import { ONE_YEAR, RAY } from '../../../helpers/constants'; export const calcCompoundedInterest = ( rate: BigNumber, currentTimestamp: BigNumber, lastUpdateTimestamp: BigNumber ) => { const timeDifference = currentTimestamp.sub(lastUpdateTimestamp); const SECONDS_PER_YEAR = BigNumber.from(ONE_YEAR); if (timeDifference.eq(0)) { return BigNumber.from(RAY); } const expMinusOne = timeDifference.sub(1); const expMinusTwo = timeDifference.gt(2) ? timeDifference.sub(2) : 0; const basePowerTwo = rate.rayMul(rate).div(SECONDS_PER_YEAR.mul(SECONDS_PER_YEAR)); const basePowerThree = basePowerTwo.rayMul(rate).div(SECONDS_PER_YEAR); const secondTerm = timeDifference.mul(expMinusOne).mul(basePowerTwo).div(2); const thirdTerm = timeDifference.mul(expMinusOne).mul(expMinusTwo).mul(basePowerThree).div(6); return BigNumber.from(RAY) .add(rate.mul(timeDifference).div(SECONDS_PER_YEAR)) .add(secondTerm) .add(thirdTerm); }; export const calcCompoundedInterestV2 = ( rate: BigNumber, currentTimestamp: BigNumber, lastUpdateTimestamp: BigNumber ) => { const timeDifference = currentTimestamp.sub(lastUpdateTimestamp); const SECONDS_PER_YEAR = BigNumber.from(ONE_YEAR); if (timeDifference.eq(0)) { return BigNumber.from(RAY); } const expMinusOne = timeDifference.sub(1); const expMinusTwo = timeDifference.gt(2) ? timeDifference.sub(2) : 0; const ratePerSecond = rate.div(SECONDS_PER_YEAR); const basePowerTwo = ratePerSecond.rayMul(ratePerSecond); const basePowerThree = basePowerTwo.rayMul(ratePerSecond); const secondTerm = timeDifference.mul(expMinusOne).mul(basePowerTwo).div(2); const thirdTerm = timeDifference.mul(expMinusOne).mul(expMinusTwo).mul(basePowerThree).div(6); return BigNumber.from(RAY).add(ratePerSecond.mul(timeDifference)).add(secondTerm).add(thirdTerm); }; export const calcDiscountRate = ( discountRate: BigNumber, ghoDiscountedPerDiscountToken: BigNumber, minDiscountTokenBalance: BigNumber, debtBalance: BigNumber, discountTokenBalance: BigNumber ) => { if (discountTokenBalance.lt(minDiscountTokenBalance) || debtBalance.eq(0)) { return 0; } else { const discountedAmount = discountTokenBalance.wadMul(ghoDiscountedPerDiscountToken); if (discountedAmount.gte(debtBalance)) { return discountRate; } else { return discountedAmount.mul(discountRate).div(debtBalance); } } }; ================================================ FILE: test/helpers/math/wadraymath.ts ================================================ import { BigNumber, BigNumberish } from 'ethers'; import { RAY, WAD, HALF_RAY, HALF_WAD, WAD_RAY_RATIO, HALF_PERCENTAGE, PERCENTAGE_FACTOR, } from '../../../helpers/constants'; declare module '@ethersproject/bignumber' { interface BigNumber { ray: () => BigNumber; wad: () => BigNumber; halfRay: () => BigNumber; halfWad: () => BigNumber; halfPercentage: () => BigNumber; percentageFactor: () => BigNumber; wadMul: (a: BigNumber) => BigNumber; wadDiv: (a: BigNumber) => BigNumber; rayMul: (a: BigNumber) => BigNumber; rayDiv: (a: BigNumber) => BigNumber; percentMul: (a: BigNumberish) => BigNumber; percentDiv: (a: BigNumberish) => BigNumber; rayToWad: () => BigNumber; wadToRay: () => BigNumber; negated: () => BigNumber; } } BigNumber.prototype.ray = (): BigNumber => BigNumber.from(RAY); BigNumber.prototype.wad = (): BigNumber => BigNumber.from(WAD); BigNumber.prototype.halfRay = (): BigNumber => BigNumber.from(HALF_RAY); BigNumber.prototype.halfWad = (): BigNumber => BigNumber.from(HALF_WAD); BigNumber.prototype.halfPercentage = (): BigNumber => BigNumber.from(HALF_PERCENTAGE); BigNumber.prototype.percentageFactor = (): BigNumber => BigNumber.from(PERCENTAGE_FACTOR); BigNumber.prototype.wadMul = function (other: BigNumber): BigNumber { return this.halfWad().add(this.mul(other)).div(this.wad()); }; BigNumber.prototype.wadDiv = function (other: BigNumber): BigNumber { const halfOther = other.div(2); return halfOther.add(this.mul(this.wad())).div(other); }; BigNumber.prototype.rayMul = function (other: BigNumber): BigNumber { return this.halfRay().add(this.mul(other)).div(this.ray()); }; BigNumber.prototype.rayDiv = function (other: BigNumber): BigNumber { const halfOther = other.div(2); return halfOther.add(this.mul(this.ray())).div(other); }; BigNumber.prototype.percentMul = function (bps: BigNumberish): BigNumber { return this.halfPercentage().add(this.mul(bps)).div(PERCENTAGE_FACTOR); }; BigNumber.prototype.percentDiv = function (bps: BigNumberish): BigNumber { const halfBps = BigNumber.from(bps).div(2); return halfBps.add(this.mul(PERCENTAGE_FACTOR)).div(bps); }; BigNumber.prototype.rayToWad = function (): BigNumber { const halfRatio = BigNumber.from(WAD_RAY_RATIO).div(2); return halfRatio.add(this).div(WAD_RAY_RATIO); }; BigNumber.prototype.wadToRay = function (): BigNumber { return this.mul(WAD_RAY_RATIO); }; BigNumber.prototype.negated = function (): BigNumber { return this.mul(-1); }; ================================================ FILE: test/helpers/tokenization-events.ts ================================================ import { ethers } from 'hardhat'; import { utils } from 'ethers'; import { GhoVariableDebtToken } from '../../types'; const GHO_VARIABLE_DEBT_TOKEN_EVENTS = [ { sig: 'Transfer(address,address,uint256)', args: ['from', 'to', 'value'] }, { sig: 'Mint(address,address,uint256,uint256,uint256)', args: ['caller', 'onBehalfOf', 'value', 'balanceIncrease', 'index'], }, { sig: 'Burn(address,address,uint256,uint256,uint256)', args: ['from', 'target', 'value', 'balanceIncrease', 'index'], }, { sig: 'DiscountPercentUpdated(address,uint256,uint256)', args: ['user', 'oldDiscountPercent', 'newDiscountPercent'], }, ]; export const printVariableDebtTokenEvents = ( variableDebtToken: GhoVariableDebtToken, receipt: ethers.providers.TransactionReceipt ) => { for (const eventSig of GHO_VARIABLE_DEBT_TOKEN_EVENTS) { const eventName = eventSig.sig.split('(')[0]; const encodedSig = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(eventSig.sig)); const rawEvents = receipt.logs.filter( (log) => log.topics[0] === encodedSig && log.address == variableDebtToken.address ); for (const rawEvent of rawEvents) { const rawParsed = variableDebtToken.interface.decodeEventLog( eventName, rawEvent.data, rawEvent.topics ); const parsed: any[] = []; let i = 0; for (const arg of eventSig.args) { parsed[i] = ['value', 'balanceIncrease', 'amountDiscounted'].includes(arg) ? ethers.utils.formatEther(rawParsed[arg]) : rawParsed[arg]; i++; } console.log(`event ${eventName} ${parsed[0]} -> ${parsed[1]}: ${parsed.slice(2).join(' ')}`); } } }; export const getVariableDebtTokenEvent = ( variableDebtToken: GhoVariableDebtToken, receipt: TransactionReceipt, eventName: string ) => { const eventSig = GHO_VARIABLE_DEBT_TOKEN_EVENTS.find( (item) => item.sig.split('(')[0] === eventName ); const results: utils.Result = []; if (eventSig) { const encodedSig = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(eventSig.sig)); const rawEvents = receipt.logs.filter( (log) => log.topics[0] === encodedSig && log.address == variableDebtToken.address ); for (const rawEvent of rawEvents) { results.push( variableDebtToken.interface.decodeEventLog(eventName, rawEvent.data, rawEvent.topics) ); } } return results; }; ================================================ FILE: test/helpers/user-setup.ts ================================================ import { impersonateAccountHardhat } from '../../helpers/misc-utils'; import { tEthereumAddress } from '../../helpers/types'; import { BigNumber } from 'ethers'; import { IERC20 } from '../../types'; import { ContractTransaction } from 'ethers'; import { Faucet } from '@aave/deploy-v3'; export const distributeErc20 = async ( erc20: IERC20, whale: tEthereumAddress, recipients: tEthereumAddress[], amount: BigNumber ) => { const promises: Promise[] = []; const whaleSigner = await impersonateAccountHardhat(whale); erc20 = erc20.connect(whaleSigner); recipients.forEach((recipient) => { promises.push(erc20.transfer(recipient, amount)); }); await Promise.all(promises); }; export const mintErc20 = async ( faucetOwner: Faucet, mintableErc20: tEthereumAddress, recipients: tEthereumAddress[], amount: BigNumber ) => { const promises: Promise[] = []; recipients.forEach(async (recipient) => { promises.push(faucetOwner.mint(mintableErc20, recipient, amount)); }); await Promise.all(promises); }; ================================================ FILE: test/initial-entitiy-configuration.test.ts ================================================ import hre from 'hardhat'; import { expect } from 'chai'; import { makeSuite, TestEnv } from './helpers/make-suite'; import { ghoEntityConfig } from '../helpers/config'; makeSuite('Initial GHO Aave Entity Configuration', (testEnv: TestEnv) => { let ethers; before(async () => { ethers = hre.ethers; }); it('Aave entity data check', async function () { const { gho, aToken } = testEnv; const aaveFacilitator = await gho.getFacilitator(aToken.address); const { label, bucketCapacity, bucketLevel } = aaveFacilitator; expect(label).to.be.equal(ghoEntityConfig.label); expect(bucketCapacity).to.be.equal(ghoEntityConfig.mintLimit); expect(bucketLevel).to.be.equal(0); }); }); ================================================ FILE: test/initial-reserve-configuration.test.ts ================================================ import hre from 'hardhat'; import { expect } from 'chai'; import { makeSuite, TestEnv } from './helpers/make-suite'; makeSuite('Initial GHO Reserve Configuration', (testEnv: TestEnv) => { let ethers; before(async () => { ethers = hre.ethers; }); it('GHO listed as a reserve', async function () { const { pool, gho } = testEnv; const reserves = await pool.getReservesList(); expect(reserves.includes(gho.address)); }); it('AToken proxy contract listed in Aave with correct implementation', async function () { const { aaveDataProvider, gho, aTokenImplementation } = testEnv; const reserveData = await aaveDataProvider.getReserveTokensAddresses(gho.address); const implementationAddressAsBytes = await ethers.provider.getStorageAt( reserveData.aTokenAddress, '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' ); const implementationAddress = ethers.utils.getAddress( ethers.utils.hexDataSlice(implementationAddressAsBytes, 12) ); expect(implementationAddress).to.be.equal(aTokenImplementation.address); }); it('StableDebtToken proxy contract listed in Aave with correct implementation', async function () { const { aaveDataProvider, gho, stableDebtTokenImplementation } = testEnv; const reserveData = await aaveDataProvider.getReserveTokensAddresses(gho.address); const implementationAddressAsBytes = await ethers.provider.getStorageAt( reserveData.stableDebtTokenAddress, '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' ); const implementationAddress = ethers.utils.getAddress( ethers.utils.hexDataSlice(implementationAddressAsBytes, 12) ); expect(implementationAddress).to.be.equal(stableDebtTokenImplementation.address); }); it('VariableDebtToken proxy contract listed in Aave with correct implementation', async function () { const { aaveDataProvider, gho, variableDebtTokenImplementation } = testEnv; const reserveData = await aaveDataProvider.getReserveTokensAddresses(gho.address); const implementationAddressAsBytes = await ethers.provider.getStorageAt( reserveData.variableDebtTokenAddress, '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' ); const implementationAddress = ethers.utils.getAddress( ethers.utils.hexDataSlice(implementationAddressAsBytes, 12) ); expect(implementationAddress).to.be.equal(variableDebtTokenImplementation.address); }); it('AToken configuration Check', async function () { const { aToken, gho, pool, treasuryAddress } = testEnv; const poolAddress = await aToken.POOL(); const underlyingAddress = await aToken.UNDERLYING_ASSET_ADDRESS(); const aTokenTreasuryAddress = await aToken.RESERVE_TREASURY_ADDRESS(); expect(poolAddress).to.be.equal(pool.address); expect(underlyingAddress).to.be.equal(gho.address); expect(aTokenTreasuryAddress).to.be.equal(treasuryAddress); }); it('StableDebtToken configuration check', async function () { const { stableDebtToken, gho, pool } = testEnv; const poolAddress = await stableDebtToken.POOL(); const underlyingAddress = await stableDebtToken.UNDERLYING_ASSET_ADDRESS(); expect(poolAddress).to.be.equal(pool.address); expect(underlyingAddress).to.be.equal(gho.address); }); it('VariableDebtToken configuration check', async function () { const { variableDebtToken, gho, pool } = testEnv; const poolAddress = await variableDebtToken.POOL(); const underlyingAddress = await variableDebtToken.UNDERLYING_ASSET_ADDRESS(); expect(poolAddress).to.be.equal(pool.address); expect(underlyingAddress).to.be.equal(gho.address); }); // it('Interest Rate Strategy should be configured correctly', async function () { // const { interestRateStrategy } = testEnv; // const rates = await interestRateStrategy.calculateInterestRates(ZERO_ADDRESS, 0, 0, 0, 0, 0); // expect(rates[0]).to.be.equal(ethers.utils.parseUnits('1.0', 25)); // expect(rates[1]).to.be.equal(ethers.utils.parseUnits('1.0', 25)); // expect(rates[2]).to.be.equal(ghoReserveConfig.INTEREST_RATE); // }); it('Reserve configuration data check', async function () { const { aaveDataProvider, gho } = testEnv; const reserverConfiguration = await aaveDataProvider.getReserveConfigurationData(gho.address); expect(reserverConfiguration.decimals).to.be.equal(18); expect(reserverConfiguration.ltv).to.be.equal(0); expect(reserverConfiguration.liquidationThreshold).to.be.equal(0); expect(reserverConfiguration.liquidationBonus).to.be.equal(0); expect(reserverConfiguration.reserveFactor).to.be.equal(0); expect(reserverConfiguration.usageAsCollateralEnabled).to.be.false; expect(reserverConfiguration.borrowingEnabled).to.be.true; expect(reserverConfiguration.stableBorrowRateEnabled).to.be.false; expect(reserverConfiguration.isActive).to.be.true; expect(reserverConfiguration.isFrozen).to.be.false; }); it('Aave oracle - gho source address check', async function () { const { aaveOracle, gho, ghoOracle } = testEnv; const ghoSource = await aaveOracle.getSourceOfAsset(gho.address); expect(ghoSource).to.be.equal(ghoOracle.address); }); }); ================================================ FILE: test/stkAave-upgrade.test.ts ================================================ import hre from 'hardhat'; import { expect } from 'chai'; import { advanceTimeAndBlock } from '../helpers/misc-utils'; import { makeSuite, TestEnv } from './helpers/make-suite'; makeSuite('Check upgraded stkAave', (testEnv: TestEnv) => { let ethers; let amountTransferred; before(async () => { ethers = hre.ethers; amountTransferred = ethers.utils.parseUnits('1.0', 18); }); it('Revision number check', async function () { const { stakedAave } = testEnv; const revision = await stakedAave.REVISION(); const expectedRevision = 5; expect(revision).to.be.equal(expectedRevision); }); it('GhoDebtToken Address check', async function () { const { stakedAave, variableDebtToken } = testEnv; let ghoDebtToken = await stakedAave.ghoDebtToken(); expect(ghoDebtToken).to.be.equal(variableDebtToken.address); }); it('Users should be able to stake AAVE', async () => { const { stakedAave, aaveToken, users } = testEnv; const amount = ethers.utils.parseUnits('1.0', 18); const approveAmount = ethers.utils.parseUnits('1.0', 18); await aaveToken.connect(users[2].signer).approve(stakedAave.address, approveAmount); await expect(stakedAave.connect(users[2].signer).stake(users[2].address, amount)) .to.emit(stakedAave, 'Staked') .withArgs(users[2].address, users[2].address, amount, amount); }); it('Users should be able to redeem stkAave', async () => { const { stakedAave, users } = testEnv; const amount = ethers.utils.parseUnits('1.0', 18); await advanceTimeAndBlock(48600); await stakedAave.connect(users[2].signer).cooldown(); const COOLDOWN_SECONDS = await stakedAave.COOLDOWN_SECONDS(); await advanceTimeAndBlock(Number(COOLDOWN_SECONDS.toString())); await expect(stakedAave.connect(users[2].signer).redeem(users[2].address, amount)) .to.emit(stakedAave, 'Redeem') .withArgs(users[2].address, users[2].address, amount, amount); }); }); ================================================ FILE: test/transfer-stkAave.test.ts ================================================ import hre from 'hardhat'; import { expect } from 'chai'; import { BigNumber } from 'ethers'; import { makeSuite, TestEnv } from './helpers/make-suite'; import { evmRevert, evmSnapshot, setBlocktime } from '../helpers/misc-utils'; import { ONE_YEAR, PERCENTAGE_FACTOR, ZERO_ADDRESS } from '../helpers/constants'; import { ghoReserveConfig } from '../helpers/config'; import { calcCompoundedInterest, calcDiscountRate } from './helpers/math/calculations'; import { getTxCostAndTimestamp } from './helpers/helpers'; import './helpers/math/wadraymath'; makeSuite('Gho StkAave Transfer', (testEnv: TestEnv) => { let ethers; let collateralAmount; let borrowAmount; let oneYearLater; let rcpt, tx; let discountRate, ghoDiscountedPerDiscountToken, minDiscountTokenBalance; before(async () => { ethers = hre.ethers; collateralAmount = ethers.utils.parseUnits('1000.0', 18); borrowAmount = ethers.utils.parseUnits('1000.0', 18); const { users, discountRateStrategy } = testEnv; // Fetch discount rate strategy parameters [discountRate, ghoDiscountedPerDiscountToken, minDiscountTokenBalance] = await Promise.all([ discountRateStrategy.DISCOUNT_RATE(), discountRateStrategy.GHO_DISCOUNTED_PER_DISCOUNT_TOKEN(), discountRateStrategy.MIN_DISCOUNT_TOKEN_BALANCE(), ]); }); it('Transfer stkAAVE to borrower of GHO', async function () { const snapId = await evmSnapshot(); // setup const { users, pool, weth, gho, variableDebtToken } = testEnv; const { aaveToken, stakedAave, stkAaveWhale } = testEnv; const stkAaveAmount = ethers.utils.parseUnits('10.0', 18); await aaveToken.connect(users[2].signer).approve(stakedAave.address, stkAaveAmount); await stakedAave.connect(users[2].signer).stake(users[2].address, stkAaveAmount); await stakedAave.connect(users[2].signer).transfer(users[1].address, stkAaveAmount); await weth.connect(users[2].signer).approve(pool.address, collateralAmount); await pool .connect(users[2].signer) .deposit(weth.address, collateralAmount, users[2].address, 0); await pool.connect(users[2].signer).borrow(gho.address, borrowAmount, 2, 0, users[2].address); const debtBalanceBefore = await variableDebtToken.balanceOf(users[2].address); await expect(stakedAave.connect(users[1].signer).transfer(users[2].address, stkAaveAmount)).to .not.be.reverted; const debtBalanceAfter = await variableDebtToken.balanceOf(users[2].address); expect(debtBalanceAfter).to.be.gte(debtBalanceBefore); await evmRevert(snapId); }); it('Transfer from user with stkAave and GHO to user without GHO', async function () { // setup const { users, pool, weth, gho, variableDebtToken } = testEnv; const { aaveToken, stakedAave, stkAaveWhale } = testEnv; const stkAaveAmount = ethers.utils.parseUnits('10.0', 18); await aaveToken.connect(users[2].signer).approve(stakedAave.address, stkAaveAmount); await stakedAave.connect(users[2].signer).stake(users[2].address, stkAaveAmount); // await stakedAave.connect(stkAaveWhale.signer).transfer(users[2].address, stkAaveAmount); await weth.connect(users[2].signer).approve(pool.address, collateralAmount); await pool .connect(users[2].signer) .deposit(weth.address, collateralAmount, users[2].address, 0); await pool.connect(users[2].signer).borrow(gho.address, borrowAmount, 2, 0, users[2].address); const { lastUpdateTimestamp, variableBorrowIndex } = await pool.getReserveData(gho.address); const user1ScaledBefore = await variableDebtToken.scaledBalanceOf(users[2].address); // Updating the timestamp for the borrow to be one year later oneYearLater = BigNumber.from(lastUpdateTimestamp).add(BigNumber.from(ONE_YEAR)); await setBlocktime(oneYearLater.toNumber()); const user1DiscountPercentBefore = await variableDebtToken.getDiscountPercent(users[2].address); expect(await variableDebtToken.getBalanceFromInterest(users[2].address)).to.be.eq(0); expect(await variableDebtToken.getBalanceFromInterest(users[1].address)).to.be.eq(0); // calculate expected results tx = await stakedAave.connect(users[2].signer).transfer(users[1].address, stkAaveAmount); rcpt = await tx.wait(); const { txTimestamp } = await getTxCostAndTimestamp(rcpt); const multiplier = calcCompoundedInterest( ghoReserveConfig.INTEREST_RATE, txTimestamp, BigNumber.from(lastUpdateTimestamp) ); const expIndex = variableBorrowIndex.rayMul(multiplier); const user1ExpectedBalanceNoDiscount = user1ScaledBefore.rayMul(expIndex); const user1BalanceIncrease = user1ExpectedBalanceNoDiscount.sub(borrowAmount); const user1ExpectedDiscount = user1BalanceIncrease .mul(user1DiscountPercentBefore) .div(PERCENTAGE_FACTOR); const user1ExpectedBalance = user1ExpectedBalanceNoDiscount.sub(user1ExpectedDiscount); const user1BalanceIncreaseWithDiscount = user1BalanceIncrease.sub(user1ExpectedDiscount); const user1DiscountTokenBalance = await stakedAave.balanceOf(users[2].address); const user1ExpectedDiscountPercent = calcDiscountRate( discountRate, ghoDiscountedPerDiscountToken, minDiscountTokenBalance, user1ExpectedBalance, user1DiscountTokenBalance ); const user1Debt = await variableDebtToken.balanceOf(users[2].address); expect(user1Debt).to.be.closeTo(user1ExpectedBalance, 1); expect(await variableDebtToken.getBalanceFromInterest(users[2].address)).to.be.closeTo( user1BalanceIncreaseWithDiscount, 1 ); expect(await variableDebtToken.getBalanceFromInterest(users[1].address)).to.be.eq(0); }); }); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "es2019", "module": "commonjs", "strict": true, "esModuleInterop": true, "outDir": "dist", "noImplicitAny": false, "resolveJsonModule": true }, "include": ["./test", "./helpers", "./tasks", "./types", "./deploy"], "files": ["./hardhat.config.ts"] }