diff --git a/.github/workflows/certora-stata.yml b/.github/workflows/certora-stata.yml index 199d29e1..fb299d0f 100644 --- a/.github/workflows/certora-stata.yml +++ b/.github/workflows/certora-stata.yml @@ -3,10 +3,10 @@ name: certora-stata on: push: branches: - - main certora + - main pull_request: branches: - - main certora + - main workflow_dispatch: @@ -51,39 +51,29 @@ jobs: max-parallel: 16 matrix: rule: - - verifyERC4626.conf --rule previewMintIndependentOfAllowance previewRedeemIndependentOfBalance previewMintAmountCheck previewDepositIndependentOfAllowanceApprove previewWithdrawIndependentOfMaxWithdraw previewWithdrawAmountCheck previewWithdrawIndependentOfBalance2 previewWithdrawIndependentOfBalance1 previewRedeemIndependentOfMaxRedeem1 previewRedeemAmountCheck previewRedeemIndependentOfMaxRedeem2 amountConversionRoundedDown withdrawCheck redeemCheck convertToAssetsCheck convertToSharesCheck toAssetsDoesNotRevert sharesConversionRoundedDown toSharesDoesNotRevert previewDepositAmountCheck maxRedeemCompliance maxWithdrawConversionCompliance - - verifyERC4626MintDepositSummarization.conf --rule depositCheckIndexGRayAssert2 depositCheckIndexERayAssert2 mintCheckIndexGRayUpperBound mintCheckIndexGRayLowerBound mintCheckIndexEqualsRay - #The following seem to be incorrect: - # verifyERC4626.conf --rule maxMintMustntRevert maxDepositMustntRevert maxRedeemMustntRevert maxWithdrawMustntRevert - # # Timeout - # - verifyERC4626DepositSummarization.conf --rule depositCheckIndexGRayAssert1 - - verifyERC4626DepositSummarization.conf --rule depositCheckIndexERayAssert1 + - verifyERC4626.conf --rule previewRedeemIndependentOfBalance previewMintAmountCheck previewDepositIndependentOfAllowanceApprove previewWithdrawAmountCheck previewWithdrawIndependentOfBalance2 previewWithdrawIndependentOfBalance1 previewRedeemIndependentOfMaxRedeem1 previewRedeemAmountCheck previewRedeemIndependentOfMaxRedeem2 amountConversionRoundedDown withdrawCheck redeemCheck redeemATokensCheck convertToAssetsCheck convertToSharesCheck toAssetsDoesNotRevert sharesConversionRoundedDown toSharesDoesNotRevert previewDepositAmountCheck maxRedeemCompliance maxWithdrawConversionCompliance + - verifyERC4626.conf --rule maxMintMustntRevert maxDepositMustntRevert maxRedeemMustntRevert maxWithdrawMustntRevert totalAssetsMustntRevert + # Timeout + # - verifyERC4626.conf --rule previewWithdrawIndependentOfMaxWithdraw + - verifyERC4626MintDepositSummarization.conf --rule depositCheckIndexGRayAssert2 depositATokensCheckIndexGRayAssert2 depositWithPermitCheckIndexGRayAssert2 depositCheckIndexERayAssert2 depositATokensCheckIndexERayAssert2 depositWithPermitCheckIndexERayAssert2 mintCheckIndexGRayUpperBound mintCheckIndexGRayLowerBound mintCheckIndexEqualsRay + - verifyERC4626DepositSummarization.conf --rule depositCheckIndexGRayAssert1 depositATokensCheckIndexGRayAssert1 depositWithPermitCheckIndexGRayAssert1 depositCheckIndexERayAssert1 depositATokensCheckIndexERayAssert1 depositWithPermitCheckIndexERayAssert1 - verifyERC4626Extended.conf --rule previewWithdrawRoundingRange previewRedeemRoundingRange amountConversionPreserved sharesConversionPreserved accountsJoiningSplittingIsLimited convertSumOfAssetsPreserved previewDepositSameAsDeposit previewMintSameAsMint - #The following seem to be incorrect: - # verifyERC4626Extended.conf --rule maxDepositConstant + - verifyERC4626Extended.conf --rule maxDepositConstant - verifyERC4626Extended.conf --rule redeemSum + - verifyERC4626Extended.conf --rule redeemATokensSum - verifyAToken.conf --rule aTokenBalanceIsFixed_for_collectAndUpdateRewards aTokenBalanceIsFixed_for_claimRewards aTokenBalanceIsFixed_for_claimRewardsOnBehalf - verifyAToken.conf --rule aTokenBalanceIsFixed_for_claimSingleRewardOnBehalf aTokenBalanceIsFixed_for_claimRewardsToSelf - - verifyStaticATokenLM.conf --rule rewardsConsistencyWhenSufficientRewardsExist - - verifyStaticATokenLM.conf --rule rewardsConsistencyWhenInsufficientRewards - - verifyStaticATokenLM.conf --rule rewardsTotalDeclinesOnlyByClaim - - verifyStaticATokenLM.conf --rule rewardsTotalDeclinesOnlyByClaim_timedout_methods - - verifyStaticATokenLM.conf --rule rewardsTotalDeclinesOnlyByClaim_redeem_methods - - verifyStaticATokenLM.conf --rule solvency_positive_total_supply_only_if_positive_asset - - verifyStaticATokenLM.conf --rule solvency_total_asset_geq_total_supply - - verifyStaticATokenLM.conf --rule solvency_total_asset_geq_total_supply_CASE_SPLIT_redeem - - verifyStaticATokenLM.conf --rule singleAssetAccruedRewards - - verifyStaticATokenLM.conf --rule totalAssets_stable - - verifyStaticATokenLM.conf --rule totalAssets_stable_after_collectAndUpdateRewards - - verifyStaticATokenLM.conf --rule reward_balance_stable_after_collectAndUpdateRewards - - verifyStaticATokenLM.conf --rule totalClaimableRewards_stable_CASE_SPLIT - # - verifyStaticATokenLM.conf --rule totalClaimableRewards_stable_CASE_SPLIT_redeem - # # Timeout - - verifyStaticATokenLM.conf --rule getClaimableRewards_stable - - verifyStaticATokenLM.conf --rule getClaimableRewards_stable_after_deposit - - verifyStaticATokenLM.conf --rule getClaimableRewards_stable_after_redeem - - verifyStaticATokenLM.conf --rule totalClaimableRewards_stable_CASE_SPLIT_deposit - - verifyStaticATokenLM.conf --rule getClaimableRewards_stable_after_refreshRewardTokens - - verifyStaticATokenLM.conf --rule getClaimableRewardsBefore_leq_claimed_claimRewardsOnBehalf + - verifyStataToken.conf --rule rewardsConsistencyWhenSufficientRewardsExist + - verifyStataToken.conf --rule rewardsConsistencyWhenInsufficientRewards + - verifyStataToken.conf --rule totalClaimableRewards_stable + - verifyStataToken.conf --rule solvency_positive_total_supply_only_if_positive_asset + - verifyStataToken.conf --rule solvency_total_asset_geq_total_supply + - verifyStataToken.conf --rule singleAssetAccruedRewards + - verifyStataToken.conf --rule totalAssets_stable + - verifyStataToken.conf --rule getClaimableRewards_stable + - verifyStataToken.conf --rule getClaimableRewards_stable_after_deposit + - verifyStataToken.conf --rule getClaimableRewards_stable_after_refreshRewardTokens + - verifyStataToken.conf --rule getClaimableRewardsBefore_leq_claimed_claimRewardsOnBehalf + - verifyStataToken.conf --rule rewardsTotalDeclinesOnlyByClaim - verifyDoubleClaim.conf --rule prevent_duplicate_reward_claiming_single_reward_sufficient - verifyDoubleClaim.conf --rule prevent_duplicate_reward_claiming_single_reward_insufficient diff --git a/Makefile b/Makefile index f46c33c3..2708df60 100644 --- a/Makefile +++ b/Makefile @@ -35,4 +35,5 @@ coverage :; forge coverage --report lcov && \ download :; cast etherscan-source --chain ${chain} -d src/etherscan/${chain}_${address} ${address} git-diff : @mkdir -p diffs + @npx prettier ${before} ${after} --write @printf '%s\n%s\n%s\n' "\`\`\`diff" "$$(git diff --no-index --diff-algorithm=patience --ignore-space-at-eol ${before} ${after})" "\`\`\`" > diffs/${out}.md diff --git a/bun.lockb b/bun.lockb index 471ed858..dc28b8a6 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/certora/stata/Makefile b/certora/stata/Makefile index f71caf35..215e7440 100644 --- a/certora/stata/Makefile +++ b/certora/stata/Makefile @@ -1,10 +1,11 @@ default: help PATCH = applyHarness.patch -CONTRACTS_DIR = ../src -LIBS_DIR = ../lib +CONTRACTS_DIR = ../../src +LIBS_DIR = ../../lib MUNGED_SRC = munged/src MUNGED_LIB = munged/lib +MUNGED_DIR = munged help: @echo "usage:" @@ -15,14 +16,14 @@ help: munged: $(wildcard $(CONTRACTS_DIR)/*.sol) $(PATCH) rm -rf $@ mkdir $@ - cp -r ../lib $@ - cp -r ../src $@ + cp -r ../../lib $@ + cp -r ../../src $@ patch -p0 -d $@ < $(PATCH) record: mkdir tmp - cp -r ../lib tmp - cp -r ../src tmp + cp -r ../../lib tmp + cp -r ../../src tmp diff -ruN tmp $(MUNGED_DIR) | sed 's+tmp/++g' | sed 's+$(MUNGED_DIR)/++g' > $(PATCH) rm -rf tmp diff --git a/certora/stata/applyHarness.patch b/certora/stata/applyHarness.patch index 6357986f..98c12412 100644 --- a/certora/stata/applyHarness.patch +++ b/certora/stata/applyHarness.patch @@ -1,7 +1,48 @@ diff -ruN .gitignore .gitignore --- .gitignore 1970-01-01 02:00:00 -+++ .gitignore 2023-04-01 01:24:40 ++++ .gitignore 2024-09-04 13:59:46 @@ -0,0 +1,2 @@ +* +!.gitignore -\ No newline at end of file \ No newline at end of file +\ No newline at end of file +diff -ruN src/core/instances/ATokenInstance.sol src/core/instances/ATokenInstance.sol +--- src/core/instances/ATokenInstance.sol 2024-09-05 19:01:54 ++++ src/core/instances/ATokenInstance.sol 2024-09-05 11:33:23 +@@ -35,15 +35,15 @@ + + _domainSeparator = _calculateDomainSeparator(); + +- emit Initialized( +- underlyingAsset, +- address(POOL), +- treasury, +- address(incentivesController), +- aTokenDecimals, +- aTokenName, +- aTokenSymbol, +- params +- ); ++ // emit Initialized( ++ // underlyingAsset, ++ // address(POOL), ++ // treasury, ++ // address(incentivesController), ++ // aTokenDecimals, ++ // aTokenName, ++ // aTokenSymbol, ++ // params ++ // ); + } + } +diff -ruN src/periphery/contracts/static-a-token/ERC20AaveLMUpgradeable.sol src/periphery/contracts/static-a-token/ERC20AaveLMUpgradeable.sol +--- src/periphery/contracts/static-a-token/ERC20AaveLMUpgradeable.sol 2024-09-05 19:01:54 ++++ src/periphery/contracts/static-a-token/ERC20AaveLMUpgradeable.sol 2024-09-05 13:48:31 +@@ -147,7 +147,7 @@ + } + + ///@inheritdoc IERC20AaveLM +- function rewardTokens() external view returns (address[] memory) { ++ function rewardTokens() public view returns (address[] memory) { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + return $._rewardTokens; + } diff --git a/certora/stata/conf/verifyAToken.conf b/certora/stata/conf/verifyAToken.conf index 531f2ac9..c2bc01e1 100644 --- a/certora/stata/conf/verifyAToken.conf +++ b/certora/stata/conf/verifyAToken.conf @@ -1,12 +1,12 @@ { "files": [ - "certora/stata/harness/StaticATokenLMHarness.sol", + "certora/stata/harness/StataTokenV2Harness.sol", "certora/stata/harness/pool/SymbolicLendingPool.sol", "certora/stata/harness/rewards/RewardsControllerHarness.sol", "certora/stata/harness/rewards/TransferStrategyHarness.sol", "certora/stata/harness/tokens/DummyERC20_aTokenUnderlying.sol", "certora/stata/harness/tokens/DummyERC20_rewardToken.sol", - "src/core/instances/ATokenInstance.sol", + "certora/stata/munged/src/core/instances/ATokenInstance.sol", ], "link": [ "SymbolicLendingPool:aToken=ATokenInstance", @@ -16,24 +16,25 @@ "ATokenInstance:POOL=SymbolicLendingPool", "ATokenInstance:_incentivesController=RewardsControllerHarness", "ATokenInstance:_underlyingAsset=DummyERC20_aTokenUnderlying", - "StaticATokenLMHarness:INCENTIVES_CONTROLLER=RewardsControllerHarness", - "StaticATokenLMHarness:POOL=SymbolicLendingPool", - "StaticATokenLMHarness:_aToken=ATokenInstance", - "StaticATokenLMHarness:_aTokenUnderlying=DummyERC20_aTokenUnderlying", - "StaticATokenLMHarness:_reward_A=DummyERC20_rewardToken" + "StataTokenV2Harness:INCENTIVES_CONTROLLER=RewardsControllerHarness", + "StataTokenV2Harness:POOL=SymbolicLendingPool", + "StataTokenV2Harness:_reward_A=DummyERC20_rewardToken" + ], + "packages": [ + "aave-v3-core/=certora/stata/munged/src/core", + "aave-v3-periphery/=certora/stata/munged/src/periphery", + "solidity-utils/=certora/stata/munged/lib/solidity-utils/src", + "forge-std/=certora/stata/munged/lib/forge-std/src", + "openzeppelin-contracts-upgradeable/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", + "openzeppelin-contracts/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", + "@openzeppelin/contracts/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", ], "loop_iter": "1", "msg": "aToken properties", "optimistic_hashing": true, "optimistic_loop": true, - "packages": [ - "aave-v3-core/=src/core", - "aave-v3-periphery/=src/periphery", - "solidity-utils/=lib/solidity-utils/src", - "forge-std/=lib/forge-std/src", - ], "solc": "solc8.20", "smt_timeout": "1400", - "verify": "StaticATokenLMHarness:certora/stata/specs/staticATokenLM/aTokenProperties.spec", + "verify": "StataTokenV2Harness:certora/stata/specs/staticATokenLM/aTokenProperties.spec", "build_cache": true, } \ No newline at end of file diff --git a/certora/stata/conf/verifyDoubleClaim.conf b/certora/stata/conf/verifyDoubleClaim.conf index 807e7dcb..55caf5e7 100644 --- a/certora/stata/conf/verifyDoubleClaim.conf +++ b/certora/stata/conf/verifyDoubleClaim.conf @@ -1,12 +1,12 @@ { "files": [ - "certora/stata/harness/StaticATokenLMHarness.sol", + "certora/stata/harness/StataTokenV2Harness.sol", "certora/stata/harness/pool/SymbolicLendingPool.sol", "certora/stata/harness/rewards/RewardsControllerHarness.sol", "certora/stata/harness/rewards/TransferStrategyHarness.sol", "certora/stata/harness/tokens/DummyERC20_aTokenUnderlying.sol", "certora/stata/harness/tokens/DummyERC20_rewardToken.sol", - "src/core/instances/ATokenInstance.sol", + "certora/stata/munged/src/core/instances/ATokenInstance.sol", ], "link": [ "SymbolicLendingPool:aToken=ATokenInstance", @@ -16,19 +16,20 @@ "ATokenInstance:POOL=SymbolicLendingPool", "ATokenInstance:_incentivesController=RewardsControllerHarness", "ATokenInstance:_underlyingAsset=DummyERC20_aTokenUnderlying", - "StaticATokenLMHarness:INCENTIVES_CONTROLLER=RewardsControllerHarness", - "StaticATokenLMHarness:POOL=SymbolicLendingPool", - "StaticATokenLMHarness:_aToken=ATokenInstance", - "StaticATokenLMHarness:_aTokenUnderlying=DummyERC20_aTokenUnderlying", - "StaticATokenLMHarness:_reward_A=DummyERC20_rewardToken" + "StataTokenV2Harness:INCENTIVES_CONTROLLER=RewardsControllerHarness", + "StataTokenV2Harness:POOL=SymbolicLendingPool", + "StataTokenV2Harness:_reward_A=DummyERC20_rewardToken" ], "packages": [ - "aave-v3-core/=src/core", - "aave-v3-periphery/=src/periphery", - "solidity-utils/=lib/solidity-utils/src", - "forge-std/=lib/forge-std/src", - ], - "verify":"StaticATokenLMHarness:certora/stata/specs/staticATokenLM/double_claim.spec", + "aave-v3-core/=certora/stata/munged/src/core", + "aave-v3-periphery/=certora/stata/munged/src/periphery", + "solidity-utils/=certora/stata/munged/lib/solidity-utils/src", + "forge-std/=certora/stata/munged/lib/forge-std/src", + "openzeppelin-contracts-upgradeable/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", + "openzeppelin-contracts/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", + "@openzeppelin/contracts/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", +], + "verify":"StataTokenV2Harness:certora/stata/specs/staticATokenLM/double_claim.spec", "solc": "solc8.20", "msg": "Multi rewards - double claim properties", "optimistic_loop": true, diff --git a/certora/stata/conf/verifyERC4626.conf b/certora/stata/conf/verifyERC4626.conf index 1583b01a..06900f28 100644 --- a/certora/stata/conf/verifyERC4626.conf +++ b/certora/stata/conf/verifyERC4626.conf @@ -1,12 +1,12 @@ { "files": [ - "certora/stata/harness/StaticATokenLMHarness.sol", + "certora/stata/harness/StataTokenV2Harness.sol", "certora/stata/harness/pool/SymbolicLendingPool.sol", "certora/stata/harness/rewards/RewardsControllerHarness.sol", "certora/stata/harness/rewards/TransferStrategyHarness.sol", "certora/stata/harness/tokens/DummyERC20_aTokenUnderlying.sol", "certora/stata/harness/tokens/DummyERC20_rewardToken.sol", - "src/core/instances/ATokenInstance.sol", + "certora/stata/munged/src/core/instances/ATokenInstance.sol", ], "link": [ "SymbolicLendingPool:aToken=ATokenInstance", @@ -16,23 +16,24 @@ "ATokenInstance:POOL=SymbolicLendingPool", "ATokenInstance:_incentivesController=RewardsControllerHarness", "ATokenInstance:_underlyingAsset=DummyERC20_aTokenUnderlying", - "StaticATokenLMHarness:INCENTIVES_CONTROLLER=RewardsControllerHarness", - "StaticATokenLMHarness:POOL=SymbolicLendingPool", - "StaticATokenLMHarness:_aToken=ATokenInstance", - "StaticATokenLMHarness:_aTokenUnderlying=DummyERC20_aTokenUnderlying", - "StaticATokenLMHarness:_reward_A=DummyERC20_rewardToken" + "StataTokenV2Harness:INCENTIVES_CONTROLLER=RewardsControllerHarness", + "StataTokenV2Harness:POOL=SymbolicLendingPool", + "StataTokenV2Harness:_reward_A=DummyERC20_rewardToken" ], "packages": [ - "aave-v3-core/=src/core", - "aave-v3-periphery/=src/periphery", - "solidity-utils/=lib/solidity-utils/src", - "forge-std/=lib/forge-std/src", - ], - "verify":"StaticATokenLMHarness:certora/stata/specs/erc4626/erc4626.spec", + "aave-v3-core/=certora/stata/munged/src/core", + "aave-v3-periphery/=certora/stata/munged/src/periphery", + "solidity-utils/=certora/stata/munged/lib/solidity-utils/src", + "forge-std/=certora/stata/munged/lib/forge-std/src", + "openzeppelin-contracts-upgradeable/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", + "openzeppelin-contracts/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", + "@openzeppelin/contracts/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", +], + "verify":"StataTokenV2Harness:certora/stata/specs/erc4626/erc4626.spec", "solc": "solc8.20", "msg": "ERC4626 properties", "optimistic_loop": true, - "smt_timeout": "1000", + "smt_timeout": "3600", "loop_iter": "1", "optimistic_hashing": true, "build_cache": true, diff --git a/certora/stata/conf/verifyERC4626DepositSummarization.conf b/certora/stata/conf/verifyERC4626DepositSummarization.conf index c0b6d885..d2ce588f 100644 --- a/certora/stata/conf/verifyERC4626DepositSummarization.conf +++ b/certora/stata/conf/verifyERC4626DepositSummarization.conf @@ -1,12 +1,12 @@ { "files": [ - "certora/stata/harness/StaticATokenLMHarness.sol", + "certora/stata/harness/StataTokenV2Harness.sol", "certora/stata/harness/pool/SymbolicLendingPool.sol", "certora/stata/harness/rewards/RewardsControllerHarness.sol", "certora/stata/harness/rewards/TransferStrategyHarness.sol", "certora/stata/harness/tokens/DummyERC20_aTokenUnderlying.sol", "certora/stata/harness/tokens/DummyERC20_rewardToken.sol", - "src/core/instances/ATokenInstance.sol", + "certora/stata/munged/src/core/instances/ATokenInstance.sol", ], "link": [ "SymbolicLendingPool:aToken=ATokenInstance", @@ -16,24 +16,24 @@ "ATokenInstance:POOL=SymbolicLendingPool", "ATokenInstance:_incentivesController=RewardsControllerHarness", "ATokenInstance:_underlyingAsset=DummyERC20_aTokenUnderlying", - "StaticATokenLMHarness:INCENTIVES_CONTROLLER=RewardsControllerHarness", - "StaticATokenLMHarness:POOL=SymbolicLendingPool", - "StaticATokenLMHarness:_aToken=ATokenInstance", - "StaticATokenLMHarness:_aTokenUnderlying=DummyERC20_aTokenUnderlying", - "StaticATokenLMHarness:_reward_A=DummyERC20_rewardToken" + "StataTokenV2Harness:INCENTIVES_CONTROLLER=RewardsControllerHarness", + "StataTokenV2Harness:POOL=SymbolicLendingPool", + "StataTokenV2Harness:_reward_A=DummyERC20_rewardToken" ], "packages": [ - "aave-v3-core/=src/core", - "aave-v3-periphery/=src/periphery", - "solidity-utils/=lib/solidity-utils/src", - "forge-std/=lib/forge-std/src", - ], - "verify": "StaticATokenLMHarness:certora/stata/specs/erc4626/erc4626DepositSummarization.spec", + "aave-v3-core/=certora/stata/munged/src/core", + "aave-v3-periphery/=certora/stata/munged/src/periphery", + "solidity-utils/=certora/stata/munged/lib/solidity-utils/src", + "forge-std/=certora/stata/munged/lib/forge-std/src", + "openzeppelin-contracts-upgradeable/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", + "openzeppelin-contracts/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", + "@openzeppelin/contracts/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", +], + "verify": "StataTokenV2Harness:certora/stata/specs/erc4626/erc4626DepositSummarization.spec", "solc": "solc8.20", - "msg": "ERC4626 summarized properties depositCheckIndexGRayAssert1", + "msg": "ERC4626 Deposit summarized", "optimistic_loop": true, "loop_iter": "1", "optimistic_hashing": true, "build_cache": true, -// "rule":["depositCheckIndexGRayAssert1"], } \ No newline at end of file diff --git a/certora/stata/conf/verifyERC4626Extended.conf b/certora/stata/conf/verifyERC4626Extended.conf index 52766658..fc4a5083 100644 --- a/certora/stata/conf/verifyERC4626Extended.conf +++ b/certora/stata/conf/verifyERC4626Extended.conf @@ -1,12 +1,20 @@ { "files": [ +<<<<<<< HEAD "certora/stata/harness/StaticATokenLMHarness.sol", +======= + "certora/stata/harness/StataTokenV2Harness.sol", +>>>>>>> certora-for-project-a "certora/stata/harness/pool/SymbolicLendingPool.sol", "certora/stata/harness/rewards/RewardsControllerHarness.sol", "certora/stata/harness/rewards/TransferStrategyHarness.sol", "certora/stata/harness/tokens/DummyERC20_aTokenUnderlying.sol", "certora/stata/harness/tokens/DummyERC20_rewardToken.sol", +<<<<<<< HEAD "src/core/instances/ATokenInstance.sol", +======= + "certora/stata/munged/src/core/instances/ATokenInstance.sol", +>>>>>>> certora-for-project-a ], "link": [ "SymbolicLendingPool:aToken=ATokenInstance", @@ -16,19 +24,20 @@ "ATokenInstance:POOL=SymbolicLendingPool", "ATokenInstance:_incentivesController=RewardsControllerHarness", "ATokenInstance:_underlyingAsset=DummyERC20_aTokenUnderlying", - "StaticATokenLMHarness:INCENTIVES_CONTROLLER=RewardsControllerHarness", - "StaticATokenLMHarness:POOL=SymbolicLendingPool", - "StaticATokenLMHarness:_aToken=ATokenInstance", - "StaticATokenLMHarness:_aTokenUnderlying=DummyERC20_aTokenUnderlying", - "StaticATokenLMHarness:_reward_A=DummyERC20_rewardToken" + "StataTokenV2Harness:INCENTIVES_CONTROLLER=RewardsControllerHarness", + "StataTokenV2Harness:POOL=SymbolicLendingPool", + "StataTokenV2Harness:_reward_A=DummyERC20_rewardToken" ], "packages": [ - "aave-v3-core/=src/core", - "aave-v3-periphery/=src/periphery", - "solidity-utils/=lib/solidity-utils/src", - "forge-std/=lib/forge-std/src", + "aave-v3-core/=certora/stata/munged/src/core", + "aave-v3-periphery/=certora/stata/munged/src/periphery", + "solidity-utils/=certora/stata/munged/lib/solidity-utils/src", + "forge-std/=certora/stata/munged/lib/forge-std/src", + "openzeppelin-contracts-upgradeable/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", + "openzeppelin-contracts/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", + "@openzeppelin/contracts/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", ], - "verify":"StaticATokenLMHarness:certora/stata/specs/erc4626/erc4626Extended.spec", + "verify":"StataTokenV2Harness:certora/stata/specs/erc4626/erc4626Extended.spec", "solc": "solc8.20", "msg": "ERC4626 Extended properties", "optimistic_loop": true, diff --git a/certora/stata/conf/verifyERC4626MintDepositSummarization.conf b/certora/stata/conf/verifyERC4626MintDepositSummarization.conf index 9b1ce314..25598676 100644 --- a/certora/stata/conf/verifyERC4626MintDepositSummarization.conf +++ b/certora/stata/conf/verifyERC4626MintDepositSummarization.conf @@ -1,12 +1,20 @@ { "files": [ +<<<<<<< HEAD "certora/stata/harness/StaticATokenLMHarness.sol", +======= + "certora/stata/harness/StataTokenV2Harness.sol", +>>>>>>> certora-for-project-a "certora/stata/harness/pool/SymbolicLendingPool.sol", "certora/stata/harness/rewards/RewardsControllerHarness.sol", "certora/stata/harness/rewards/TransferStrategyHarness.sol", "certora/stata/harness/tokens/DummyERC20_aTokenUnderlying.sol", "certora/stata/harness/tokens/DummyERC20_rewardToken.sol", +<<<<<<< HEAD "src/core/instances/ATokenInstance.sol", +======= + "certora/stata/munged/src/core/instances/ATokenInstance.sol", +>>>>>>> certora-for-project-a ], "link": [ "SymbolicLendingPool:aToken=ATokenInstance", @@ -16,20 +24,21 @@ "ATokenInstance:POOL=SymbolicLendingPool", "ATokenInstance:_incentivesController=RewardsControllerHarness", "ATokenInstance:_underlyingAsset=DummyERC20_aTokenUnderlying", - "StaticATokenLMHarness:INCENTIVES_CONTROLLER=RewardsControllerHarness", - "StaticATokenLMHarness:POOL=SymbolicLendingPool", - "StaticATokenLMHarness:_aToken=ATokenInstance", - "StaticATokenLMHarness:_aTokenUnderlying=DummyERC20_aTokenUnderlying", - "StaticATokenLMHarness:_reward_A=DummyERC20_rewardToken" + "StataTokenV2Harness:INCENTIVES_CONTROLLER=RewardsControllerHarness", + "StataTokenV2Harness:POOL=SymbolicLendingPool", + "StataTokenV2Harness:_reward_A=DummyERC20_rewardToken" ], "packages": [ - "aave-v3-core/=src/core", - "aave-v3-periphery/=src/periphery", - "solidity-utils/=lib/solidity-utils/src", - "forge-std/=lib/forge-std/src", + "aave-v3-core/=certora/stata/munged/src/core", + "aave-v3-periphery/=certora/stata/munged/src/periphery", + "solidity-utils/=certora/stata/munged/lib/solidity-utils/src", + "forge-std/=certora/stata/munged/lib/forge-std/src", + "openzeppelin-contracts-upgradeable/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", + "openzeppelin-contracts/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", + "@openzeppelin/contracts/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", ], "verify": - "StaticATokenLMHarness:certora/stata/specs/erc4626/erc4626MintDepositSummarization.spec", + "StataTokenV2Harness:certora/stata/specs/erc4626/erc4626MintDepositSummarization.spec", "solc": "solc8.20", "msg": "ERC4626 Summarized no transferFrom properties", "optimistic_loop": true, diff --git a/certora/stata/conf/verifyStataToken.conf b/certora/stata/conf/verifyStataToken.conf new file mode 100644 index 00000000..a1406810 --- /dev/null +++ b/certora/stata/conf/verifyStataToken.conf @@ -0,0 +1,40 @@ +{ + "files": [ + "certora/stata/harness/StataTokenV2Harness.sol", + "certora/stata/harness/pool/SymbolicLendingPool.sol", + "certora/stata/harness/rewards/RewardsControllerHarness.sol", + "certora/stata/harness/rewards/TransferStrategyHarness.sol", + "certora/stata/harness/tokens/DummyERC20_aTokenUnderlying.sol", + "certora/stata/harness/tokens/DummyERC20_rewardToken.sol", + "certora/stata/munged/src/core/instances/ATokenInstance.sol", + ], + "link": [ + "SymbolicLendingPool:aToken=ATokenInstance", + "SymbolicLendingPool:underlyingToken=DummyERC20_aTokenUnderlying", + "TransferStrategyHarness:INCENTIVES_CONTROLLER=RewardsControllerHarness", + "TransferStrategyHarness:REWARD=DummyERC20_rewardToken", + "ATokenInstance:POOL=SymbolicLendingPool", + "ATokenInstance:_incentivesController=RewardsControllerHarness", + "ATokenInstance:_underlyingAsset=DummyERC20_aTokenUnderlying", + "StataTokenV2Harness:INCENTIVES_CONTROLLER=RewardsControllerHarness", + "StataTokenV2Harness:POOL=SymbolicLendingPool", + "StataTokenV2Harness:_reward_A=DummyERC20_rewardToken" + ], + "packages": [ + "aave-v3-core/=certora/stata/munged/src/core", + "aave-v3-periphery/=certora/stata/munged/src/periphery", + "solidity-utils/=certora/stata/munged/lib/solidity-utils/src", + "forge-std/=certora/stata/munged/lib/forge-std/src", + "openzeppelin-contracts-upgradeable/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", + "openzeppelin-contracts/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", + "@openzeppelin/contracts/=certora/stata/munged/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", + ], + "verify":"StataTokenV2Harness:certora/stata/specs/StataToken/StataToken.spec", + "solc": "solc8.20", + "msg": "Rewards related properties", + "optimistic_loop": true, + "smt_timeout": "1400", + "loop_iter": "1", + "optimistic_hashing": true, + "build_cache": true, +} \ No newline at end of file diff --git a/certora/stata/harness/StaticATokenLMHarness.sol b/certora/stata/harness/StataTokenV2Harness.sol similarity index 63% rename from certora/stata/harness/StaticATokenLMHarness.sol rename to certora/stata/harness/StataTokenV2Harness.sol index ecd932be..ce615d08 100644 --- a/certora/stata/harness/StaticATokenLMHarness.sol +++ b/certora/stata/harness/StataTokenV2Harness.sol @@ -1,42 +1,40 @@ // SPDX-License-Identifier: agpl-3.0 pragma solidity ^0.8.10; -import {StaticATokenLM, StaticATokenErrors, IPool, IRewardsController, IERC20} from 'aave-v3-periphery/contracts/static-a-token/StaticATokenLM.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/interfaces/IERC20.sol'; +import {StataTokenV2, IPool, IRewardsController} from 'aave-v3-periphery/contracts/static-a-token/StataTokenV2.sol'; import {SymbolicLendingPool} from './pool/SymbolicLendingPool.sol'; -contract StaticATokenLMHarness is StaticATokenLM{ - +contract StataTokenV2Harness is StataTokenV2 { address internal _reward_A; - address internal _reward_B; constructor( - IPool pool, - IRewardsController rewardsController - ) StaticATokenLM(pool, rewardsController){} - - // returns the address of the underlying asset of the static aToken - function getStaticATokenUnderlying() public view returns (address){ - return _aTokenUnderlying; + IPool pool, + IRewardsController rewardsController + ) StataTokenV2(pool, rewardsController) {} + + function rate() external view returns (uint256) { + return _rate(); } - + // returns the address of the i-th reward token in the reward tokens list maintained by the static aToken function getRewardToken(uint256 i) external view returns (address) { - return _rewardTokens[i]; + return rewardTokens()[i]; } // returns the length of the reward tokens list maintained by the static aToken function getRewardTokensLength() external view returns (uint256) { - return _rewardTokens.length; + return rewardTokens().length; } // returns a user's reward index on last interaction for a given reward - function getRewardsIndexOnLastInteraction(address user, address reward) - external view returns (uint128) { - UserRewardsData memory currentUserRewardsData = _userRewardsData[user][reward]; - return currentUserRewardsData.rewardsIndexOnLastInteraction; - } + // function getRewardsIndexOnLastInteraction(address user, address reward) + // external view returns (uint128) { + // UserRewardsData memory currentUserRewardsData = _userRewardsData[user][reward]; + // return currentUserRewardsData.rewardsIndexOnLastInteraction; + // } // claims rewards for a user on the static aToken. // the method builds the rewards array with a single reward and calls the internal claim function with it @@ -53,8 +51,7 @@ contract StaticATokenLMHarness is StaticATokenLM{ // @MM - think of the best way to get rid of this require require( msg.sender == onBehalfOf || - msg.sender == INCENTIVES_CONTROLLER.getClaimer(onBehalfOf), - StaticATokenErrors.INVALID_CLAIMER + msg.sender == INCENTIVES_CONTROLLER.getClaimer(onBehalfOf) ); _claimRewardsOnBehalf(onBehalfOf, receiver, rewards); } @@ -74,25 +71,15 @@ contract StaticATokenLMHarness is StaticATokenLM{ require( msg.sender == onBehalfOf || - msg.sender == INCENTIVES_CONTROLLER.getClaimer(onBehalfOf), - StaticATokenErrors.INVALID_CLAIMER + msg.sender == INCENTIVES_CONTROLLER.getClaimer(onBehalfOf) ); _claimRewardsOnBehalf(onBehalfOf, receiver, rewards); } - + // wrapper function for the erc20 _mint function. Used to reduce running times function _mintWrapper(address to, uint256 amount) external { _mint(to, amount); } - - function getUserRewardsData(address user, address reward) - external view - returns (uint128) { - UserRewardsData memory currentUserRewardsData = _userRewardsData[user][ - reward - ]; - return currentUserRewardsData.rewardsIndexOnLastInteraction; - } - + } diff --git a/certora/stata/harness/pool/SymbolicLendingPool.sol b/certora/stata/harness/pool/SymbolicLendingPool.sol index fda7f324..ec3b7ef4 100644 --- a/certora/stata/harness/pool/SymbolicLendingPool.sol +++ b/certora/stata/harness/pool/SymbolicLendingPool.sol @@ -78,8 +78,31 @@ contract SymbolicLendingPool { return liquidityIndex; } + DataTypes.ReserveDataLegacy reserveLegacy; DataTypes.ReserveData reserve; - function getReserveData(address asset) external view returns (DataTypes.ReserveData memory) { + + function getReserveData(address asset) external view returns (DataTypes.ReserveDataLegacy memory) { + DataTypes.ReserveDataLegacy memory res; + + res.configuration = reserve.configuration; + res.liquidityIndex = reserve.liquidityIndex; + res.currentLiquidityRate = reserve.currentLiquidityRate; + res.variableBorrowIndex = reserve.variableBorrowIndex; + res.currentVariableBorrowRate = reserve.currentVariableBorrowRate; + res.currentStableBorrowRate = reserve.currentStableBorrowRate; + res.lastUpdateTimestamp = reserve.lastUpdateTimestamp; + res.id = reserve.id; + res.aTokenAddress = reserve.aTokenAddress; + res.stableDebtTokenAddress = reserve.stableDebtTokenAddress; + res.variableDebtTokenAddress = reserve.variableDebtTokenAddress; + res.interestRateStrategyAddress = reserve.interestRateStrategyAddress; + res.accruedToTreasury = reserve.accruedToTreasury; + res.unbacked = reserve.unbacked; + res.isolationModeTotalDebt = reserve.isolationModeTotalDebt; + return res; + } + + function getReserveDataExtended(address asset) external view returns (DataTypes.ReserveData memory) { return reserve; } } diff --git a/certora/stata/harness/tokens/DummyERC20Impl.sol b/certora/stata/harness/tokens/DummyERC20Impl.sol index a4822551..d6f32d65 100644 --- a/certora/stata/harness/tokens/DummyERC20Impl.sol +++ b/certora/stata/harness/tokens/DummyERC20Impl.sol @@ -1,46 +1,41 @@ // SPDX-License-Identifier: agpl-3.0 -pragma solidity ^0.8.10; +pragma solidity ^0.8.0; -// with mint contract DummyERC20Impl { uint256 t; - mapping (address => uint256) b; - mapping (address => mapping (address => uint256)) a; + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; string public name; string public symbol; uint public decimals; - function myAddress() public returns (address) { + function myAddress() external view 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); + b[msg.sender] -= amount; + 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; } @@ -49,9 +44,10 @@ contract DummyERC20Impl { 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); + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + return true; } } \ No newline at end of file diff --git a/certora/stata/harness/tokens/DummyERC20_rewardTokenB.sol b/certora/stata/harness/tokens/DummyERC20_rewardTokenB.sol deleted file mode 100644 index e5072788..00000000 --- a/certora/stata/harness/tokens/DummyERC20_rewardTokenB.sol +++ /dev/null @@ -1,5 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; -import "./DummyERC20Impl.sol"; - -contract DummyERC20_rewardTokenB is DummyERC20Impl {} diff --git a/certora/stata/scripts/run-all.sh b/certora/stata/scripts/run-all.sh index cf5e50e9..c00dcb4e 100644 --- a/certora/stata/scripts/run-all.sh +++ b/certora/stata/scripts/run-all.sh @@ -1,118 +1,95 @@ #CMN="--compilation_steps_only" - - echo "******** Running: 1 ***************" -certoraRun $CMN certora/stata/conf/verifyERC4626.conf --rule previewMintIndependentOfAllowance previewRedeemIndependentOfBalance previewMintAmountCheck previewDepositIndependentOfAllowanceApprove previewWithdrawIndependentOfMaxWithdraw previewWithdrawAmountCheck previewWithdrawIndependentOfBalance2 previewWithdrawIndependentOfBalance1 previewRedeemIndependentOfMaxRedeem1 previewRedeemAmountCheck previewRedeemIndependentOfMaxRedeem2 amountConversionRoundedDown withdrawCheck redeemCheck convertToAssetsCheck convertToSharesCheck toAssetsDoesNotRevert sharesConversionRoundedDown toSharesDoesNotRevert previewDepositAmountCheck maxRedeemCompliance maxWithdrawConversionCompliance \ +certoraRun $CMN certora/stata/conf/verifyERC4626.conf --rule previewRedeemIndependentOfBalance previewMintAmountCheck previewDepositIndependentOfAllowanceApprove previewWithdrawAmountCheck previewWithdrawIndependentOfBalance2 previewWithdrawIndependentOfBalance1 previewRedeemIndependentOfMaxRedeem1 previewRedeemAmountCheck previewRedeemIndependentOfMaxRedeem2 amountConversionRoundedDown withdrawCheck redeemCheck redeemATokensCheck convertToAssetsCheck convertToSharesCheck toAssetsDoesNotRevert sharesConversionRoundedDown toSharesDoesNotRevert previewDepositAmountCheck maxRedeemCompliance maxWithdrawConversionCompliance \ + maxMintMustntRevert maxDepositMustntRevert maxRedeemMustntRevert maxWithdrawMustntRevert totalAssetsMustntRevert \ --msg "1: verifyERC4626.conf" -# maxMintMustntRevert maxDepositMustntRevert maxRedeemMustntRevert maxWithdrawMustntRevert + +echo "******** Running: 1.5 ***************" +certoraRun $CMN certora/stata/conf/verifyERC4626.conf --rule previewWithdrawIndependentOfMaxWithdraw \ +--msg "1.5: verifyERC4626.conf" echo "******** Running: 2 ***************" -certoraRun $CMN certora/stata/conf/verifyERC4626MintDepositSummarization.conf --rule depositCheckIndexGRayAssert2 depositCheckIndexERayAssert2 mintCheckIndexGRayUpperBound mintCheckIndexGRayLowerBound mintCheckIndexEqualsRay \ +certoraRun $CMN certora/stata/conf/verifyERC4626MintDepositSummarization.conf --rule depositCheckIndexGRayAssert2 depositATokensCheckIndexGRayAssert2 depositWithPermitCheckIndexGRayAssert2 depositCheckIndexERayAssert2 depositATokensCheckIndexERayAssert2 depositWithPermitCheckIndexERayAssert2 mintCheckIndexGRayUpperBound mintCheckIndexGRayLowerBound mintCheckIndexEqualsRay \ --msg "2: verifyERC4626MintDepositSummarization.conf" echo "******** Running: 3 ***************" -certoraRun $CMN certora/stata/conf/verifyERC4626DepositSummarization.conf --rule depositCheckIndexERayAssert1 \ +certoraRun $CMN certora/stata/conf/verifyERC4626DepositSummarization.conf --rule depositCheckIndexGRayAssert1 depositATokensCheckIndexGRayAssert1 depositWithPermitCheckIndexGRayAssert1 depositCheckIndexERayAssert1 depositATokensCheckIndexERayAssert1 depositWithPermitCheckIndexERayAssert1 \ --msg "3: " echo "******** Running: 4 ***************" certoraRun $CMN certora/stata/conf/verifyERC4626Extended.conf --rule previewWithdrawRoundingRange previewRedeemRoundingRange amountConversionPreserved sharesConversionPreserved accountsJoiningSplittingIsLimited convertSumOfAssetsPreserved previewDepositSameAsDeposit previewMintSameAsMint \ + maxDepositConstant \ --msg "4: " - # maxDepositConstant \ echo "******** Running: 5 ***************" certoraRun $CMN certora/stata/conf/verifyERC4626Extended.conf --rule redeemSum \ --msg "5: " echo "******** Running: 6 ***************" -certoraRun $CMN certora/stata/conf/verifyAToken.conf --rule aTokenBalanceIsFixed_for_collectAndUpdateRewards aTokenBalanceIsFixed_for_claimRewards aTokenBalanceIsFixed_for_claimRewardsOnBehalf \ +certoraRun $CMN certora/stata/conf/verifyERC4626Extended.conf --rule redeemATokensSum \ --msg "6: " echo "******** Running: 7 ***************" -certoraRun $CMN certora/stata/conf/verifyAToken.conf --rule aTokenBalanceIsFixed_for_claimSingleRewardOnBehalf aTokenBalanceIsFixed_for_claimRewardsToSelf \ +certoraRun $CMN certora/stata/conf/verifyAToken.conf --rule aTokenBalanceIsFixed_for_collectAndUpdateRewards aTokenBalanceIsFixed_for_claimRewards aTokenBalanceIsFixed_for_claimRewardsOnBehalf \ --msg "7: " echo "******** Running: 8 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule rewardsConsistencyWhenSufficientRewardsExist \ +certoraRun $CMN certora/stata/conf/verifyAToken.conf --rule aTokenBalanceIsFixed_for_claimSingleRewardOnBehalf aTokenBalanceIsFixed_for_claimRewardsToSelf \ --msg "8: " echo "******** Running: 9 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule rewardsConsistencyWhenInsufficientRewards \ +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule rewardsConsistencyWhenSufficientRewardsExist \ --msg "9: " echo "******** Running: 10 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule rewardsTotalDeclinesOnlyByClaim \ +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule rewardsConsistencyWhenInsufficientRewards \ --msg "10: " echo "******** Running: 11 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule rewardsTotalDeclinesOnlyByClaim_timedout_methods \ +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule totalClaimableRewards_stable \ --msg "11: " echo "******** Running: 12 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule rewardsTotalDeclinesOnlyByClaim_redeem_methods \ +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule solvency_positive_total_supply_only_if_positive_asset \ --msg "12: " echo "******** Running: 13 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule solvency_positive_total_supply_only_if_positive_asset \ +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule solvency_total_asset_geq_total_supply \ --msg "13: " echo "******** Running: 14 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule solvency_total_asset_geq_total_supply \ +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule singleAssetAccruedRewards \ --msg "14: " echo "******** Running: 15 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule solvency_total_asset_geq_total_supply_CASE_SPLIT_redeem \ +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule totalAssets_stable \ --msg "15: " echo "******** Running: 16 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule singleAssetAccruedRewards \ +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule getClaimableRewards_stable \ --msg "16: " echo "******** Running: 17 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule totalAssets_stable \ +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule getClaimableRewards_stable_after_deposit \ --msg "17: " - + echo "******** Running: 18 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule totalAssets_stable_after_collectAndUpdateRewards \ +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule getClaimableRewards_stable_after_refreshRewardTokens \ --msg "18: " echo "******** Running: 19 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule reward_balance_stable_after_collectAndUpdateRewards \ +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule getClaimableRewardsBefore_leq_claimed_claimRewardsOnBehalf \ --msg "19: " echo "******** Running: 20 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule totalClaimableRewards_stable_CASE_SPLIT \ +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule rewardsTotalDeclinesOnlyByClaim \ --msg "20: " echo "******** Running: 21 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule getClaimableRewards_stable \ +certoraRun $CMN certora/stata/conf/verifyDoubleClaim.conf --rule prevent_duplicate_reward_claiming_single_reward_sufficient \ --msg "21: " echo "******** Running: 22 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule getClaimableRewards_stable_after_deposit \ ---msg "22: " - -echo "******** Running: 23 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule getClaimableRewards_stable_after_redeem \ ---msg "23: " - -echo "******** Running: 24 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule totalClaimableRewards_stable_CASE_SPLIT_deposit \ ---msg "24: " - -echo "******** Running: 25 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule getClaimableRewards_stable_after_refreshRewardTokens \ ---msg "25: " - -echo "******** Running: 26 ***************" -certoraRun $CMN certora/stata/conf/verifyStaticATokenLM.conf --rule getClaimableRewardsBefore_leq_claimed_claimRewardsOnBehalf \ ---msg "26: " - -echo "******** Running: 27 ***************" -certoraRun $CMN certora/stata/conf/verifyDoubleClaim.conf --rule prevent_duplicate_reward_claiming_single_reward_sufficient \ ---msg "27: " - -echo "******** Running: 28 ***************" certoraRun $CMN certora/stata/conf/verifyDoubleClaim.conf --rule prevent_duplicate_reward_claiming_single_reward_insufficient \ ---msg "28: " - +--msg "22: " diff --git a/certora/stata/specs/StataToken/StataToken.spec b/certora/stata/specs/StataToken/StataToken.spec new file mode 100644 index 00000000..80d33b17 --- /dev/null +++ b/certora/stata/specs/StataToken/StataToken.spec @@ -0,0 +1,396 @@ +import "../methods/methods_base.spec"; + +/////////////////// Methods //////////////////////// + + methods { + function _.getIncentivesController() external => CONSTANT; + function _.getRewardsList() external => NONDET; + //call by RewardsController.IncentivizedERC20.sol and also by StaticATokenLM.sol + function _.handleAction(address,uint256,uint256) external => DISPATCHER(true); + + function balanceOf(address) external returns (uint256) envfree; + function totalSupply() external returns (uint256) envfree; + } + + +///////////////// Properties /////////////////////// + + /** + * @title Rewards claiming when sufficient rewards exist + * Ensures rewards are updated correctly after claiming, when there are enough + * reward funds. + * + * @dev Passed in job-id=`655ba8737ada43efab71eaabf8d41096` + */ + rule rewardsConsistencyWhenSufficientRewardsExist() { + // Assuming single reward + single_RewardToken_setup(); + + // Create a rewards array + address[] _rewards; + require _rewards[0] == _DummyERC20_rewardToken; + require _rewards.length == 1; + + env e; + require e.msg.sender != currentContract; // Cannot claim to contract + uint256 rewardsBalancePre = _DummyERC20_rewardToken.balanceOf(e.msg.sender); + uint256 claimablePre = getClaimableRewards(e, e.msg.sender, _DummyERC20_rewardToken); + + // Ensure contract has sufficient rewards + require _DummyERC20_rewardToken.balanceOf(currentContract) >= claimablePre; + + claimRewardsToSelf(e, _rewards); + + uint256 rewardsBalancePost = _DummyERC20_rewardToken.balanceOf(e.msg.sender); + uint256 unclaimedPost = getUnclaimedRewards(e.msg.sender, _DummyERC20_rewardToken); + uint256 claimablePost = getClaimableRewards(e, e.msg.sender, _DummyERC20_rewardToken); + + assert rewardsBalancePost >= rewardsBalancePre, "Rewards balance reduced after claim"; + mathint rewardsGiven = rewardsBalancePost - rewardsBalancePre; + assert to_mathint(claimablePre) == rewardsGiven + unclaimedPost, "Rewards given unequal to claimable"; + assert claimablePost == unclaimedPost, "Claimable different from unclaimed"; + assert unclaimedPost == 0; // Left last as this is an implementation detail + } + + /** + * @title Rewards claiming when rewards are insufficient + * Ensures rewards are updated correctly after claiming, when there aren't + * enough funds. + */ + rule rewardsConsistencyWhenInsufficientRewards() { + // Assuming single reward + single_RewardToken_setup(); + + env e; + require e.msg.sender != currentContract; // Cannot claim to contract + require e.msg.sender != _TransferStrategy; + + uint256 rewardsBalancePre = _DummyERC20_rewardToken.balanceOf(e.msg.sender); + uint256 claimablePre = getClaimableRewards(e, e.msg.sender, _DummyERC20_rewardToken); + + // Ensure contract does not have sufficient rewards + require _DummyERC20_rewardToken.balanceOf(currentContract) < claimablePre; + + claimSingleRewardOnBehalf(e, e.msg.sender, e.msg.sender, _DummyERC20_rewardToken); + + uint256 rewardsBalancePost = _DummyERC20_rewardToken.balanceOf(e.msg.sender); + uint256 unclaimedPost = getUnclaimedRewards(e.msg.sender, _DummyERC20_rewardToken); + uint256 claimablePost = getClaimableRewards(e, e.msg.sender, _DummyERC20_rewardToken); + + assert rewardsBalancePost >= rewardsBalancePre, "Rewards balance reduced after claim"; + mathint rewardsGiven = rewardsBalancePost - rewardsBalancePre; + // Note, when `rewardsGiven` is 0 the unclaimed rewards are not updated + assert ( + ( (rewardsGiven > 0) => (to_mathint(claimablePre) == rewardsGiven + unclaimedPost) ) && + ( (rewardsGiven == 0) => (claimablePre == claimablePost) ) + ), "Claimable rewards changed unexpectedly"; + } + + + /** + * @title Only claiming rewards should reduce contract's total rewards balance + * Only "claim reward" methods should cause the total rewards balance of + * `StaticATokenLM` to decline. Note that `initialize` and `emergencyEtherTransfer` + * are filtered out. To avoid timeouts the rest of the + * methods were split between several versions of this rule. + * + * @dev Passed with rule-sanity in job-id=`98beb842d5b94278ac4a9222249fb564` + * + */ + rule rewardsTotalDeclinesOnlyByClaim(method f) filtered { + f -> ( + f.contract == currentContract && + !harnessOnlyMethods(f) && + f.selector != sig:initialize(address, string, string).selector) && + f.selector != sig:emergencyEtherTransfer(address,uint256).selector + } { + // Assuming single reward + single_RewardToken_setup(); + rewardsController_reward_setup(); + + require _AToken.UNDERLYING_ASSET_ADDRESS() == _DummyERC20_aTokenUnderlying; + + env e; + require e.msg.sender != currentContract; + uint256 preTotal = getTotalClaimableRewards(e, _DummyERC20_rewardToken); + + calldataarg args; + f(e, args); + + uint256 postTotal = getTotalClaimableRewards(e, _DummyERC20_rewardToken); + + assert (postTotal < preTotal) => ( + (f.selector == sig:claimRewardsOnBehalf(address, address, address[]).selector) || + (f.selector == sig:claimRewards(address, address[]).selector) || + (f.selector == sig:claimRewardsToSelf(address[]).selector) || + (f.selector == sig:claimSingleRewardOnBehalf(address,address,address).selector) || + (f.selector == sig:emergencyTokenTransfer(address,address,uint256).selector) + ), "Total rewards decline due to function other than claim or emergency rescue"; + } + + //pass -t=1400,-mediumTimeout=800,-depth=10 + /// @notice Total supply is non-zero only if total assets is non-zero + invariant solvency_positive_total_supply_only_if_positive_asset() + ((_AToken.scaledBalanceOf(currentContract) == 0) => (totalSupply() == 0)) + filtered { f -> + f.contract == currentContract + && !harnessMethodsMinusHarnessClaimMethods(f) + && !claimFunctions(f) + && f.selector != sig:claimDoubleRewardOnBehalfSame(address, address, address).selector + && f.selector != sig:emergencyTokenTransfer(address,address,uint256).selector + && f.selector != sig:emergencyEtherTransfer(address,uint256).selector + } + { + preserved redeem(uint256 shares, address receiver, address owner) with (env e1) { + requireInvariant solvency_total_asset_geq_total_supply(); + require balanceOf(owner) <= totalSupply(); + } + preserved redeemATokens(uint256 shares, address receiver, address owner) with (env e2) { + requireInvariant solvency_total_asset_geq_total_supply(); + require balanceOf(owner) <= totalSupply(); + } + preserved withdraw(uint256 assets, address receiver, address owner) with (env e3) { + requireInvariant solvency_total_asset_geq_total_supply(); + require balanceOf(owner) <= totalSupply(); + } + + } + + + + //pass with -t=1400,-mediumTimeout=800,-depth=15 + //https://vaas-stg.certora.com/output/99352/7252b6b75144419c825fb00f1f11acc8/?anonymousKey=8cb67238d3cb2a14c8fbad5c1c8554b00221de95 + //pass with -t=1400,-mediumTimeout=800,-depth=10 + + /// @nitce Total assets is greater than or equal to total supply. + invariant solvency_total_asset_geq_total_supply() + (_AToken.scaledBalanceOf(currentContract) >= totalSupply()) + filtered { f -> + f.contract == currentContract + && !harnessMethodsMinusHarnessClaimMethods(f) + && !claimFunctions(f) + && f.selector != sig:emergencyEtherTransfer(address,uint256).selector + && f.selector != sig:emergencyTokenTransfer(address,address,uint256).selector + && f.selector != sig:claimDoubleRewardOnBehalfSame(address, address, address).selector } + { + preserved withdraw(uint256 assets, address receiver, address owner) with (env e3) { + require balanceOf(owner) <= totalSupply(); + } + preserved depositWithPermit(uint256 assets, address receiver, uint256 deadline, IERC4626StataToken.SignatureParams signature, bool depositToAave) with (env e4) { + require balanceOf(receiver) <= totalSupply(); + require e4.msg.sender != currentContract; + } + preserved depositATokens(uint256 assets, address receiver) with (env e5) { + require balanceOf(receiver) <= totalSupply(); + require e5.msg.sender != currentContract; + } + preserved deposit(uint256 assets, address receiver) with (env e5) { + require balanceOf(receiver) <= totalSupply(); + require e5.msg.sender != currentContract; + } + preserved mint(uint256 shares, address receiver) with (env e6) { + require balanceOf(receiver) <= totalSupply(); + require e6.msg.sender != currentContract; + } + preserved redeem(uint256 shares, address receiver, address owner) with (env e2) { + require balanceOf(owner) <= totalSupply(); + } + preserved redeemATokens(uint256 shares, address receiver, address owner) with (env e2) { + require balanceOf(owner) <= totalSupply(); + } + } + + + + //pass + /// @title correct accrued value is fetched + /// @notice assume a single asset + //pass with rule_sanity basic except metaDeposit() + //https://vaas-stg.certora.com/output/99352/ab6c92a9f96d4327b52da331d634d3ab/?anonymousKey=abb27f614a8656e6e300ce21c517009cbe0c4d3a + //https://vaas-stg.certora.com/output/99352/d8c9a8bbea114d5caad43683b06d8ba0/?anonymousKey=a079d7f7dd44c47c05c866808c32235d56bca8e8 + invariant singleAssetAccruedRewards(env e0, address _asset, address reward, address user) + ((_RewardsController.getAssetListLength() == 1 && _RewardsController.getAssetByIndex(0) == _asset) + => (_RewardsController.getUserAccruedReward(_asset, reward, user) == _RewardsController.getUserAccruedRewards(reward, user))) + filtered {f -> + f.contract == currentContract && + f.selector != sig:emergencyEtherTransfer(address,uint256).selector && + !harnessOnlyMethods(f) + } + { + preserved with (env e1){ + setup(e1, user); + require _asset != _RewardsController; + require _asset != _TransferStrategy; + require reward != _StaticATokenLM; + require reward != _AToken; + require reward != _TransferStrategy; + } + } + + + + //pass with --rule_sanity basic + //https://vaas-stg.certora.com/output/99352/4df615c845e2445b8657ece2db477ce5/?anonymousKey=76379915d60fc1056ed4e5b391c69cd5bba3cce0 + /// @title Claiming rewards should not affect totalAssets() + rule totalAssets_stable(method f) + filtered { f -> f.selector == sig:claimSingleRewardOnBehalf(address, address, address).selector + || f.selector == sig:collectAndUpdateRewards(address).selector } + { + env e; + calldataarg args; + mathint totalAssetBefore = totalAssets(); + f(e, args); + mathint totalAssetAfter = totalAssets(); + assert totalAssetAfter == totalAssetBefore; + } + + /// @title getTotalClaimableRewards() is stable unless rewards were claimed or emergency rescue was applied + rule totalClaimableRewards_stable(method f) + filtered { f -> + f.contract == currentContract + && !f.isView + && !claimFunctions(f) + && !collectAndUpdateFunction(f) + && !harnessOnlyMethods(f) + && f.selector != sig:initialize(address,string,string).selector + && f.selector != sig:emergencyEtherTransfer(address,uint256).selector + } + { + env e; + require e.msg.sender != currentContract; + setup(e, 0); + calldataarg args; + address reward; + require e.msg.sender != reward ; + require currentContract != e.msg.sender; + require _AToken != e.msg.sender; + require _RewardsController != e.msg.sender; + require _DummyERC20_aTokenUnderlying != e.msg.sender; + require _DummyERC20_rewardToken != e.msg.sender; + require _SymbolicLendingPool != e.msg.sender; + require _TransferStrategy != e.msg.sender; + + require currentContract != reward; + require _AToken != reward; + require _RewardsController != reward; + require _DummyERC20_aTokenUnderlying != reward; + require _SymbolicLendingPool != reward; + require _TransferStrategy != reward; + require _TransferStrategy != reward; + + + mathint totalClaimableRewardsBefore = getTotalClaimableRewards(e, reward); + f(e, args); + mathint totalClaimableRewardsAfter = getTotalClaimableRewards(e, reward); + assert totalClaimableRewardsAfter == totalClaimableRewardsBefore || + f.selector == sig:emergencyTokenTransfer(address,address,uint256).selector; + } + + + + //pass with -t=1400,-mediumTimeout=800,-depth=15 + //https://vaas-stg.certora.com/output/99352/a10c05634b4342d6b31f777826444616/?anonymousKey=67bb71ebd716ef5d10be8743ded7b466f699e32c + //pass with -t=1400,-mediumTimeout=800,-depth=10 +rule getClaimableRewards_stable(method f) + filtered { f -> + f.contract == currentContract && + !f.isView + && !claimFunctions(f) + && !collectAndUpdateFunction(f) + && f.selector != sig:initialize(address,string,string).selector + && f.selector != sig:emergencyEtherTransfer(address,uint256).selector + && !harnessOnlyMethods(f) + } + { + env e; + calldataarg args; + address user; + address reward; + + require user != 0; + + require currentContract != reward; + require _AToken != reward; + require _RewardsController != reward; // + require _DummyERC20_aTokenUnderlying != reward; + require _SymbolicLendingPool != reward; + require _TransferStrategy != reward; + + //require isRegisteredRewardToken(reward); //todo: review the assumption + + mathint claimableRewardsBefore = getClaimableRewards(e, user, reward); + + require getRewardTokensLength() > 0; + require getRewardToken(0) == reward; //todo: review + require _RewardsController.getAvailableRewardsCount(_AToken) > 0; //todo: review + require _RewardsController.getRewardsByAsset(_AToken, 0) == reward; //todo: review + f(e, args); + mathint claimableRewardsAfter = getClaimableRewards(e, user, reward); + assert claimableRewardsAfter == claimableRewardsBefore; + } + + + + //pass + rule getClaimableRewards_stable_after_deposit() + { + env e; + address user; + address reward; + + uint256 assets; + address recipient; + // uint16 referralCode; + // bool fromUnderlying; + + require user != 0; + + + mathint claimableRewardsBefore = getClaimableRewards(e, user, reward); + require getRewardTokensLength() > 0; + require getRewardToken(0) == reward; //todo: review + + require _RewardsController.getAvailableRewardsCount(_AToken) > 0; //todo: review + require _RewardsController.getRewardsByAsset(_AToken, 0) == reward; //todo: review + // deposit(e, assets, recipient,referralCode,fromUnderlying); + depositATokens(e, assets, recipient); // try depositWithPermit() + mathint claimableRewardsAfter = getClaimableRewards(e, user, reward); + assert claimableRewardsAfter == claimableRewardsBefore; + } + + + + //todo: remove + //pass with --loop_iter=2 --rule_sanity basic + //https://vaas-stg.certora.com/output/99352/290a1108baa64316ac4f20b5501b4617/?anonymousKey=930379a90af5aa498ec3fed2110a08f5c096efb3 + /// @title getClaimableRewards() is stable unless rewards were claimed + rule getClaimableRewards_stable_after_refreshRewardTokens() + { + env e; + address user; + address reward; + + mathint claimableRewardsBefore = getClaimableRewards(e, user, reward); + refreshRewardTokens(e); + + mathint claimableRewardsAfter = getClaimableRewards(e, user, reward); + assert claimableRewardsAfter == claimableRewardsBefore; + } + + + /// @title The amount of rewards that was actually received by claimRewards() cannot exceed the initial amount of rewards + rule getClaimableRewardsBefore_leq_claimed_claimRewardsOnBehalf(method f) + { + env e; + address onBehalfOf; + address receiver; + require receiver != currentContract; + + mathint balanceBefore = _DummyERC20_rewardToken.balanceOf(receiver); + mathint claimableRewardsBefore = getClaimableRewards(e, onBehalfOf, _DummyERC20_rewardToken); + claimSingleRewardOnBehalf(e, onBehalfOf, receiver, _DummyERC20_rewardToken); + mathint balanceAfter = _DummyERC20_rewardToken.balanceOf(receiver); + mathint deltaBalance = balanceAfter - balanceBefore; + + assert deltaBalance <= claimableRewardsBefore; + } diff --git a/certora/stata/specs/staticATokenLM/aTokenProperties.spec b/certora/stata/specs/StataToken/aTokenProperties.spec similarity index 51% rename from certora/stata/specs/staticATokenLM/aTokenProperties.spec rename to certora/stata/specs/StataToken/aTokenProperties.spec index e8ff929a..55d4e705 100644 --- a/certora/stata/specs/staticATokenLM/aTokenProperties.spec +++ b/certora/stata/specs/StataToken/aTokenProperties.spec @@ -1,13 +1,6 @@ import "../methods/methods_base.spec"; -/////////////////// Methods //////////////////////// - - methods - { - function _.permit(address,address,uint256,uint256,uint8,bytes32,bytes32) external => NONDET; - } - ////////////////// FUNCTIONS ////////////////////// /// @title Sum of scaled balances of AToken @@ -47,25 +40,16 @@ import "../methods/methods_base.spec"; */ rule aTokenBalanceIsFixed(method f) filtered { // Exclude balance changing methods - f -> (f.selector != sig:deposit(uint256,address).selector) && - (f.selector != sig:deposit(uint256,address,uint16,bool).selector) && + f -> (f.selector != sig:depositATokens(uint256,address).selector) && (f.selector != sig:withdraw(uint256,address,address).selector) && - (f.selector != sig:redeem(uint256,address,address).selector) && - (f.selector != sig:redeem(uint256,address,address,bool).selector) && + (f.selector != sig:redeemATokens(uint256,address,address).selector) && (f.selector != sig:mint(uint256,address).selector) && - (f.selector != sig:metaDeposit(address,address,uint256,uint16,bool,uint256, - IStaticATokenLM.PermitParams,IStaticATokenLM.SignatureParams).selector) && - (f.selector != sig:metaWithdraw(address,address,uint256,uint256,bool,uint256, - IStaticATokenLM.SignatureParams).selector) && - // Exclude reward related methods - these are handled below (f.selector != sig:collectAndUpdateRewards(address).selector) && (f.selector != sig:claimRewardsOnBehalf(address,address,address[]).selector) && (f.selector != sig:claimSingleRewardOnBehalf(address,address,address).selector) && (f.selector != sig:claimRewardsToSelf(address[]).selector) && (f.selector != sig:claimRewards(address,address[]).selector) } { - require _AToken == asset(); - require _AToken.UNDERLYING_ASSET_ADDRESS() == _DummyERC20_aTokenUnderlying; env e; @@ -83,10 +67,6 @@ import "../methods/methods_base.spec"; } rule aTokenBalanceIsFixed_for_collectAndUpdateRewards() { - require _AToken == asset(); - require _AToken.UNDERLYING_ASSET_ADDRESS() == _DummyERC20_aTokenUnderlying; - require _AToken != _DummyERC20_rewardToken; - env e; // Limit sender @@ -104,10 +84,6 @@ import "../methods/methods_base.spec"; rule aTokenBalanceIsFixed_for_claimRewardsOnBehalf(address onBehalfOf, address receiver) { - require _AToken == asset(); - require _AToken.UNDERLYING_ASSET_ADDRESS() == _DummyERC20_aTokenUnderlying; - require _AToken != _DummyERC20_rewardToken; - // Create a rewards array address[] _rewards; require _rewards[0] == _DummyERC20_rewardToken; @@ -138,10 +114,6 @@ import "../methods/methods_base.spec"; rule aTokenBalanceIsFixed_for_claimSingleRewardOnBehalf(address onBehalfOf, address receiver) { - require _AToken == asset(); - require _AToken.UNDERLYING_ASSET_ADDRESS() == _DummyERC20_aTokenUnderlying; - require _AToken != _DummyERC20_rewardToken; - env e; // Limit sender @@ -159,7 +131,7 @@ import "../methods/methods_base.spec"; uint256 preBalance = _AToken.balanceOf(e.msg.sender); - claimSingleRewardOnBehalf(e, onBehalfOf, receiver, _DummyERC20_aTokenUnderlying); + claimSingleRewardOnBehalf(e, onBehalfOf, receiver, _DummyERC20_rewardToken); uint256 postBalance = _AToken.balanceOf(e.msg.sender); assert preBalance == postBalance, "aToken balance changed by claimSingleRewardOnBehalf"; @@ -167,10 +139,6 @@ import "../methods/methods_base.spec"; rule aTokenBalanceIsFixed_for_claimRewardsToSelf() { - require _AToken == asset(); - require _AToken.UNDERLYING_ASSET_ADDRESS() == _DummyERC20_aTokenUnderlying; - require _AToken != _DummyERC20_rewardToken; - // Create a rewards array address[] _rewards; require _rewards[0] == _DummyERC20_rewardToken; @@ -193,10 +161,6 @@ import "../methods/methods_base.spec"; rule aTokenBalanceIsFixed_for_claimRewards(address receiver) { - require _AToken == asset(); - require _AToken.UNDERLYING_ASSET_ADDRESS() == _DummyERC20_aTokenUnderlying; - require _AToken != _DummyERC20_rewardToken; - // Create a rewards array address[] _rewards; require _rewards[0] == _DummyERC20_rewardToken; @@ -219,30 +183,18 @@ import "../methods/methods_base.spec"; assert preBalance == postBalance, "aToken balance changed by claimRewards"; } - /// @title Sum of balances=totalSupply() - //fail without Sload require balance <= sumAllBalance(); - //https://vaas-stg.certora.com/output/99352/1ada6d42ed4c4c03bb85b34d1da92be2/?anonymousKey=d27e10c2ac7220d8571d6374b4618b9e24f1a8a0 - // invariant sumAllBalance_eq_totalSupply() - // sumAllBalance() == totalSupply() - - //fail without Sload require balance <= sumAllBalance(); - //https://vaas-stg.certora.com/output/99352/57f284feabc84b6ca392b781de221e9f/?anonymousKey=3554c35d0a060196775a71fdff3a14a6d2177861 - //todo: add presreved blocks and enable the invariant - // invariant inv_balanceOf_leq_totalSupply(address user) - // balanceOf(user) <= totalSupply() - // { - // preserved { - // requireInvariant sumAllBalance_eq_totalSupply(); - // } - // } - /// @title AToken balancerOf(user) <= AToken totalSupply() //timeout on redeem metaWithdraw //error when running with rule_sanity //https://vaas-stg.certora.com/output/99352/509a56a1d46348eea0872b3a57c4d15a/?anonymousKey=3e15ac5a5b01e689eb3f71580e3532d8098e71b5 invariant inv_atoken_balanceOf_leq_totalSupply(address user) _AToken.balanceOf(user) <= _AToken.totalSupply() - filtered { f -> !f.isView && f.selector != sig:redeem(uint256,address,address,bool).selector && !harnessOnlyMethods(f)} + filtered { f -> + !f.isView && + f.selector != sig:redeem(uint256,address,address).selector && + f.selector != sig:redeemATokens(uint256,address,address).selector && + f.selector != sig:emergencyEtherTransfer(address,uint256).selector && + !harnessOnlyMethods(f)} { preserved with (env e){ requireInvariant sumAllATokenScaledBalance_eq_totalSupply(); @@ -254,52 +206,27 @@ import "../methods/methods_base.spec"; //pass, times out with rule_sanity basic invariant inv_atoken_balanceOf_leq_totalSupply_redeem(address user) _AToken.balanceOf(user) <= _AToken.totalSupply() - filtered { f -> f.selector == sig:redeem(uint256,address,address,bool).selector} + filtered { f -> f.selector == sig:redeem(uint256,address,address).selector } { preserved with (env e){ requireInvariant sumAllATokenScaledBalance_eq_totalSupply(); } } - //timeout when running with rule_sanity - //https://vaas-stg.certora.com/output/99352/7840410509f94183bbef864770193ed9/?anonymousKey=b1a13994a4e51f586db837cc284b39c670532f50 - /// @title AToken sum of 2 balancers <= AToken totalSupply() - // invariant inv_atoken_balanceOf_2users_leq_totalSupply(address user1, address user2) - // (_AToken.balanceOf(user1) + _AToken.balanceOf(user2))<= _AToken.totalSupply() - // { - // preserved with (env e1){ - // setup(e1, user1); - // setup(e1, user2); - // } - // preserved redeem(uint256 shares, address receiver, address owner) with (env e2){ - // require user1 != user2; - // require _AToken.balanceOf(currentContract) + _AToken.balanceOf(user1) + _AToken.balanceOf(user2) <= _AToken.totalSupply(); - // } - // preserved redeem(uint256 shares, address receiver, address owner, bool toUnderlying) with (env e3){ - // require user1 != user2; - // requireInvariant sumAllATokenScaledBalance_eq_totalSupply(); - // require _AToken.balanceOf(e3.msg.sender) + _AToken.balanceOf(user1) + _AToken.balanceOf(user2) <= _AToken.totalSupply(); - // require _AToken.balanceOf(currentContract) + _AToken.balanceOf(user1) + _AToken.balanceOf(user2) <= _AToken.totalSupply(); - // } - // preserved withdraw(uint256 assets, address receiver,address owner) with (env e4){ - // require user1 != user2; - // requireInvariant sumAllATokenScaledBalance_eq_totalSupply(); - // require _AToken.balanceOf(e4.msg.sender) + _AToken.balanceOf(user1) + _AToken.balanceOf(user2) <= _AToken.totalSupply(); - // require _AToken.balanceOf(currentContract) + _AToken.balanceOf(user1) + _AToken.balanceOf(user2) <= _AToken.totalSupply(); - // } - - // preserved metaWithdraw(address owner, address recipient,uint256 staticAmount,uint256 dynamicAmount,bool toUnderlying,uint256 deadline,_StaticATokenLM.SignatureParams sigParams) - // with (env e5){ - // require user1 != user2; - // requireInvariant sumAllATokenScaledBalance_eq_totalSupply(); - // require _AToken.balanceOf(e5.msg.sender) + _AToken.balanceOf(user1) + _AToken.balanceOf(user2) <= _AToken.totalSupply(); - // require _AToken.balanceOf(currentContract) + _AToken.balanceOf(user1) + _AToken.balanceOf(user2) <= _AToken.totalSupply(); - // } - - // } + /// @title AToken balancerOf(user) <= AToken totalSupply() + /// @dev case split of inv_atoken_balanceOf_leq_totalSupply + //pass, times out with rule_sanity basic + invariant inv_atoken_balanceOf_leq_totalSupply_redeemAToken(address user) + _AToken.balanceOf(user) <= _AToken.totalSupply() + filtered { f -> f.selector == sig:redeemATokens(uint256,address,address).selector } + { + preserved with (env e){ + requireInvariant sumAllATokenScaledBalance_eq_totalSupply(); + } + } /// @title Sum of AToken scaled balances = AToken scaled totalSupply() - //pass with rule_sanity basic except metaDeposit() + //pass with rule_sanity basic //https://vaas-stg.certora.com/output/99352/4f91637a96d647baab9accb1093f1690/?anonymousKey=53ccda4a9dd8988205d4b614d9989d1e4148533f invariant sumAllATokenScaledBalance_eq_totalSupply() sumAllATokenScaledBalance == to_mathint(_AToken.scaledTotalSupply()) @@ -307,7 +234,7 @@ import "../methods/methods_base.spec"; /// @title AToken scaledBalancerOf(user) <= AToken scaledTotalSupply() - //pass with rule_sanity basic except metaDeposit() + //pass with rule_sanity basic //https://vaas-stg.certora.com/output/99352/6798b502f97a4cd2b05fce30947911c0/?anonymousKey=c5808a8997a75480edbc45153165c8763488cd1e invariant inv_atoken_scaled_balanceOf_leq_totalSupply(address user) _AToken.scaledBalanceOf(user) <= _AToken.scaledTotalSupply() @@ -317,54 +244,3 @@ import "../methods/methods_base.spec"; requireInvariant sumAllATokenScaledBalance_eq_totalSupply(); } } - - // //from unregistered_atoken.spec - // // fail - // //todo: remove - // rule claimable_leq_total_claimable() { - // require _RewardsController.getAvailableRewardsCount(_AToken) == 1; - - // require _RewardsController.getRewardsByAsset(_AToken, 0) == _DummyERC20_rewardToken; - - // env e; - // address user; - - // require currentContract != user; - // require _AToken != user; - // require _RewardsController != user; - // require _DummyERC20_aTokenUnderlying != user; - // require _DummyERC20_rewardToken != user; - // require _SymbolicLendingPoolL1 != user; - // require _TransferStrategy != user; - // require _ScaledBalanceToken != user; - // require _TransferStrategy != user; - - // requireInvariant inv_atoken_balanceOf_leq_totalSupply(currentContract); - // requireInvariant inv_atoken_scaled_balanceOf_leq_totalSupply(currentContract); - - - // require getRewardsIndexOnLastInteraction(user, _DummyERC20_rewardToken) == 0; - // require getUnclaimedRewards(e, user, _DummyERC20_rewardToken) == 0; - - // uint256 total = getTotalClaimableRewards(e, _DummyERC20_rewardToken); - // uint256 claimable = getClaimableRewards(e, user, _DummyERC20_rewardToken); - // assert claimable <= total, "Too much claimable"; - // } - - - - - - //The invariant fails, as suspected, with loop_iter=2. - //The invariant is remove. - //requireInvaraint are replaced with require in rules getClaimableRewards_stable* - // invariant registered_reward_exists_in_controller(address reward) - // (isRegisteredRewardToken(reward) => - // (_RewardsController.getAvailableRewardsCount(_AToken) > 0 - // && _RewardsController.getRewardsByAsset(_AToken, 0) == reward)) - // filtered { f -> f.selector != sig:initialize(address,string,string).selector }//Todo: remove filter and use preserved block when CERT-1706 is fixed - // { - // //preserved initialize(address newAToken,string staticATokenName, string staticATokenSymbol) { - // // require newAToken == _AToken; - // // } - // } diff --git a/certora/stata/specs/staticATokenLM/double_claim.spec b/certora/stata/specs/StataToken/double_claim.spec similarity index 97% rename from certora/stata/specs/staticATokenLM/double_claim.spec rename to certora/stata/specs/StataToken/double_claim.spec index e7c120a5..466fa3ee 100644 --- a/certora/stata/specs/staticATokenLM/double_claim.spec +++ b/certora/stata/specs/StataToken/double_claim.spec @@ -2,7 +2,7 @@ import "../methods/methods_multi_reward.spec"; ///////////////// Properties /////////////////////// -/// @dev Broke the rule into two cases to speed up verification + /// @dev Broke the rule into two cases to speed up verification /** * @title Claiming the same reward twice assuming sufficient rewards diff --git a/certora/stata/specs/erc4626/erc4626.spec b/certora/stata/specs/erc4626/erc4626.spec index 1a99d6ec..b790c3fb 100644 --- a/certora/stata/specs/erc4626/erc4626.spec +++ b/certora/stata/specs/erc4626/erc4626.spec @@ -3,7 +3,25 @@ import "../methods/methods_base.spec"; methods { function balanceOf(address) external returns (uint256) envfree; function totalSupply() external returns (uint256) envfree; + function ReserveConfiguration.getDecimals(DataTypes.ReserveConfigurationMap memory) internal returns (uint256) => limitReserveDecimals(); + function ReserveConfiguration.getSupplyCap(DataTypes.ReserveConfigurationMap memory) internal returns (uint256) => limitReserveSupplyCap(); } + +///////////////// FUNCTIONS /////////////////////// + + function limitReserveDecimals() returns uint256 { + uint256 dec; + require dec >= 6 && dec <= 18; + return dec; + } + + function limitReserveSupplyCap() returns uint256 { + uint256 cap; + require cap <= 10^36; + return cap; + } + + ///////////////// Properties /////////////////////// /**************************** * previewDeposit * @@ -407,8 +425,7 @@ methods { uint256 allowed = allowance(e, owner, e.msg.sender); uint256 balBefore = _AToken.balanceOf(receiver); uint256 shareBalBefore = balanceOf(owner); - require getStaticATokenUnderlying() == _AToken.UNDERLYING_ASSET_ADDRESS(); - uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(getStaticATokenUnderlying()); + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); require e.msg.sender != currentContract; require receiver != currentContract; require owner != currentContract; @@ -453,8 +470,7 @@ methods { mathint allowed = allowance(e, owner, e.msg.sender); uint256 balBefore = balanceOf(owner); - require getStaticATokenUnderlying() == _AToken.UNDERLYING_ASSET_ADDRESS(); - uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(getStaticATokenUnderlying()); + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); require index > RAY(); require e.msg.sender != currentContract; require receiver != currentContract; @@ -467,6 +483,36 @@ methods { assert to_mathint(shares) == balBefore - balAfter,"exactly the specified amount of shares must be burnt"; } + /*** + * rule to check the following for the withdraw function: + * 1. SHOULD check msg.sender can spend owner funds using allowance. + * 2. MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, slippage, the owner not having enough shares, etc). + */ + // STATUS: VERIFIED + // https://vaas-stg.certora.com/output/11775/ff8f93d3158f40a5bb27ba35b15e771d/?anonymousKey=c0e02f130ff0d31552c6741d3b1751bda5177bfd + ///@title allowance and minted share amount check for redeem function + ///@notice This rules checks that the redeem function burns shares upto the allowance for the msg.sender and that the shares burned are exactly equal to the specified share amount + rule redeemATokensCheck(env e){ + uint256 shares; + address receiver; + address owner; + uint256 assets; + mathint allowed = allowance(e, owner, e.msg.sender); + uint256 balBefore = balanceOf(owner); + + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); + require index > RAY(); + require e.msg.sender != currentContract; + require receiver != currentContract; + + assets = redeemATokens(e, shares, receiver, owner); + + uint256 balAfter = balanceOf(owner); + + assert e.msg.sender != owner => allowed >= (balBefore - balAfter),"msg.sender should have allowance for transferring owner's shares"; + assert to_mathint(shares) == balBefore - balAfter,"exactly the specified amount of shares must be burnt"; + } + /***************************** * convertToAssets * *****************************/ @@ -559,11 +605,10 @@ methods { uint256 assets2; storage before = lastStorage; - mathint shares1 = convertToShares(e1, assets1) at before; mathint shares2 = convertToShares(e2, assets1) at before; mathint shares3 = convertToShares(e2, assets2) at before; - mathint combinedShares = convertToShares(e3, assert_uint256(assets1 + assets2)) at before; + mathint combinedShares = convertToShares(e3, require_uint256(assets1 + assets2)) at before; assert shares1 == shares2,"conversion to shares should be independent of env variables including msg.sender"; assert shares1 + shares3 <= combinedShares,"conversion should round down and not up"; @@ -609,84 +654,90 @@ methods { assert !reverted, "Conversion to shares reverted"; } -/************************ - * maxWithdraw * - *************************/ - -// maxWithdraw must not revert -// Nissan remark Aug-2025: this rule doesn't hold due to (a theoretical) possible arithmetical overflow -// in the functions rayDivRoundUp/Down -rule maxWithdrawMustntRevert(address user){ - // This assumption subject to correct configuration of the pool, aToken and statAToken. - // The assumption was ran by and approved by BGD - require rate() > 0; - maxWithdraw@withrevert(user); - assert !lastReverted; -} + /************************ + * maxWithdraw * + *************************/ + + // maxWithdraw must not revert + // Nissan remark Aug-2025: this rule doesn't hold due to (a theoretical) possible arithmetical overflow + // in the functions rayDivRoundUp/Down + rule maxWithdrawMustntRevert(address user){ + // This assumption subject to correct configuration of the pool, aToken and statAToken. + // The assumption was ran by and approved by BGD + require rate() > RAY(); + require rate() <= 100 * RAY(); + maxWithdraw@withrevert(user); + assert !lastReverted; + } -/// @title Ensure `maxWithdraw` conforms to conversion functions -rule maxWithdrawConversionCompliance(address owner) { - env e; - uint256 shares = balanceOf(owner); - uint256 amountConverted = convertToAssets(e, shares); - - assert maxWithdraw(e, owner) <= amountConverted, "Can withdraw more than converted amount"; -} + /// @title Ensure `maxWithdraw` conforms to conversion functions + rule maxWithdrawConversionCompliance(address owner) { + env e; + uint256 shares = balanceOf(owner); + uint256 amountConverted = convertToAssets(e, shares); + + assert maxWithdraw(e, owner) <= amountConverted, "Can withdraw more than converted amount"; + } -/********************** - * maxRedeem * - ***********************/ - -// maxRedeem must not revert -// Nissan remark Aug-2025: this rule doesn't hold due to (a theoretical) possible arithmetical overflow -// in the functions rayDivRoundUp/Down -rule maxRedeemMustntRevert(address user) { - // This assumption subject to correct configuration of the pool, aToken and statAToken. - // The assumption was ran by and approved by BGD - require rate() > 0; - maxRedeem@withrevert(user); - assert !lastReverted; -} + /********************** + * maxRedeem * + ***********************/ + + // maxRedeem must not revert + // Nissan remark Aug-2025: this rule doesn't hold due to (a theoretical) possible arithmetical overflow + // in the functions rayDivRoundUp/Down + rule maxRedeemMustntRevert(address user) { + // This assumption subject to correct configuration of the pool, aToken and statAToken. + // The assumption was ran by and approved by BGD + require rate() > RAY(); + require rate() <= 100 * RAY(); + maxRedeem@withrevert(user); + assert !lastReverted; + } -/// @title Ensure `maxRedeem` is not higher than balance -rule maxRedeemCompliance(address owner) { - uint256 shares = balanceOf(owner); - assert maxRedeem(owner) <= shares, "Can redeem more than available shares)"; -} + /// @title Ensure `maxRedeem` is not higher than balance + rule maxRedeemCompliance(address owner) { + uint256 shares = balanceOf(owner); + assert maxRedeem(owner) <= shares, "Can redeem more than available shares)"; + } -/************************ - * maxDeposit * - *************************/ - -// maxDeposit must not revert -// Nissan remark Aug-2025: this rule doesn't hold due to (a theoretical) possible arithmetical overflow -// in the functions rayDivRoundUp/Down -rule maxDepositMustntRevert(address user) { - env e; - require e.msg.value ==0; - // This assumption subject to correct configuration of the pool, aToken and statAToken. - // The assumption was ran by and approved by BGD - require rate() > 0; - maxDeposit@withrevert(e, user); - assert !lastReverted; -} + /************************ + * maxDeposit * + *************************/ + + // maxDeposit must not revert + // Nissan remark Aug-2025: this rule doesn't hold due to (a theoretical) possible arithmetical overflow + // in the functions rayDivRoundUp/Down + rule maxDepositMustntRevert(address user) { + env e; + require e.msg.value ==0; + // This assumption subject to correct configuration of the pool, aToken and statAToken. + // The assumption was ran by and approved by BGD + require _AToken.scaledTotalSupply() <= 10^36; // arbitrary extremely large sum of tokens. 10^18 of 18 decimals tokens + require rate() > RAY(); + require rate() <= 100 * RAY(); + maxDeposit@withrevert(e, user); + assert !lastReverted; + } -/************************ - * maxMint * - *************************/ - -// maxMint must not revert -// Nissan remark Aug-2025: this rule doesn't hold due to (a theoretical) possible arithmetical overflow -// in the functions rayDivRoundUp/Down -rule maxMintMustntRevert(address user) { - env e; - require e.msg.value ==0; - // This assumption subject to correct configuration of the pool, aToken and statAToken. - // The assumption was ran by and approved by BGD - require rate() > 0; - maxMint@withrevert(e,user); - assert !lastReverted; -} + /************************ + * maxMint * + *************************/ + + // maxMint must not revert + // Nissan remark Aug-2025: this rule doesn't hold due to (a theoretical) possible arithmetical overflow + // in the functions rayDivRoundUp/Down + rule maxMintMustntRevert(address user) { + env e; + require e.msg.value ==0; + // This assumption subject to correct configuration of the pool, aToken and statAToken. + // The assumption was ran by and approved by BGD + require rate() > RAY(); + require rate() <= 100 * RAY(); + require _AToken.scaledTotalSupply() <= 10^36; // arbitrary extremely large sum of tokens. 10^18 of 18 decimals tokens + maxMint@withrevert(e,user); + assert !lastReverted; + } /************************* * totalAssets * @@ -696,7 +747,8 @@ rule maxMintMustntRevert(address user) { rule totalAssetsMustntRevert(address user){ // This assumption subject to correct configuration of the pool, aToken and statAToken. // The assumption was ran by and approved by BGD - require rate() > 0; + require rate() > RAY(); + require rate() <= 100 * RAY(); totalAssets@withrevert(); assert !lastReverted; } diff --git a/certora/stata/specs/erc4626/erc4626DepositSummarization.spec b/certora/stata/specs/erc4626/erc4626DepositSummarization.spec index 2d6f8613..f6675d10 100644 --- a/certora/stata/specs/erc4626/erc4626DepositSummarization.spec +++ b/certora/stata/specs/erc4626/erc4626DepositSummarization.spec @@ -6,7 +6,7 @@ methods{ // static aToken // ------------- function previewDeposit(uint256) external returns(uint256) envfree => NONDET; - function ERC20._mint(address, uint256) internal => NONDET; + function ERC20Upgradeable._mint(address, uint256) internal => NONDET; // rewards controller // ------------------ @@ -34,9 +34,8 @@ methods{ uint256 assets; address receiver; uint256 contractAssetBalBefore = _AToken.balanceOf(currentContract); - uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(getStaticATokenUnderlying()); + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); - require getStaticATokenUnderlying() == _AToken.UNDERLYING_ASSET_ADDRESS();//Without this a different index will be used for conversions in the Atoken contract compared to the one used in StaticAToken require e.msg.sender != currentContract; require index > RAY();//since the index is initiated as RAY and only increases after that. index < RAY gives strange behaviors causing wildly inaccurate amounts being deposited and minted @@ -48,6 +47,55 @@ methods{ assert contractAssetBalAfter - contractAssetBalBefore <= assets + index/RAY(); } + // The function doesn't always deposit exactly the asset amount specified by the user due to rounding. The amount deposited is within 1/2AToken of the + // amount specified by the user. The amount of shares minted would be zero for less than 1AToken worth of assets. + // STATUS: Verified + // https://prover.certora.com/output/11775/0c902c255ba748e99e9f7c2f50395706/?anonymousKey=aeb7ec100e687a415dd05c0eb9a45f823ceaeb25 + ///@title Deposit aTokens amount check for Index > RAY + ///@notice This rule checks that, for index > RAY, the deposit function will deposit upto 1 AToken more than the specified deposit amount + rule depositATokensCheckIndexGRayAssert1(env e){ + uint256 assets; + address receiver; + uint256 contractAssetBalBefore = _AToken.balanceOf(currentContract); + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); + + require e.msg.sender != currentContract; + require index > RAY();//since the index is initiated as RAY and only increases after that. index < RAY gives strange behaviors causing wildly inaccurate amounts being deposited and minted + + uint256 shares = depositATokens(e, assets, receiver); + + uint256 contractAssetBalAfter = _AToken.balanceOf(currentContract); + + // upper bound for deposited assets + assert contractAssetBalAfter - contractAssetBalBefore <= assets + index/RAY(); + } + + // The function doesn't always deposit exactly the asset amount specified by the user due to rounding. The amount deposited is within 1/2AToken of the + // amount specified by the user. The amount of shares minted would be zero for less than 1AToken worth of assets. + // STATUS: Verified + // https://prover.certora.com/output/11775/0c902c255ba748e99e9f7c2f50395706/?anonymousKey=aeb7ec100e687a415dd05c0eb9a45f823ceaeb25 + ///@title Deposit with permit amount check for Index > RAY + ///@notice This rule checks that, for index > RAY, the deposit function will deposit upto 1 AToken more than the specified deposit amount + rule depositWithPermitCheckIndexGRayAssert1(env e){ + uint256 assets; + address receiver; + uint256 deadline; + IERC4626StataToken.SignatureParams signature; + bool depositToAave; + uint256 contractAssetBalBefore = _AToken.balanceOf(currentContract); + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); + + require e.msg.sender != currentContract; + require index > RAY();//since the index is initiated as RAY and only increases after that. index < RAY gives strange behaviors causing wildly inaccurate amounts being deposited and minted + + uint256 shares = depositWithPermit(e, assets, receiver, deadline, signature, depositToAave); + + uint256 contractAssetBalAfter = _AToken.balanceOf(currentContract); + + // upper bound for deposited assets + assert contractAssetBalAfter - contractAssetBalBefore <= assets + index/RAY(); + } + // STATUS: Verified // https://prover.certora.com/output/11775/c149a926d08e44b98b05cec42ff97c0c/?anonymousKey=3a094dbfe8370e0ce3242e97cabd57a6df75a8c8 ///@title Deposit amount check for Index == RAY @@ -56,9 +104,8 @@ methods{ uint256 assets; address receiver; uint256 contractAssetBalBefore = _AToken.balanceOf(currentContract); - uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(getStaticATokenUnderlying()); + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); - require getStaticATokenUnderlying() == _AToken.UNDERLYING_ASSET_ADDRESS();//Without this a different index will be used for conversions in the Atoken contract compared to the one used in StaticAToken require e.msg.sender != currentContract; require index == RAY();//since the index is initiated as RAY and only increases after that. index < RAY gives strange behaviors causing wildly inaccurate amounts being deposited and minted @@ -69,3 +116,48 @@ methods{ // upper bound for deposited assets assert contractAssetBalAfter - contractAssetBalBefore <= assets + index/(2 * RAY()); } + + // STATUS: Verified + // https://prover.certora.com/output/11775/c149a926d08e44b98b05cec42ff97c0c/?anonymousKey=3a094dbfe8370e0ce3242e97cabd57a6df75a8c8 + ///@title Deposit aTokens amount check for Index == RAY + ///@notice This rule checks that, for index == RAY, the deposit function will deposit upto 1/2AToken more than the amount specified deposit amount + rule depositATokensCheckIndexERayAssert1(env e){ + uint256 assets; + address receiver; + uint256 contractAssetBalBefore = _AToken.balanceOf(currentContract); + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); + + require e.msg.sender != currentContract; + require index == RAY();//since the index is initiated as RAY and only increases after that. index < RAY gives strange behaviors causing wildly inaccurate amounts being deposited and minted + + uint256 shares = depositATokens(e, assets, receiver); + + uint256 contractAssetBalAfter = _AToken.balanceOf(currentContract); + + // upper bound for deposited assets + assert contractAssetBalAfter - contractAssetBalBefore <= assets + index/(2 * RAY()); + } + + // STATUS: Verified + // https://prover.certora.com/output/11775/c149a926d08e44b98b05cec42ff97c0c/?anonymousKey=3a094dbfe8370e0ce3242e97cabd57a6df75a8c8 + ///@title Deposit with permit amount check for Index == RAY + ///@notice This rule checks that, for index == RAY, the deposit function will deposit upto 1/2AToken more than the amount specified deposit amount + rule depositWithPermitCheckIndexERayAssert1(env e){ + uint256 assets; + address receiver; + uint256 deadline; + IERC4626StataToken.SignatureParams signature; + bool depositToAave; + uint256 contractAssetBalBefore = _AToken.balanceOf(currentContract); + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); + + require e.msg.sender != currentContract; + require index == RAY();//since the index is initiated as RAY and only increases after that. index < RAY gives strange behaviors causing wildly inaccurate amounts being deposited and minted + + uint256 shares = depositWithPermit(e, assets, receiver, deadline, signature, depositToAave); + + uint256 contractAssetBalAfter = _AToken.balanceOf(currentContract); + + // upper bound for deposited assets + assert contractAssetBalAfter - contractAssetBalBefore <= assets + index/(2 * RAY()); + } diff --git a/certora/stata/specs/erc4626/erc4626Extended.spec b/certora/stata/specs/erc4626/erc4626Extended.spec index 581c9d9f..c84370b9 100644 --- a/certora/stata/specs/erc4626/erc4626Extended.spec +++ b/certora/stata/specs/erc4626/erc4626Extended.spec @@ -135,9 +135,25 @@ import "../methods/methods_base.spec"; assert assetsSum < assets1 + assets2 + 2, "Redeemed sum far larger than parts"; } + /// @title Redeeming aTokens sum of assets is nearly equal to sum of redeeming + rule redeemATokensSum(uint256 shares1, uint256 shares2) { + env e; + address owner = e.msg.sender; // Handy alias + + uint256 assets1 = redeemATokens(e, shares1, owner, owner); + uint256 assets2 = redeemATokens(e, shares2, owner, owner); + mathint assetsSum = redeemATokens(e, require_uint256(shares1 + shares2), owner, owner); + + assert assetsSum >= assets1 + assets2, "Redeemed sum smaller than parts"; + + /* See `accountsJoiningSplittingIsLimited` rule for why the following assertion + * is correct. + */ + assert assetsSum < assets1 + assets2 + 2, "Redeemed sum far larger than parts"; + } + /* The commented out rule below (withdrawSum) timed out after 6994 seconds (see link below). * However, we can deduce worse bounds from previous rules, here is the proof. - * TODO: should we try for better bounds? * Let w = withdraw(assets), p = previewWithdraw(assets), s = convertToShares(assets), * then: * p - 1 <= w <= p -- by previewWithdrawNearlyWithdraw @@ -203,35 +219,36 @@ import "../methods/methods_base.spec"; assert previewAssets == assets, "previewMint is unequal to mint"; } -/*************************** - * maxDeposit * - ***************************/ -// The EIP4626 spec requires that the previewDeposit function must not account for maxDeposit limit or the allowance of asset tokens. -// Since maxDeposit is a constant, it cannot have any impact on the previewDeposit value. -// STATUS: Verified for all f except metaDeposit which has a reachability issue -// https://vaas-stg.certora.com/output/11775/044c54bdf1c0414898e88d9b03dda5a5/?anonymousKey=aaa9c0c1c413cd1fd3cbb9fdfdcaa20a098274c5 - -///@title maxDeposit is constant -///@notice This rule verifies that maxDeposit returns a constant value and therefore it cannot have any impact on the previewDeposit value. - -// Remark (by Nissan on Aug-2025): Currently this rule fails for several functions, like mint, deposit, redeem and more: -// https://prover.certora.com/output/66114/571e3b8d94884f9594d59efacba86999/?anonymousKey=4ffc80fdbcce585966830513ec609dfdcbe37807 -// It looks likr it is supposed to fail since maxDeposit depends on the scaled-total-suuply of the aToken which may be changed -// by the above functions. -rule maxDepositConstant(method f) - filtered { - f -> - f.contract == currentContract && - f.selector != sig:metaDeposit(address,address,uint256,uint16,bool,uint256,IStaticATokenLM.PermitParams calldata, IStaticATokenLM.SignatureParams calldata).selector - } - { - env e; - address receiver; - uint256 maxDep1 = maxDeposit(e, receiver); - calldataarg args; - f(e, args); - uint256 maxDep2 = maxDeposit(e, receiver); - - assert maxDep1 == maxDep2,"maxDeposit should not change"; + /*************************** + * maxDeposit * + ***************************/ + // The EIP4626 spec requires that the previewDeposit function must not account for maxDeposit limit or the allowance of asset tokens. + // Since maxDeposit is a constant, it cannot have any impact on the previewDeposit value. + // STATUS: Verified for all f except metaDeposit which has a reachability issue + // https://vaas-stg.certora.com/output/11775/044c54bdf1c0414898e88d9b03dda5a5/?anonymousKey=aaa9c0c1c413cd1fd3cbb9fdfdcaa20a098274c5 + + ///@title maxDeposit is constant + ///@notice This rule verifies that maxDeposit returns a constant value and therefore it cannot have any impact on the previewDeposit value. + rule maxDepositConstant(method f) + filtered { + f -> + f.contract == currentContract && + !f.isView && + !harnessOnlyMethods(f) && + f.selector != sig:emergencyEtherTransfer(address,uint256).selector && + f.selector != sig:deposit(uint256,address).selector && + f.selector != sig:depositWithPermit(uint256,address,uint256,IERC4626StataToken.SignatureParams,bool).selector && + f.selector != sig:withdraw(uint256,address,address).selector && + f.selector != sig:redeem(uint256,address,address).selector && + f.selector != sig:mint(uint256,address).selector } - + { + env e; + address receiver; + uint256 maxDep1 = maxDeposit(e, receiver); + calldataarg args; + f(e, args); + uint256 maxDep2 = maxDeposit(e, receiver); + + assert maxDep1 == maxDep2,"maxDeposit should not change"; + } diff --git a/certora/stata/specs/erc4626/erc4626MintDepositSummarization.spec b/certora/stata/specs/erc4626/erc4626MintDepositSummarization.spec index 05409d5a..16ea48ae 100644 --- a/certora/stata/specs/erc4626/erc4626MintDepositSummarization.spec +++ b/certora/stata/specs/erc4626/erc4626MintDepositSummarization.spec @@ -6,10 +6,9 @@ using ATokenInstance as _AToken; /////////////////// Methods //////////////////////// methods{ - // static aToken harness - // --------------------- - function getStaticATokenUnderlying() external returns (address) envfree; - + // static aToken + // ------------- + function asset() external returns (address) envfree; // erc20 // ----- function _.transferFrom(address,address,uint256) external => NONDET; @@ -20,12 +19,9 @@ methods{ // aToken // ------ function _AToken.UNDERLYING_ASSET_ADDRESS() external returns (address) envfree; + function RAY() external returns (uint256) envfree; } -///////////////// DEFINITIONS ////////////////////// - - definition RAY() returns uint256 = 10^27; - ///////////////// Properties /////////////////////// /******************** @@ -44,9 +40,8 @@ methods{ rule depositCheckIndexGRayAssert2(env e){ uint256 assets; address receiver; - uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(getStaticATokenUnderlying()); + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); - require getStaticATokenUnderlying() == _AToken.UNDERLYING_ASSET_ADDRESS();//Without this a different index will be used for conversions in the Atoken contract compared to the one used in StaticAToken require e.msg.sender != currentContract; require index > RAY();//since the index is initiated as RAY and only increases after that. index < RAY gives strange behaviors causing wildly inaccurate amounts being deposited and minted @@ -55,17 +50,53 @@ methods{ assert assets * RAY() >= to_mathint(index) => shares != 0; //if the assets amount is worth at least 1 Atoken then receiver will get atleast 1 share } + + // STATUS: Verified + // https://vaas-stg.certora.com/output/11775/b10cd30ab6fb400baeff6b61c07bb375/?anonymousKey=ccba22e832b7549efea9f0d4b1288da2c1377ccb + ///@title DepositATokens function mint amount check for index > RAY + ///@notice This rule checks that, for index > RAY, the deposit function will mint atleast 1 share as long as the specified deposit amount is worth atleast 1 AToken + rule depositATokensCheckIndexGRayAssert2(env e){ + uint256 assets; + address receiver; + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); + + require e.msg.sender != currentContract; + require index > RAY();//since the index is initiated as RAY and only increases after that. index < RAY gives strange behaviors causing wildly inaccurate amounts being deposited and minted + + uint256 shares = depositATokens(e, assets, receiver); + + assert assets * RAY() >= to_mathint(index) => shares != 0; //if the assets amount is worth at least 1 Atoken then receiver will get atleast 1 share + } + + // STATUS: Verified + // https://vaas-stg.certora.com/output/11775/b10cd30ab6fb400baeff6b61c07bb375/?anonymousKey=ccba22e832b7549efea9f0d4b1288da2c1377ccb + ///@title Deposit with permit function mint amount check for index > RAY + ///@notice This rule checks that, for index > RAY, the deposit function will mint atleast 1 share as long as the specified deposit amount is worth atleast 1 AToken + rule depositWithPermitCheckIndexGRayAssert2(env e){ + uint256 assets; + address receiver; + uint256 deadline; + IERC4626StataToken.SignatureParams signature; + bool depositToAave; + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); + + require e.msg.sender != currentContract; + require index > RAY();//since the index is initiated as RAY and only increases after that. index < RAY gives strange behaviors causing wildly inaccurate amounts being deposited and minted + + uint256 shares = depositWithPermit(e, assets, receiver, deadline, signature, depositToAave); + + assert assets * RAY() >= to_mathint(index) => shares != 0; //if the assets amount is worth at least 1 Atoken then receiver will get atleast 1 share + } + // STATUS: Verified // https://vaas-stg.certora.com/output/11775/2e162e12cafb49e688a7959a1d7dd4ca/?anonymousKey=d23ad1899e6bfa4e14fbf79799d008fa003dd633 ///@title Deposit function mint amount check for index == RAY ///@notice This rule checks that, for index == RAY, the deposit function will mint atleast 1 share as long as the specified deposit amount is worth atleast 1 AToken - rule depositCheckIndexERayAssert2(env e){ uint256 assets; address receiver; - uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(getStaticATokenUnderlying()); + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); - require getStaticATokenUnderlying() == _AToken.UNDERLYING_ASSET_ADDRESS();//Without this a different index will be used for conversions in the Atoken contract compared to the one used in StaticAToken require e.msg.sender != currentContract; require index == RAY();//since the index is initiated as RAY and only increases after that. index < RAY gives strange behaviors causing wildly inaccurate amounts being deposited and minted @@ -74,6 +105,42 @@ methods{ assert assets * RAY() >= to_mathint(index) => shares != 0; //if the assets amount is worth at least 1 Atoken then receiver will get atleast 1 share } + // STATUS: Verified + // https://vaas-stg.certora.com/output/11775/2e162e12cafb49e688a7959a1d7dd4ca/?anonymousKey=d23ad1899e6bfa4e14fbf79799d008fa003dd633 + ///@title Deposit function mint amount check for index == RAY + ///@notice This rule checks that, for index == RAY, the deposit function will mint atleast 1 share as long as the specified deposit amount is worth atleast 1 AToken + rule depositATokensCheckIndexERayAssert2(env e){ + uint256 assets; + address receiver; + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); + + require e.msg.sender != currentContract; + require index == RAY();//since the index is initiated as RAY and only increases after that. index < RAY gives strange behaviors causing wildly inaccurate amounts being deposited and minted + + uint256 shares = depositATokens(e, assets, receiver); + + assert assets * RAY() >= to_mathint(index) => shares != 0; //if the assets amount is worth at least 1 Atoken then receiver will get atleast 1 share + } + + // STATUS: Verified + // https://vaas-stg.certora.com/output/11775/2e162e12cafb49e688a7959a1d7dd4ca/?anonymousKey=d23ad1899e6bfa4e14fbf79799d008fa003dd633 + ///@title Deposit function mint amount check for index == RAY + ///@notice This rule checks that, for index == RAY, the deposit function will mint atleast 1 share as long as the specified deposit amount is worth atleast 1 AToken + rule depositWithPermitCheckIndexERayAssert2(env e){ + uint256 assets; + address receiver; + uint256 deadline; + IERC4626StataToken.SignatureParams signature; + bool depositToAave; + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); + + require e.msg.sender != currentContract; + require index == RAY();//since the index is initiated as RAY and only increases after that. index < RAY gives strange behaviors causing wildly inaccurate amounts being deposited and minted + + uint256 shares = depositWithPermit(e, assets, receiver, deadline, signature, depositToAave); + + assert assets * RAY() >= to_mathint(index) => shares != 0; //if the assets amount is worth at least 1 Atoken then receiver will get atleast 1 share + } /***************** * mint * ******************/ @@ -93,9 +160,8 @@ methods{ uint256 shares; address receiver; uint256 assets; - require getStaticATokenUnderlying() == _AToken.UNDERLYING_ASSET_ADDRESS(); require e.msg.sender != currentContract; - uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(getStaticATokenUnderlying()); + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); uint256 receiverBalBefore = balanceOf(e, receiver); require receiverBalBefore + shares <= max_uint256;//avoiding overflow @@ -116,9 +182,8 @@ methods{ uint256 shares; address receiver; uint256 assets; - require getStaticATokenUnderlying() == _AToken.UNDERLYING_ASSET_ADDRESS(); require e.msg.sender != currentContract; - uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(getStaticATokenUnderlying()); + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); uint256 receiverBalBefore = balanceOf(e, receiver); require receiverBalBefore + shares <= max_uint256;//avoiding overflow @@ -139,9 +204,8 @@ methods{ uint256 shares; address receiver; uint256 assets; - require getStaticATokenUnderlying() == _AToken.UNDERLYING_ASSET_ADDRESS(); require e.msg.sender != currentContract; - uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(getStaticATokenUnderlying()); + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); uint256 receiverBalBefore = balanceOf(e, receiver); require receiverBalBefore + shares <= max_uint256;//avoiding overflow diff --git a/certora/stata/specs/methods/CVLMath.spec b/certora/stata/specs/methods/CVLMath.spec new file mode 100644 index 00000000..ba475be8 --- /dev/null +++ b/certora/stata/specs/methods/CVLMath.spec @@ -0,0 +1,236 @@ +/****************************************** +----------- CVL Math Library -------------- +*******************************************/ + + ///////////////// DEFINITIONS ////////////////////// + + // A restriction on the value of w = x * y / z + // The ratio between x (or y) and z is a rational number a/b or b/a. + // Important : do not set a = 0 or b = 0. + // Note: constRatio(x,y,z,a,b,w) <=> constRatio(x,y,z,b,a,w) + definition constRatio(uint256 x, uint256 y, uint256 z, + uint256 a, uint256 b, uint256 w) + returns bool = + ( a * x == b * z && to_mathint(w) == (b * y) / a ) || + ( b * x == a * z && to_mathint(w) == (a * y) / b ) || + ( a * y == b * z && to_mathint(w) == (b * x) / a ) || + ( b * y == a * z && to_mathint(w) == (a * x) / b ); + + // A restriction on the value of w = x * y / z + // The division quotient between x (or y) and z is an integer q or 1/q. + // Important : do not set q=0 + definition constQuotient(uint256 x, uint256 y, uint256 z, + uint256 q, uint256 w) + + returns bool = + ( to_mathint(x) == q * z && to_mathint(w) == q * y ) || + ( q * x == to_mathint(z) && to_mathint(w) == y / q ) || + ( to_mathint(y) == q * z && to_mathint(w) == q * x ) || + ( q * y == to_mathint(z) && to_mathint(w) == x / q ); + + /// Equivalent to the one above, but with implication + definition constQuotientImply(uint256 x, uint256 y, uint256 z, + uint256 q, uint256 w) + + returns bool = + ( to_mathint(x) == q * z => to_mathint(w) == q * y ) && + ( q * x == to_mathint(z) => to_mathint(w) == y / q ) && + ( to_mathint(y) == q * z => to_mathint(w) == q * x ) && + ( q * y == to_mathint(z) => to_mathint(w) == x / q ); + + definition ONE18() returns uint256 = 1000000000000000000; + // definition RAY() returns uint256 = 10^27; + + definition _monotonicallyIncreasing(uint256 x, uint256 y, uint256 fx, uint256 fy) returns bool = + (x > y => fx >= fy); + + definition _monotonicallyDecreasing(uint256 x, uint256 y, uint256 fx, uint256 fy) returns bool = + (x > y => fx <= fy); + + definition abs(mathint x) returns mathint = + x >= 0 ? x : 0 - x; + + definition min(mathint x, mathint y) returns mathint = + x > y ? y : x; + + definition max(mathint x, mathint y) returns mathint = + x > y ? x : y; + + /// Returns whether y is equal to x up to error bound of 'err' (18 decs). + /// e.g. 10% relative error => err = 1e17 + definition relativeErrorBound(mathint x, mathint y, mathint err) returns bool = + (x != 0 + ? abs(x - y) * ONE18() <= abs(x) * err + : abs(y) <= err); + + /// Axiom for a weighted average of the form WA = (x * y) / (y + z) + /// This is valid as long as z + y > 0 => make certain of that condition in the use of this definition. + definition weightedAverage(mathint x, mathint y, mathint z, mathint WA) returns bool = + ((x > 0 && y > 0) => (WA >= 0 && WA <= x)) + && + ((x < 0 && y > 0) => (WA <= 0 && WA >= x)) + && + ((x > 0 && y < 0) => (WA <= 0 && WA - x <= 0)) + && + ((x < 0 && y < 0) => (WA >= 0 && WA + x <= 0)) + && + ((x == 0 || y == 0) => (WA == 0)); + + + + ////////////////// FUNCTIONS ////////////////////// + + function mulDivDownAbstract(uint256 x, uint256 y, uint256 z) returns uint256 { + require z !=0; + uint256 xy = require_uint256(x * y); + uint256 res; + mathint rem; + require z * res + rem == to_mathint(xy); + require rem < to_mathint(z); + return res; + } + + function mulDivDownAbstractPlus(uint256 x, uint256 y, uint256 z) returns uint256 { + uint256 res; + require z != 0; + uint256 xy = require_uint256(x * y); + uint256 fz = require_uint256(res * z); + + require xy >= fz; + require fz + z > to_mathint(xy); + return res; + } + + function mulDivUpAbstractPlus(uint256 x, uint256 y, uint256 z) returns uint256 { + uint256 res; + require z != 0; + uint256 xy = require_uint256(x * y); + uint256 fz = require_uint256(res * z); + require xy >= fz; + require fz + z > to_mathint(xy); + + if(xy == fz) { + return res; + } + return require_uint256(res + 1); + } + + function mulDownWad(uint256 x, uint256 y) returns uint256 { + return mulDivDownAbstractPlus(x, y, ONE18()); + } + + function mulUpWad(uint256 x, uint256 y) returns uint256 { + return mulDivUpAbstractPlus(x, y, ONE18()); + } + + function divDownWad(uint256 x, uint256 y) returns uint256 { + return mulDivDownAbstractPlus(x, ONE18(), y); + } + + function divUpWad(uint256 x, uint256 y) returns uint256 { + return mulDivUpAbstractPlus(x, ONE18(), y); + } + + function discreteQuotientMulDiv(uint256 x, uint256 y, uint256 z) returns uint256 + { + uint256 res; + require z != 0 && noOverFlowMul(x, y); + // Discrete quotients: + require( + ((x ==0 || y ==0) && res == 0) || + (x == z && res == y) || + (y == z && res == x) || + constQuotient(x, y, z, 2, res) || // Division quotient is 1/2 or 2 + constQuotient(x, y, z, 5, res) || // Division quotient is 1/5 or 5 + constQuotient(x, y, z, 100, res) // Division quotient is 1/100 or 100 + ); + return res; + } + + function discreteRatioMulDiv(uint256 x, uint256 y, uint256 z) returns uint256 + { + uint256 res; + require z != 0 && noOverFlowMul(x, y); + // Discrete ratios: + require( + ((x ==0 || y ==0) && res == 0) || + (x == z && res == y) || + (y == z && res == x) || + constRatio(x, y, z, 2, 1, res) || // f = 2*x or f = x/2 (same for y) + constRatio(x, y, z, 5, 1, res) || // f = 5*x or f = x/5 (same for y) + constRatio(x, y, z, 2, 3, res) || // f = 2*x/3 or f = 3*x/2 (same for y) + constRatio(x, y, z, 2, 7, res) // f = 2*x/7 or f = 7*x/2 (same for y) + ); + return res; + } + + function noOverFlowMul(uint256 x, uint256 y) returns bool + { + return x * y <= max_uint; + } + + /// @doc Ghost power function that incorporates mathematical pure x^y axioms. + /// @warning Some of these axioms might be false, depending on the Solidity implementation + /// The user must bear in mind that equality-like axioms can be violated because of rounding errors. + ghost _ghostPow(uint256, uint256) returns uint256 { + /// x^0 = 1 + axiom forall uint256 x. _ghostPow(x, 0) == ONE18(); + /// 0^x = 1 + axiom forall uint256 y. _ghostPow(0, y) == 0; + /// x^1 = x + axiom forall uint256 x. _ghostPow(x, ONE18()) == x; + /// 1^y = 1 + axiom forall uint256 y. _ghostPow(ONE18(), y) == ONE18(); + + /// I. x > 1 && y1 > y2 => x^y1 > x^y2 + /// II. x < 1 && y1 > y2 => x^y1 < x^y2 + axiom forall uint256 x. forall uint256 y1. forall uint256 y2. + x >= ONE18() && y1 > y2 => _ghostPow(x, y1) >= _ghostPow(x, y2); + axiom forall uint256 x. forall uint256 y1. forall uint256 y2. + x < ONE18() && y1 > y2 => (_ghostPow(x, y1) <= _ghostPow(x, y2) && _ghostPow(x,y2) <= ONE18()); + axiom forall uint256 x. forall uint256 y. + x < ONE18() && y > ONE18() => (_ghostPow(x, y) <= x); + axiom forall uint256 x. forall uint256 y. + x < ONE18() && y <= ONE18() => (_ghostPow(x, y) >= x); + axiom forall uint256 x. forall uint256 y. + x >= ONE18() && y > ONE18() => (_ghostPow(x, y) >= x); + axiom forall uint256 x. forall uint256 y. + x >= ONE18() && y <= ONE18() => (_ghostPow(x, y) <= x); + /// x1 > x2 && y > 0 => x1^y > x2^y + axiom forall uint256 x1. forall uint256 x2. forall uint256 y. + x1 > x2 => _ghostPow(x1, y) >= _ghostPow(x2, y); + + /* Additional axioms - potentially unsafe + /// x^y * x^(1-y) == x -> 0.01% relative error + axiom forall uint256 x. forall uint256 y. forall uint256 z. + (0 <= y && y <= ONE18() && z + y == to_mathint(ONE18())) => + relativeErrorBound(_ghostPow(x, y) * _ghostPow(x, z), x * ONE18(), ONE18() / 10000); + + /// (x^y)^(1/y) == x -> 1% relative error + axiom forall uint256 x. forall uint256 y. forall uint256 z. + (0 <= y && y <= ONE18() && z * y == ONE18()*ONE18() ) => + relativeErrorBound(_ghostPow(_ghostPow(x, y), z), x, ONE18() / 100); + */ + } + + function CVLPow(uint256 x, uint256 y) returns uint256 { + if (y == 0) {return ONE18();} + if (x == 0) {return 0;} + return _ghostPow(x, y); + } + + function CVLSqrt(uint256 x) returns uint256 { + mathint SQRT; + require SQRT*SQRT <= to_mathint(x) && (SQRT + 1)*(SQRT + 1) > to_mathint(x); + return require_uint256(SQRT); + } + + // For Aave + function rayMulCVLPrecise(uint x, uint y) returns uint256 { + return require_uint256((x*y + RAY()/2) / RAY()); + } + + function rayDivCVLPrecise(uint x, uint y) returns uint256 { + require y != 0; + return require_uint256((x*RAY() + y/2)/y); + } \ No newline at end of file diff --git a/certora/stata/specs/methods/methods_base.spec b/certora/stata/specs/methods/methods_base.spec index 1678f706..d34f85d1 100644 --- a/certora/stata/specs/methods/methods_base.spec +++ b/certora/stata/specs/methods/methods_base.spec @@ -1,6 +1,7 @@ import "erc20.spec"; +import "CVLMath.spec"; -using StaticATokenLMHarness as _StaticATokenLM; +using StataTokenV2Harness as _StaticATokenLM; using SymbolicLendingPool as _SymbolicLendingPool; using RewardsControllerHarness as _RewardsController; using TransferStrategyHarness as _TransferStrategy; @@ -10,93 +11,98 @@ using DummyERC20_rewardToken as _DummyERC20_rewardToken; /////////////////// Methods //////////////////////// -methods { - // static aToken - // ------------- - function asset() external returns (address) envfree; - function totalAssets() external returns (uint256) envfree; - function maxWithdraw(address owner) external returns (uint256) envfree; - function maxRedeem(address owner) external returns (uint256) envfree; - function previewWithdraw(uint256) external returns (uint256) envfree; - function previewRedeem(uint256) external returns (uint256) envfree; - function maxDeposit(address) external returns (uint256); - function previewMint(uint256) external returns (uint256) envfree; - function maxMint(address) external returns (uint256); - function rate() external returns (uint256) envfree; - function getUnclaimedRewards(address, address) external returns (uint256) envfree; - function rewardTokens() external returns (address[]) envfree; - function isRegisteredRewardToken(address) external returns (bool) envfree; + methods { + // static aToken + // ------------- + function asset() external returns (address) envfree; + function totalAssets() external returns (uint256) envfree; + function maxWithdraw(address owner) external returns (uint256) envfree; + function maxRedeem(address owner) external returns (uint256) envfree; + function previewWithdraw(uint256) external returns (uint256) envfree; + function previewRedeem(uint256) external returns (uint256) envfree; + function maxDeposit(address) external returns (uint256); + function previewMint(uint256) external returns (uint256) envfree; + function maxMint(address) external returns (uint256); + function rate() external returns (uint256) envfree; + function getUnclaimedRewards(address, address) external returns (uint256) envfree; + function rewardTokens() external returns (address[]) envfree; + function isRegisteredRewardToken(address) external returns (bool) envfree; + + // static aToken harness + // --------------------- + function getRewardTokensLength() external returns (uint256) envfree; + function getRewardToken(uint256) external returns (address) envfree; + + // erc20 + // ----- + function _.transferFrom(address,address,uint256) external => DISPATCHER(true); + + // pool + // ---- + function _SymbolicLendingPool.getReserveNormalizedIncome(address) external returns (uint256) envfree; + function _SymbolicLendingPool.getReserveData(address) external returns (DataTypes.ReserveDataLegacy); + function _SymbolicLendingPool.getReserveDataExtended(address) external returns (DataTypes.ReserveData); - // static aToken harness - // --------------------- - function getStaticATokenUnderlying() external returns (address) envfree; - function getRewardsIndexOnLastInteraction(address, address) external returns (uint128) envfree; - function getRewardTokensLength() external returns (uint256) envfree; - function getRewardToken(uint256) external returns (address) envfree; - - // erc20 - // ----- - function _.transferFrom(address,address,uint256) external => DISPATCHER(true); - - // pool - // ---- - function _SymbolicLendingPool.getReserveNormalizedIncome(address) external returns (uint256) envfree; - function _SymbolicLendingPool.getReserveData(address) external returns (DataTypes.ReserveData) => CONSTANT; - - // rewards controller - // ------------------ - // In RewardsDistributor.sol called by RewardsController.sol - function _.getAssetIndex(address, address) external=> DISPATCHER(true); - // In ScaledBalanceTokenBase.sol called by getAssetIndex - function _.scaledTotalSupply() external => DISPATCHER(true); - // Called by RewardsController._transferRewards() - // Defined in TransferStrategyHarness as simple transfer() - function _.performTransfer(address,address,uint256) external => DISPATCHER(true); - - // harness methods of the rewards controller - function _RewardsController.getRewardsIndex(address,address) external returns (uint256) envfree; - function _RewardsController.getAvailableRewardsCount(address) external returns (uint128) envfree; - function _RewardsController.getRewardsByAsset(address, uint128) external returns (address) envfree; - function _RewardsController.getAssetListLength() external returns (uint256) envfree; - function _RewardsController.getAssetByIndex(uint256) external returns (address) envfree; - function _RewardsController.getDistributionEnd(address, address) external returns (uint256) envfree; - function _RewardsController.getUserAccruedRewards(address, address) external returns (uint256) envfree; - function _RewardsController.getUserAccruedReward(address, address, address) external returns (uint256) envfree; - function _RewardsController.getAssetDecimals(address) external returns (uint8) envfree; - function _RewardsController.getRewardsData(address,address) external returns (uint256,uint256,uint256,uint256) envfree; - function _RewardsController.getUserAssetIndex(address,address, address) external returns (uint256) envfree; - - // underlying token - // ---------------- - function _DummyERC20_aTokenUnderlying.balanceOf(address) external returns(uint256) envfree; - - // aToken - // ------ - function _AToken.balanceOf(address) external returns (uint256) envfree; - function _AToken.totalSupply() external returns (uint256) envfree; - function _AToken.allowance(address, address) external returns (uint256) envfree; - function _AToken.UNDERLYING_ASSET_ADDRESS() external returns (address) envfree; - function _AToken.scaledBalanceOf(address) external returns (uint256) envfree; - function _AToken.scaledTotalSupply() external returns (uint256) envfree; - - // called in aToken - function _.finalizeTransfer(address, address, address, uint256, uint256, uint256) external => NONDET; - // Called by rewardscontroller.sol - // Defined in scaledbalancetokenbase.sol - function _.getScaledUserBalanceAndSupply(address) external => DISPATCHER(true); - - // reward token - // ------------ - function _DummyERC20_rewardToken.balanceOf(address) external returns (uint256) envfree; - function _DummyERC20_rewardToken.totalSupply() external returns (uint256) envfree; - - function _.UNDERLYING_ASSET_ADDRESS() external => CONSTANT UNRESOLVED; -} + // rewards controller + // ------------------ + // In RewardsDistributor.sol called by RewardsController.sol + function _.getAssetIndex(address, address) external=> DISPATCHER(true); + // In ScaledBalanceTokenBase.sol called by getAssetIndex + function _.scaledTotalSupply() external => DISPATCHER(true); + // Called by RewardsController._transferRewards() + // Defined in TransferStrategyHarness as simple transfer() + function _.performTransfer(address,address,uint256) external => DISPATCHER(true); + + // harness methods of the rewards controller + function _RewardsController.getRewardsIndex(address,address) external returns (uint256) envfree; + function _RewardsController.getAvailableRewardsCount(address) external returns (uint128) envfree; + function _RewardsController.getRewardsByAsset(address, uint128) external returns (address) envfree; + function _RewardsController.getAssetListLength() external returns (uint256) envfree; + function _RewardsController.getAssetByIndex(uint256) external returns (address) envfree; + function _RewardsController.getDistributionEnd(address, address) external returns (uint256) envfree; + function _RewardsController.getUserAccruedRewards(address, address) external returns (uint256) envfree; + function _RewardsController.getUserAccruedReward(address, address, address) external returns (uint256) envfree; + function _RewardsController.getAssetDecimals(address) external returns (uint8) envfree; + function _RewardsController.getRewardsData(address,address) external returns (uint256,uint256,uint256,uint256) envfree; + function _RewardsController.getUserAssetIndex(address,address, address) external returns (uint256) envfree; + + // underlying token + // ---------------- + function _DummyERC20_aTokenUnderlying.balanceOf(address) external returns(uint256) envfree; + + function _.permit(address,address,uint256,uint256,uint8,bytes32,bytes32) external => NONDET; + + // aToken + // ------ + function _AToken.balanceOf(address) external returns (uint256) envfree; + function _AToken.totalSupply() external returns (uint256) envfree; + function _AToken.allowance(address, address) external returns (uint256) envfree; + function _AToken.UNDERLYING_ASSET_ADDRESS() external returns (address) envfree; + function _AToken.scaledBalanceOf(address) external returns (uint256) envfree; + function _AToken.scaledTotalSupply() external returns (uint256) envfree; + + // called in aToken + function _.finalizeTransfer(address, address, address, uint256, uint256, uint256) external => NONDET; + // Called by rewardscontroller.sol + // Defined in scaledbalancetokenbase.sol + function _.getScaledUserBalanceAndSupply(address) external => DISPATCHER(true); + + // reward token + // ------------ + function _DummyERC20_rewardToken.balanceOf(address) external returns (uint256) envfree; + function _DummyERC20_rewardToken.totalSupply() external returns (uint256) envfree; + + function _.UNDERLYING_ASSET_ADDRESS() external => CONSTANT UNRESOLVED; + + function RAY() external returns (uint256) envfree; + + // math lib + // ------------ + function _.mulDiv(uint256 x, uint256 y, uint256 denominator, Math.Rounding rounding) internal => mulDivCVL(x, y, denominator, rounding) expect (uint256); + } ///////////////// DEFINITIONS ////////////////////// - definition RAY() returns uint256 = 10^27; - /// @notice Claim rewards methods definition claimFunctions(method f) returns bool = (f.selector == sig:claimRewardsToSelf(address[]).selector || @@ -112,13 +118,29 @@ methods { f.selector == sig:claimDoubleRewardOnBehalfSame(address, address, address).selector); definition harnessMethodsMinusHarnessClaimMethods(method f) returns bool = - (f.selector == sig:getStaticATokenUnderlying().selector || - f.selector == sig:getRewardTokensLength().selector || + (f.selector == sig:getRewardTokensLength().selector || f.selector == sig:getRewardToken(uint256).selector || - f.selector == sig:getRewardsIndexOnLastInteraction(address, address).selector || - f.selector == sig:getUserRewardsData(address, address).selector || f.selector == sig:_mintWrapper(address, uint256).selector); +////////////////// Hooks ////////////////////// + + /// @title Reward hook + /// @notice allows a single reward + hook Sload address reward (slot 0x4fad66563f105be0bff96185c9058c4934b504d3ba15ca31e86294f0b01fd200).(offset 32)[INDEX uint256 i] /*_rewardTokens*/ { + require reward == _DummyERC20_rewardToken; + } + + /// @title aToken hook + hook Sload address aToken (slot 0x55029d3f54709e547ed74b2fc842d93107ab1490ab7555dd9dd0bf6451101900).(offset 0) /*aToken*/ { + require aToken == _AToken; + } + + /// @title underlying hook + hook Sload address underlying (slot 0x0773e532dfede91f04b12a73d3d2acd361424f41f76b4fb79f090161e36b4e00).(offset 0) /*_asset*/ { + require underlying == _DummyERC20_aTokenUnderlying; + } + + ////////////////// FUNCTIONS ////////////////////// /** @@ -129,7 +151,7 @@ methods { function single_RewardToken_setup() { require getRewardTokensLength() == 1; require getRewardToken(0) == _DummyERC20_rewardToken; - } + } /** * @title Single reward setup in RewardsController @@ -158,3 +180,15 @@ methods { require _TransferStrategy != user; require _TransferStrategy != user; } + + /** + * @title MulDiv summarization in CVL. + * @dev Rounds up or down depends on user specification + */ + function mulDivCVL(uint256 x, uint256 y, uint256 denominator, Math.Rounding rounding) returns uint256 { + if (rounding == Math.Rounding.Floor) { + return mulDivDownAbstractPlus(x, y, denominator); + } else { + return mulDivUpAbstractPlus(x, y, denominator); + } + } diff --git a/certora/stata/specs/methods/methods_multi_reward.spec b/certora/stata/specs/methods/methods_multi_reward.spec index 5bea0e34..60046be5 100644 --- a/certora/stata/specs/methods/methods_multi_reward.spec +++ b/certora/stata/specs/methods/methods_multi_reward.spec @@ -8,11 +8,11 @@ using DummyERC20_rewardToken as _DummyERC20_rewardToken; /////////////////// Methods //////////////////////// -/// @dev Using mostly `NONDET` in the methods block, to speed up verification. + /// @dev Using mostly `NONDET` in the methods block, to speed up verification. -methods { + methods { // static aToken - // ------------- + // ------------- function _.getCurrentRewardsIndex(address reward) external => CONSTANT; function getUnclaimedRewards(address, address) external returns (uint256) envfree; function rewardTokens() external returns (address[]) envfree; @@ -32,10 +32,10 @@ methods { function _.finalizeTransfer(address, address, address, uint256, uint256, uint256) external => NONDET; // In ScaledBalanceTokenBase.sol called by getAssetIndex - function _.scaledTotalSupply() external => NONDET; + function _.scaledTotalSupply() external => DISPATCHER(true); // rewards controller - // ------------------ + // ------------------ function _RewardsController.getAvailableRewardsCount(address) external returns (uint128) envfree; function _RewardsController.getRewardsByAsset(address, uint128) external returns (address) envfree; // Called by IncentivizedERC20.sol and by StaticATokenLM.sol @@ -48,7 +48,11 @@ methods { function _.performTransfer(address,address,uint256) external => NONDET; // aToken +<<<<<<< HEAD // ------ +======= + // ------ +>>>>>>> certora-for-project-a function _AToken.UNDERLYING_ASSET_ADDRESS() external returns (address) envfree; function _.mint(address,address,uint256,uint256) external => NONDET; function _.burn(address,address,uint256,uint256) external => NONDET; @@ -58,9 +62,9 @@ methods { function _DummyERC20_rewardToken.balanceOf(address) external returns (uint256) envfree; function _.permit(address,address,uint256,uint256,uint8,bytes32,bytes32) external => NONDET; -} + } -///////////////// Definition /////////////////////// +///////////////// FUNCTIONS /////////////////////// /// @title Set up a single reward token function single_RewardToken_setup() { diff --git a/certora/stata/specs/staticATokenLM/StaticATokenLM.spec b/certora/stata/specs/staticATokenLM/StaticATokenLM.spec deleted file mode 100644 index 49dbd95e..00000000 --- a/certora/stata/specs/staticATokenLM/StaticATokenLM.spec +++ /dev/null @@ -1,876 +0,0 @@ -import "../methods/methods_base.spec"; - -/////////////////// Methods //////////////////////// - -methods { - function _.permit(address,address,uint256,uint256,uint8,bytes32,bytes32) external => NONDET; - function _.getIncentivesController() external => CONSTANT; - function _.getRewardsList() external => NONDET; - //call by RewardsController.IncentivizedERC20.sol and also by StaticATokenLM.sol - function _.handleAction(address,uint256,uint256) external => DISPATCHER(true); - - function balanceOf(address) external returns (uint256) envfree; - function totalSupply() external returns (uint256) envfree; -} - -////////////////// FUNCTIONS ////////////////////// - - /// @title Reward hook - /// @notice allows a single reward - //todo: allow 2 or 3 rewards - hook Sload address reward _rewardTokens[INDEX uint256 i] { - require reward == _DummyERC20_rewardToken; - } - - /// @title Sum of balances of StaticAToken - ghost sumAllBalance() returns mathint { - init_state axiom sumAllBalance() == 0; - } - - hook Sstore balanceOf[KEY address a] uint256 balance (uint256 old_balance) { - havoc sumAllBalance assuming sumAllBalance@new() == sumAllBalance@old() + balance - old_balance; - } - -///////////////// Properties /////////////////////// - - /** - * @title Rewards claiming when sufficient rewards exist - * Ensures rewards are updated correctly after claiming, when there are enough - * reward funds. - * - * @dev Passed in job-id=`655ba8737ada43efab71eaabf8d41096` - */ - rule rewardsConsistencyWhenSufficientRewardsExist() { - // Assuming single reward - single_RewardToken_setup(); - - // Create a rewards array - address[] _rewards; - require _rewards[0] == _DummyERC20_rewardToken; - require _rewards.length == 1; - - env e; - require e.msg.sender != currentContract; // Cannot claim to contract - uint256 rewardsBalancePre = _DummyERC20_rewardToken.balanceOf(e.msg.sender); - uint256 claimablePre = getClaimableRewards(e, e.msg.sender, _DummyERC20_rewardToken); - - // Ensure contract has sufficient rewards - require _DummyERC20_rewardToken.balanceOf(currentContract) >= claimablePre; - - claimRewardsToSelf(e, _rewards); - - uint256 rewardsBalancePost = _DummyERC20_rewardToken.balanceOf(e.msg.sender); - uint256 unclaimedPost = getUnclaimedRewards(e.msg.sender, _DummyERC20_rewardToken); - uint256 claimablePost = getClaimableRewards(e, e.msg.sender, _DummyERC20_rewardToken); - - assert rewardsBalancePost >= rewardsBalancePre, "Rewards balance reduced after claim"; - mathint rewardsGiven = rewardsBalancePost - rewardsBalancePre; - assert to_mathint(claimablePre) == rewardsGiven + unclaimedPost, "Rewards given unequal to claimable"; - assert claimablePost == unclaimedPost, "Claimable different from unclaimed"; - assert unclaimedPost == 0; // Left last as this is an implementation detail - } - - /** - * @title Rewards claiming when rewards are insufficient - /* Ensures rewards are updated correctly after claiming, when there aren't - * enough funds. - * - * @dev Failed in previous version of the code: job-id=`274946aa85a247149c025df228c71bc1`. - * Failure reported in: `https://github.com/bgd-labs/static-a-token-v3/issues/23`. - * Passed after fix with rule-sanity in job-id=`32b981560c2c41eab24606daa7d38694`. - */ - rule rewardsConsistencyWhenInsufficientRewards() { - // Assuming single reward - single_RewardToken_setup(); - - env e; - require e.msg.sender != currentContract; // Cannot claim to contract - require e.msg.sender != _TransferStrategy; - - uint256 rewardsBalancePre = _DummyERC20_rewardToken.balanceOf(e.msg.sender); - uint256 claimablePre = getClaimableRewards(e, e.msg.sender, _DummyERC20_rewardToken); - - // Ensure contract does not have sufficient rewards - require _DummyERC20_rewardToken.balanceOf(currentContract) < claimablePre; - - claimSingleRewardOnBehalf(e, e.msg.sender, e.msg.sender, _DummyERC20_rewardToken); - - uint256 rewardsBalancePost = _DummyERC20_rewardToken.balanceOf(e.msg.sender); - uint256 unclaimedPost = getUnclaimedRewards(e.msg.sender, _DummyERC20_rewardToken); - uint256 claimablePost = getClaimableRewards(e, e.msg.sender, _DummyERC20_rewardToken); - - assert rewardsBalancePost >= rewardsBalancePre, "Rewards balance reduced after claim"; - mathint rewardsGiven = rewardsBalancePost - rewardsBalancePre; - // Note, when `rewardsGiven` is 0 the unclaimed rewards are not updated - assert ( - ( (rewardsGiven > 0) => (to_mathint(claimablePre) == rewardsGiven + unclaimedPost) ) && - ( (rewardsGiven == 0) => (claimablePre == claimablePost) ) - ), "Claimable rewards changed unexpectedly"; - } - - - /** - * @title Only claiming rewards should reduce contract's total rewards balance - * Only "claim reward" methods should cause the total rewards balance of - * `StaticATokenLM` to decline. Note that `initialize`, `metaDeposit` - * and `metaWithdraw` are filtered out. To avoid timeouts the rest of the - * methods were split between several versions of this rule. - * - * WARNING: `metaDeposit` seems to be vacuous, i.e. **always** fails on a - * require statement. - * - * @dev Passed with rule-sanity in job-id=`98beb842d5b94278ac4a9222249fb564` - * - */ - rule rewardsTotalDeclinesOnlyByClaim(method f) filtered { - // Filter out initialize - f -> ( - f.contract == currentContract && - !harnessOnlyMethods(f) && - f.selector != sig:initialize(address, string, string).selector) && - // Exclude meta functions - (f.selector != sig:metaDeposit( - address,address,uint256,uint16,bool,uint256, - IStaticATokenLM.PermitParams calldata,IStaticATokenLM.SignatureParams calldata).selector) && - (f.selector != sig:metaWithdraw( - address,address,uint256,uint256,bool,uint256,IStaticATokenLM.SignatureParams calldata - ).selector) && - // Exclude methods that time out - (f.selector != sig:deposit(uint256,address).selector) && - (f.selector != sig:deposit(uint256,address,uint16,bool).selector) && - (f.selector != sig:redeem(uint256,address,address).selector) && - (f.selector != sig:redeem(uint256,address,address,bool).selector) && - (f.selector != sig:mint(uint256,address).selector) - } { - // Assuming single reward - single_RewardToken_setup(); - rewardsController_reward_setup(); - - require _AToken.UNDERLYING_ASSET_ADDRESS() == _DummyERC20_aTokenUnderlying; - - env e; - require e.msg.sender != currentContract; - uint256 preTotal = getTotalClaimableRewards(e, _DummyERC20_rewardToken); - - calldataarg args; - f(e, args); - - uint256 postTotal = getTotalClaimableRewards(e, _DummyERC20_rewardToken); - - assert (postTotal < preTotal) => ( - (f.selector == sig:claimRewardsOnBehalf(address, address, address[]).selector) || - (f.selector == sig:claimRewards(address, address[]).selector) || - (f.selector == sig:claimRewardsToSelf(address[]).selector) || - (f.selector == sig:claimSingleRewardOnBehalf(address,address,address).selector) - ), "Total rewards decline not due to claim"; - } - - /// @dev Passed with rule-sanity in job-id=`f71cabe338614768af9e119dea55b00e` - rule rewardsTotalDeclinesOnlyByClaim_timedout_methods(method f) filtered { - // Include only the timed out methods, excluding redeem - f -> (f.selector == sig:deposit(uint256,address).selector) || - (f.selector == sig:deposit(uint256,address,uint16,bool).selector) || - (f.selector == sig:mint(uint256,address).selector) - } { - // Assuming single reward - single_RewardToken_setup(); - rewardsController_reward_setup(); - - require _AToken.UNDERLYING_ASSET_ADDRESS() == _DummyERC20_aTokenUnderlying; - - env e; - require e.msg.sender != currentContract; - uint256 preTotal = getTotalClaimableRewards(e, _DummyERC20_rewardToken); - - calldataarg args; - f(e, args); - - uint256 postTotal = getTotalClaimableRewards(e, _DummyERC20_rewardToken); - - assert (postTotal < preTotal) => ( - (f.selector == sig:claimRewardsOnBehalf(address, address, address[]).selector) || - (f.selector == sig:claimRewards(address, address[]).selector) || - (f.selector == sig:claimRewardsToSelf(address[]).selector) || - (f.selector == sig:claimSingleRewardOnBehalf(address,address,address).selector) - ), "Total rewards decline not due to claim"; - } - - /** - * @dev Passed in job-id=`06a3d30b577b4a8a8d26182e7486b065` - * using `--settings -t=7200,-mediumTimeout=40,-depth=7` - */ - rule rewardsTotalDeclinesOnlyByClaim_redeem_methods(method f) filtered { - // Include only the redeem timed out methods - f -> (f.selector == sig:redeem(uint256,address,address).selector) || - (f.selector == sig:redeem(uint256,address,address,bool).selector) - } { - // Assuming single reward - single_RewardToken_setup(); - rewardsController_reward_setup(); - - require _AToken.UNDERLYING_ASSET_ADDRESS() == _DummyERC20_aTokenUnderlying; - - env e; - require e.msg.sender != currentContract; - uint256 preTotal = getTotalClaimableRewards(e, _DummyERC20_rewardToken); - - calldataarg args; - f(e, args); - - uint256 postTotal = getTotalClaimableRewards(e, _DummyERC20_rewardToken); - - assert (postTotal < preTotal) => ( - (f.selector == sig:claimRewardsOnBehalf(address, address, address[]).selector) || - (f.selector == sig:claimRewards(address, address[]).selector) || - (f.selector == sig:claimRewardsToSelf(address[]).selector) || - (f.selector == sig:claimSingleRewardOnBehalf(address,address,address).selector) - ), "Total rewards decline not due to claim"; - } - - //fail. todo: check if property should hold - //https://vaas-stg.certora.com/output/99352/5521be2ec21640feb23be9d8b1faa08a/?anonymousKey=e2a56ad93292b812fb66afa29a0ae8fcf53b4f37 - //https://vaas-stg.certora.com/output/99352/c288ca4de50e4f7ab76bfa70fac1c7ff/?anonymousKey=9cf2d1fb28a3b4f47b952eae91f3d330ea346181 - // invariant solvency_user_balance_leq_total_asset_CASE_SPLIT_redeem_in_shares_4(address user) - // balanceOf(user) <= _AToken.scaledBalanceOf(currentContract) - // filtered { f -> f.selector == sig:redeem(uint256,address,address,bool).selector} - // { - // preserved redeem(uint256 shares, address receiver, address owner, bool toUnderlying) with (env e1) { - // requireInvariant solvency_total_asset_geq_total_supply(); - // require balanceOf(owner) <= totalSupply(); //todo: replace with requireInvariant - // require receiver != _AToken; - // require user != _SymbolicLendingPool; // TODO: review !!! - // } - // } - - - //pass -t=1400,-mediumTimeout=800,-depth=10 - /// @notice Total supply is non-zero only if total assets is non-zero -invariant solvency_positive_total_supply_only_if_positive_asset() - ((_AToken.scaledBalanceOf(currentContract) == 0) => (totalSupply() == 0)) - filtered { f -> - f.contract == currentContract && - f.selector != sig:metaWithdraw(address,address,uint256,uint256,bool,uint256,IStaticATokenLM.SignatureParams).selector - && !harnessMethodsMinusHarnessClaimMethods(f) - && !claimFunctions(f) - && f.selector != sig:claimDoubleRewardOnBehalfSame(address, address, address).selector - } - { - preserved redeem(uint256 shares, address receiver, address owner, bool toUnderlying) with (env e1) { - requireInvariant solvency_total_asset_geq_total_supply(); - require balanceOf(owner) <= totalSupply(); //todo: replace with requireInvariant - } - preserved redeem(uint256 shares, address receiver, address owner) with (env e2) { - requireInvariant solvency_total_asset_geq_total_supply(); - require balanceOf(owner) <= totalSupply(); - } - preserved withdraw(uint256 assets, address receiver, address owner) with (env e3) { - requireInvariant solvency_total_asset_geq_total_supply(); - require balanceOf(owner) <= totalSupply(); - } - - } - - - - //fail on deposit if e1.msg.sender == currentContract - //https://vaas-stg.certora.com/output/99352/83c1362989b540658dd72714c68f4f6a/?anonymousKey=00b5efbeb76fc09cbb12b5c0a53248703a16f5fe - //https://vaas-stg.certora.com/output/99352/a929ada034a5437ca001dd11b221038b/?anonymousKey=a90c16f056686fdd38110f16f6ec8f72a8d2062b - - //pass with -t=1400,-mediumTimeout=800,-depth=15 - //https://vaas-stg.certora.com/output/99352/7252b6b75144419c825fb00f1f11acc8/?anonymousKey=8cb67238d3cb2a14c8fbad5c1c8554b00221de95 - //pass with -t=1400,-mediumTimeout=800,-depth=10 - - /// @nitce Total asseta is greater than or equal to total supply. -invariant solvency_total_asset_geq_total_supply() - (_AToken.scaledBalanceOf(currentContract) >= totalSupply()) - filtered { f -> - f.contract == currentContract && - f.selector != sig:metaWithdraw(address,address,uint256,uint256,bool,uint256,IStaticATokenLM.SignatureParams calldata).selector - && f.selector != sig:redeem(uint256,address,address).selector - && f.selector != sig:redeem(uint256,address,address,bool).selector - && !harnessMethodsMinusHarnessClaimMethods(f) - && !claimFunctions(f) - && f.selector != sig:claimDoubleRewardOnBehalfSame(address, address, address).selector } - { - preserved withdraw(uint256 assets, address receiver, address owner) with (env e3) { - require balanceOf(owner) <= totalSupply(); - } - preserved deposit(uint256 assets, address receiver) with (env e4) { - require balanceOf(receiver) <= totalSupply(); //todo: replace with requireInvariant - require e4.msg.sender != currentContract; //todo: review - } - preserved deposit(uint256 assets, address receiver,uint16 referralCode, bool fromUnderlying) with (env e5) { - require balanceOf(receiver) <= totalSupply(); //todo: replace with requireInvariant - require e5.msg.sender != currentContract; //todo: review - } - preserved mint(uint256 shares, address receiver) with (env e6) { - require balanceOf(receiver) <= totalSupply(); //todo: replace with requireInvariant - require e6.msg.sender != currentContract; //todo: review - } - - } - - //timeout - //https://vaas-stg.certora.com/output/99352/cf6f23d546ed4834ae27bc4de2df81d7/?anonymousKey=a0ab74898224e42db0ef4770909848880d61abaa - //timeout with -t=1200,-mediumTimeout=800,-depth=10 - invariant solvency_total_asset_geq_total_supply_CASE_SPLIT_redeem() - (_AToken.scaledBalanceOf(currentContract) >= totalSupply()) - filtered { f -> f.selector == sig:redeem(uint256,address,address).selector} - { - preserved redeem(uint256 shares, address receiver, address owner) with (env e2) { - require balanceOf(owner) <= totalSupply(); - } - } - - //pass - /// @title correct accrued value is fetched - /// @notice assume a single asset - //pass with rule_sanity basic except metaDeposit() - //https://vaas-stg.certora.com/output/99352/ab6c92a9f96d4327b52da331d634d3ab/?anonymousKey=abb27f614a8656e6e300ce21c517009cbe0c4d3a - //https://vaas-stg.certora.com/output/99352/d8c9a8bbea114d5caad43683b06d8ba0/?anonymousKey=a079d7f7dd44c47c05c866808c32235d56bca8e8 -invariant singleAssetAccruedRewards(env e0, address _asset, address reward, address user) - ((_RewardsController.getAssetListLength() == 1 && _RewardsController.getAssetByIndex(0) == _asset) - => (_RewardsController.getUserAccruedReward(_asset, reward, user) == _RewardsController.getUserAccruedRewards(reward, user))) - filtered {f -> - f.contract == currentContract && - !harnessOnlyMethods(f) - } { - preserved with (env e1){ - setup(e1, user); - require _asset != _RewardsController; - require _asset != _TransferStrategy; - require reward != _StaticATokenLM; - require reward != _AToken; - require reward != _TransferStrategy; - } -} - - - - //pass with --rule_sanity basic - //https://vaas-stg.certora.com/output/99352/4df615c845e2445b8657ece2db477ce5/?anonymousKey=76379915d60fc1056ed4e5b391c69cd5bba3cce0 - /// @title Claiming rewards should not affect totalAssets() - rule totalAssets_stable(method f) - filtered { f -> f.selector == sig:claimSingleRewardOnBehalf(address, address, address).selector - || f.selector == sig:collectAndUpdateRewards(address).selector } - { - env e; - calldataarg args; - mathint totalAssetBefore = totalAssets(); - f(e, args); - mathint totalAssetAfter = totalAssets(); - assert totalAssetAfter == totalAssetBefore; - } - - //fail - //https://vaas-stg.certora.com/output/99352/2104ed63c44845c2a8a793007224cb2a/?anonymousKey=f2e12ee6154f8b707b21fb62d1cc12a46a6c03b1 - rule totalAssets_stable_after_collectAndUpdateRewards() - { - env e; - address reward; - mathint totalAssetBefore = totalAssets(); - collectAndUpdateRewards(e, reward); - mathint totalAssetAfter = totalAssets(); - assert totalAssetAfter == totalAssetBefore; - } - - - //pass - //https://vaas-stg.certora.com/output/99352/7d2ce2bb69ad4d71b4a179daa08633e4/?anonymousKey=7446e8a015ea9aa79cbc542c10b932e04169a0ab - /// @title Receiving ATokens does not affect the amount of rewards fetched by collectAndUpdateRewards() - rule reward_balance_stable_after_collectAndUpdateRewards() - { - uint256 totalAccrued = _RewardsController.getUserAccruedRewards(_AToken, currentContract); - require (totalAccrued == 0); - - env e; - address reward; - - mathint totalAssetBefore = totalAssets(); - - collectAndUpdateRewards(e, reward); - mathint totalAssetAfter = totalAssets(); - - assert totalAssetAfter == totalAssetBefore; - } - - // //timeout - // /// @title getTotalClaimableRewards() is stable unless rewards were claimed - // rule totalClaimableRewards_stable(method f) - // filtered { f -> !f.isView && !claimFunctions(f) && f.selector != sig:initialize(address,string,string).selector } - // { - // env e; - // require e.msg.sender != currentContract; - // setup(e, 0); - // calldataarg args; - // address reward; - // require e.msg.sender != reward ; - // require currentContract != e.msg.sender; - // require _AToken != e.msg.sender; - // require _RewardsController != e.msg.sender; - // require _DummyERC20_aTokenUnderlying != e.msg.sender; - // require _DummyERC20_rewardToken != e.msg.sender; - // require _SymbolicLendingPool != e.msg.sender; - // require _TransferStrategy != e.msg.sender; - - // require currentContract != reward; - // require _AToken != reward; - // require _RewardsController != reward; - // require _DummyERC20_aTokenUnderlying != reward; - // require _SymbolicLendingPool != reward; - // require _TransferStrategy != reward; - // require _TransferStrategy != reward; - - - // mathint totalClaimableRewardsBefore = getTotalClaimableRewards(e, reward); - // f(e, args); - // mathint totalClaimableRewardsAfter = getTotalClaimableRewards(e, reward); - // assert totalClaimableRewardsAfter == totalClaimableRewardsBefore; - // } - - //pass with -t=1400,-mediumTimeout=800,-depth=10 - rule totalClaimableRewards_stable_CASE_SPLIT(method f) - filtered { f -> !f.isView && !claimFunctions(f) - && !collectAndUpdateFunction(f) - && f.selector != sig:initialize(address,string,string).selector - && f.selector != sig:redeem(uint256,address,address,bool).selector - && f.selector != sig:redeem(uint256,address,address).selector - && f.selector != sig:withdraw(uint256,address,address).selector - && f.selector != sig:deposit(uint256,address).selector - && f.selector != sig:mint(uint256,address).selector - && f.selector != sig:metaWithdraw(address,address,uint256,uint256,bool,uint256,IStaticATokenLM.SignatureParams calldata).selector - && f.selector != sig:claimSingleRewardOnBehalf(address,address,address).selector - } - { - require _RewardsController.getAssetByIndex(0) != _RewardsController; - require _RewardsController.getAssetListLength() > 0; - - uint256 totalAccrued = _RewardsController.getUserAccruedRewards(_AToken, currentContract); - require (totalAccrued == 0); - - - - env e; - address reward; - - mathint totalAssetBefore = totalAssets(); - - collectAndUpdateRewards(e, reward); - mathint totalAssetAfter = totalAssets(); - - assert totalAssetAfter == totalAssetBefore; - } - - - //timeout -t=1400,-mediumTimeout=800,-depth=10 =10 - //pass with -t=1200,-mediumTimeout=1200,-depth=10 - // 16 minutes - //https://vaas-stg.certora.com/output/99352/12829795cff241098f304ce49f73051e/?anonymousKey=739f53463e398ed7d5b2eec101f254b6095e0841 - // 40 minutes - //https://vaas-stg.certora.com/output/99352/5a29622bc18c438ba016a27162a82c84/?anonymousKey=130051cfba518c2cdf7f0e86ecc71ea05d98367b - rule totalClaimableRewards_stable_CASE_SPLIT_redeem() - { - env e; - require e.msg.sender != currentContract; - setup(e, 0); - address reward; - require e.msg.sender != reward; - require currentContract != e.msg.sender; - require _AToken != e.msg.sender; - require _RewardsController != e.msg.sender; - require _DummyERC20_aTokenUnderlying != e.msg.sender; - require _DummyERC20_rewardToken != e.msg.sender; - require _SymbolicLendingPool != e.msg.sender; - require _TransferStrategy != e.msg.sender; - - require currentContract != reward; - require _AToken != reward; - require _RewardsController != reward; - require _DummyERC20_aTokenUnderlying != reward; - require _SymbolicLendingPool != reward; - require _TransferStrategy != reward; - require _TransferStrategy != reward; - - //todo: run with different settings - mathint totalClaimableRewardsBefore = getTotalClaimableRewards(e, reward); - uint256 shares; - address receiver; - address owner; - redeem(e, shares, receiver, owner); - mathint totalClaimableRewardsAfter = getTotalClaimableRewards(e, reward); - assert totalClaimableRewardsAfter == totalClaimableRewardsBefore; - } - - //timeout with -t=1000,-mediumTimeout=100,-depth=10 - //timeout no SMT run: -t=1200,-mediumTimeout=1000,-depth=6 -t=1200,-mediumTimeout=1200,-depth=10 - rule totalClaimableRewards_stable_CASE_SPLIT_deposit() - { - env e; - require e.msg.sender != currentContract; - setup(e, 0); - address reward; - require e.msg.sender != reward; - require currentContract != e.msg.sender; - require _AToken != e.msg.sender; - require _RewardsController != e.msg.sender; - require _DummyERC20_aTokenUnderlying != e.msg.sender; - require _DummyERC20_rewardToken != e.msg.sender; - require _SymbolicLendingPool != e.msg.sender; - require _TransferStrategy != e.msg.sender; - - require currentContract != reward; - require _AToken != reward; - require _RewardsController != reward; - require _DummyERC20_aTokenUnderlying != reward; - require _SymbolicLendingPool != reward; - require _TransferStrategy != reward; - require _TransferStrategy != reward; - - - mathint totalClaimableRewardsBefore = getTotalClaimableRewards(e, reward); - uint256 assets; - address receiver; - deposit(e, assets, receiver); - mathint totalClaimableRewardsAfter = getTotalClaimableRewards(e, reward); - assert totalClaimableRewardsAfter == totalClaimableRewardsBefore; - } - - // //timeout - // rule totalClaimableRewards_stable_less_requires_6(method f) - // filtered { f -> !f.isView && !claimFunctions(f) && f.selector != sig:initialize(address,string,string).selector } - // { - // env e; - // require e.msg.sender != currentContract; - // setup(e, 0); - // calldataarg args; - // address reward; - // require _DummyERC20_aTokenUnderlying != reward; - // require e.msg.sender != reward; - // require currentContract != e.msg.sender; - // require _RewardsController != e.msg.sender; - - // require currentContract != reward; - // require _DummyERC20_aTokenUnderlying != reward; - - - // mathint totalClaimableRewardsBefore = getTotalClaimableRewards(e, reward); - // f(e, args); - // mathint totalClaimableRewardsAfter = getTotalClaimableRewards(e, reward); - // assert totalClaimableRewardsAfter == totalClaimableRewardsBefore; - // } - - // //pass with -t=1400,-mediumTimeout=800,-depth=10 - // //https://vaas-stg.certora.com/output/99352/34ccc167c8d84d1985e3b0f75a5d41a6/?anonymousKey=e6f31abf1c7bb0cb66702a218b84b2830fde8928 - // rule totalClaimableRewards_stable_less_requires_7(method f) - // filtered { f -> !f.isView && !claimFunctions(f) - // && f.selector != sig:initialize(address,string,string).selector - // && f.selector != sig:redeem(uint256,address,address,bool).selector - // && f.selector != sig:redeem(uint256,address,address).selector - // && f.selector != sig:withdraw(uint256,address,address).selector - // && f.selector != sig:deposit(uint256,address).selector - // && f.selector != sig:mint(uint256,address).selector - // && f.selector != metaWithdraw(address,address,uint256,uint256,bool,uint256,(uint8,bytes32,bytes32)).selector - // && f.selector !=sig:claimSingleRewardOnBehalf(address,address,address).selector - // } - // { - // env e; - // require e.msg.sender != currentContract; - // setup(e, 0); - // calldataarg args; - // address reward; - // require _DummyERC20_aTokenUnderlying != reward; - // require _AToken != reward; - // require e.msg.sender != reward; - // require _AToken != e.msg.sender; - // require _RewardsController != e.msg.sender; - - // require currentContract != reward; - - // mathint totalClaimableRewardsBefore = getTotalClaimableRewards(e, reward); - // f(e, args); - // mathint totalClaimableRewardsAfter = getTotalClaimableRewards(e, reward); - // assert totalClaimableRewardsAfter == totalClaimableRewardsBefore; - // } - - - // //timeout - // //https://vaas-stg.certora.com/output/99352/e755cdb34d84406ab17a782c4ffd6954/?anonymousKey=2efcab1e1dd4ea504c315b148b42ef3cec8acaa7 - // rule totalClaimableRewards_stable_less_requires_7_CASE_SPLIT_redeem() - // { - // env e; - // require e.msg.sender != currentContract; - // setup(e, 0); - // address reward; - // require _DummyERC20_aTokenUnderlying != reward; - // require _AToken != reward; - // require e.msg.sender != reward; - // require _AToken != e.msg.sender; - // require _RewardsController != e.msg.sender; - - // require currentContract != reward; - - // mathint totalClaimableRewardsBefore = getTotalClaimableRewards(e, reward); - // uint256 shares; - // address receiver; - // address owner; - // redeem(e, shares, receiver, owner); - // mathint totalClaimableRewardsAfter = getTotalClaimableRewards(e, reward); - // assert totalClaimableRewardsAfter == totalClaimableRewardsBefore; - // } - - - // //timeout - // //https://vaas-stg.certora.com/output/99352/bee8d3a583c549e195c0f755bb804b34/?anonymousKey=4b9e6f6791ede0049659d54a07db5e0f146b4a42 - // rule totalClaimableRewards_stable_less_requires_7_CASE_SPLIT_deposit() - // { - // env e; - // require e.msg.sender != currentContract; - // setup(e, 0); - // address reward; - // require _DummyERC20_aTokenUnderlying != reward; - // require _AToken != reward; - // require e.msg.sender != reward; - // require _AToken != e.msg.sender; - // require _RewardsController != e.msg.sender; - - // require currentContract != reward; - - // mathint totalClaimableRewardsBefore = getTotalClaimableRewards(e, reward); - // uint256 assets; - // address receiver; - // deposit(e, assets, receiver); - // mathint totalClaimableRewardsAfter = getTotalClaimableRewards(e, reward); - // assert totalClaimableRewardsAfter == totalClaimableRewardsBefore; - // } - - - //todo: add separate rules for redeem, mint - //pass with rule_sanity basic, except metaDeposit, timeout withdraw(uint256,address,address) - //https://vaas-stg.certora.com/output/99352/ee42e7f2603740de96a8a0aaf7c676ff/?anonymousKey=f7aaa1b8ed030a8f600136ec0b94ae0bc81a0e0c - //pass - //https://vaas-stg.certora.com/output/99352/109a4a815a9a4c3abcee760bf77c5f7d/?anonymousKey=f215580ed8e698028fdedecf096752c7a3e9363c - //timeout - - /// @notice At a given block, getClaimableRewards() is unchanged unless rewards were claimed. - // rule getClaimableRewards_stable(method f) - // filtered { f -> !f.isView - // && !claimFunctions(f) - // && f.selector != sig:initialize(address,string,string).selector - // && f.selector != sig:deposit(uint256,address,uint16,bool).selector - // && f.selector != sig:redeem(uint256,address,address).selector - // && f.selector != sig:redeem(uint256,address,address,bool).selector - // && f.selector != sig:mint(uint256,address).selector - // && f.selector != metaWithdraw(address,address,uint256,uint256,bool,uint256,(uint8,bytes32,bytes32)).selector - // && f.selector !=sig:claimSingleRewardOnBehalf(address,address,address).selector - // } - // { - // env e; - // calldataarg args; - // address user; - // address reward; - - // require user != 0; - - // require currentContract != user; - // require _AToken != user; - // require _RewardsController != user; - // require _DummyERC20_aTokenUnderlying != user; - // require _DummyERC20_rewardToken != user; - // require _SymbolicLendingPool != user; - // require _TransferStrategy != user; - - // require currentContract != reward; - // require _AToken != reward; - // require _RewardsController != reward; // - // require _DummyERC20_aTokenUnderlying != reward; - // require _SymbolicLendingPool != reward; - // require _TransferStrategy != reward; - - // //require isRegisteredRewardToken(reward); //todo: review the assumption - - // mathint claimableRewardsBefore = getClaimableRewards(e, user, reward); - - // require getRewardTokensLength() > 0; - // require getRewardToken(0) == reward; //todo: review - // require _RewardsController.getAvailableRewardsCount(_AToken) > 0; //todo: review - // require _RewardsController.getRewardsByAsset(_AToken, 0) == reward; //todo: review - // f(e, args); - // mathint claimableRewardsAfter = getClaimableRewards(e, user, reward); - // assert claimableRewardsAfter == claimableRewardsBefore; - // } - // //timeout - // //should pass after excluding claimSingleRewardOnBehalf - // rule getClaimableRewards_stable_6(method f) - // filtered { f -> !f.isView - // && !claimFunctions(f) - // && f.selector != sig:initialize(address,string,string).selector - // && f.selector != sig:deposit(uint256,address,uint16,bool).selector - // && f.selector != sig:redeem(uint256,address,address).selector - // && f.selector != sig:redeem(uint256,address,address,bool).selector - // && f.selector != sig:mint(uint256,address).selector - // && f.selector != metaWithdraw(address,address,uint256,uint256,bool,uint256,(uint8,bytes32,bytes32)).selector - // && f.selector !=sig:claimSingleRewardOnBehalf(address,address,address).selector - // } - // { - // env e; - // calldataarg args; - // address user; - // address reward; - - // require user != 0; - - - // mathint claimableRewardsBefore = getClaimableRewards(e, user, reward); - - // require getRewardTokensLength() > 0; - // require getRewardToken(0) == reward; //todo: review - // require _RewardsController.getAvailableRewardsCount(_AToken) > 0; //todo: review - // require _RewardsController.getRewardsByAsset(_AToken, 0) == reward; //todo: review - // f(e, args); - // mathint claimableRewardsAfter = getClaimableRewards(e, user, reward); - // assert claimableRewardsAfter == claimableRewardsBefore; - // } - - //pass with -t=1400,-mediumTimeout=800,-depth=15 - //https://vaas-stg.certora.com/output/99352/a10c05634b4342d6b31f777826444616/?anonymousKey=67bb71ebd716ef5d10be8743ded7b466f699e32c - //pass with -t=1400,-mediumTimeout=800,-depth=10 -rule getClaimableRewards_stable(method f) - filtered { f -> - f.contract == currentContract && - !f.isView - && !claimFunctions(f) - && !collectAndUpdateFunction(f) - && f.selector != sig:initialize(address,string,string).selector - && f.selector != sig:deposit(uint256,address,uint16,bool).selector - && f.selector != sig:redeem(uint256,address,address).selector - && f.selector != sig:redeem(uint256,address,address,bool).selector - && f.selector != sig:mint(uint256,address).selector - && f.selector != sig:metaWithdraw(address,address,uint256,uint256,bool,uint256,IStaticATokenLM.SignatureParams calldata).selector - && !harnessOnlyMethods(f) - } - { - env e; - calldataarg args; - address user; - address reward; - - require user != 0; - - require currentContract != reward; - require _AToken != reward; - require _RewardsController != reward; // - require _DummyERC20_aTokenUnderlying != reward; - require _SymbolicLendingPool != reward; - require _TransferStrategy != reward; - - //require isRegisteredRewardToken(reward); //todo: review the assumption - - mathint claimableRewardsBefore = getClaimableRewards(e, user, reward); - - require getRewardTokensLength() > 0; - require getRewardToken(0) == reward; //todo: review - require _RewardsController.getAvailableRewardsCount(_AToken) > 0; //todo: review - require _RewardsController.getRewardsByAsset(_AToken, 0) == reward; //todo: review - f(e, args); - mathint claimableRewardsAfter = getClaimableRewards(e, user, reward); - assert claimableRewardsAfter == claimableRewardsBefore; - } - - //pass with -t=1400,-mediumTimeout=800,-depth=10 - rule getClaimableRewards_stable_after_redeem() - { - env e; - address user; - address reward; - - require user != 0; - - require currentContract != reward; - require _AToken != reward; - require _RewardsController != reward; // - require _DummyERC20_aTokenUnderlying != reward; - require _SymbolicLendingPool != reward; - require _TransferStrategy != reward; - - //require isRegisteredRewardToken(reward); //todo: review the assumption - - mathint claimableRewardsBefore = getClaimableRewards(e, user, reward); - - require getRewardTokensLength() > 0; - require getRewardToken(0) == reward; //todo: review - require _RewardsController.getAvailableRewardsCount(_AToken) > 0; //todo: review - require _RewardsController.getRewardsByAsset(_AToken, 0) == reward; //todo: review - uint256 shares; - address receiver; - address owner; - bool toUnderlying; - - redeem(e, shares, receiver, owner, toUnderlying); - - mathint claimableRewardsAfter = getClaimableRewards(e, user, reward); - assert claimableRewardsAfter == claimableRewardsBefore; - } - - - //pass - rule getClaimableRewards_stable_after_deposit() - { - env e; - address user; - address reward; - - uint256 assets; - address recipient; - uint16 referralCode; - bool fromUnderlying; - - require user != 0; - - - mathint claimableRewardsBefore = getClaimableRewards(e, user, reward); - require getRewardTokensLength() > 0; - require getRewardToken(0) == reward; //todo: review - - require _RewardsController.getAvailableRewardsCount(_AToken) > 0; //todo: review - require _RewardsController.getRewardsByAsset(_AToken, 0) == reward; //todo: review - deposit(e, assets, recipient,referralCode,fromUnderlying); - mathint claimableRewardsAfter = getClaimableRewards(e, user, reward); - assert claimableRewardsAfter == claimableRewardsBefore; - } - - - - //todo: remove - //pass with --loop_iter=2 --rule_sanity basic - //https://vaas-stg.certora.com/output/99352/290a1108baa64316ac4f20b5501b4617/?anonymousKey=930379a90af5aa498ec3fed2110a08f5c096efb3 - /// @title getClaimableRewards() is stable unless rewards were claimed - rule getClaimableRewards_stable_after_refreshRewardTokens() - { - env e; - address user; - address reward; - - mathint claimableRewardsBefore = getClaimableRewards(e, user, reward); - refreshRewardTokens(e); - - mathint claimableRewardsAfter = getClaimableRewards(e, user, reward); - assert claimableRewardsAfter == claimableRewardsBefore; - } - - - /// @title The amount of rewards that was actually received by claimRewards() cannot exceed the initial amount of rewards - rule getClaimableRewardsBefore_leq_claimed_claimRewardsOnBehalf(method f) - { - env e; - address onBehalfOf; - address receiver; - require receiver != currentContract; - - mathint balanceBefore = _DummyERC20_rewardToken.balanceOf(receiver); - mathint claimableRewardsBefore = getClaimableRewards(e, onBehalfOf, _DummyERC20_rewardToken); - claimSingleRewardOnBehalf(e, onBehalfOf, receiver, _DummyERC20_rewardToken); - mathint balanceAfter = _DummyERC20_rewardToken.balanceOf(receiver); - mathint deltaBalance = balanceAfter - balanceBefore; - - assert deltaBalance <= claimableRewardsBefore; - } diff --git a/foundry.toml b/foundry.toml index 44eaccc7..e5ae0500 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,15 +4,18 @@ test = 'tests' script = 'scripts' optimizer = true optimizer_runs = 200 -solc='0.8.19' +solc = '0.8.20' evm_version = 'paris' bytecode_hash = 'none' ignored_warnings_from = ["src/periphery/contracts/treasury/RevenueSplitter.sol"] out = 'out' libs = ['lib'] -remappings = [ +remappings = [] +fs_permissions = [ + { access = "write", path = "./reports" }, + { access = "read", path = "./out" }, + { access = "read", path = "./config" }, ] -fs_permissions = [{access = "write", path = "./reports"}, {access = "read", path = "./out" }, {access = "read", path = "./config"}] ffi = true [fuzz] @@ -26,7 +29,7 @@ avalanche = "${RPC_AVALANCHE}" polygon = "${RPC_POLYGON}" arbitrum = "${RPC_ARBITRUM}" fantom = "${RPC_FANTOM}" -scroll= "${RPC_SCROLL}" +scroll = "${RPC_SCROLL}" celo = "${RPC_CELO}" fantom_testnet = "${RPC_FANTOM_TESTNET}" harmony = "${RPC_HARMONY}" @@ -39,19 +42,19 @@ gnosis = "${RPC_GNOSIS}" base = "${RPC_BASE}" [etherscan] -mainnet={key="${ETHERSCAN_API_KEY_MAINNET}",chainId=1} -optimism={key="${ETHERSCAN_API_KEY_OPTIMISM}",chainId=10} -avalanche={key="${ETHERSCAN_API_KEY_AVALANCHE}",chainId=43114} -polygon={key="${ETHERSCAN_API_KEY_POLYGON}",chainId=137} -arbitrum={key="${ETHERSCAN_API_KEY_ARBITRUM}",chainId=42161} -fantom={key="${ETHERSCAN_API_KEY_FANTOM}",chainId=250} -scroll={key="${ETHERSCAN_API_KEY_SCROLL}",chainId=534352, url='https://api.scrollscan.com/api\?'} -celo={key="${ETHERSCAN_API_KEY_CELO}",chainId=42220} -sepolia={key="${ETHERSCAN_API_KEY_MAINNET}",chainId=11155111} -mumbai={key="${ETHERSCAN_API_KEY_POLYGON}",chainId=80001} -amoy={key="${ETHERSCAN_API_KEY_POLYGON}",chainId=80002} -bnb_testnet={key="${ETHERSCAN_API_KEY_BNB}",chainId=97,url='https://api-testnet.bscscan.com/api'} -bnb={key="${ETHERSCAN_API_KEY_BNB}",chainId=56,url='https://api.bscscan.com/api'} -base={key="${ETHERSCAN_API_KEY_BASE}",chain=8453} -gnosis={key="${ETHERSCAN_API_KEY_GNOSIS}",chainId=100} +mainnet = { key = "${ETHERSCAN_API_KEY_MAINNET}", chainId = 1 } +optimism = { key = "${ETHERSCAN_API_KEY_OPTIMISM}", chainId = 10 } +avalanche = { key = "${ETHERSCAN_API_KEY_AVALANCHE}", chainId = 43114 } +polygon = { key = "${ETHERSCAN_API_KEY_POLYGON}", chainId = 137 } +arbitrum = { key = "${ETHERSCAN_API_KEY_ARBITRUM}", chainId = 42161 } +fantom = { key = "${ETHERSCAN_API_KEY_FANTOM}", chainId = 250 } +scroll = { key = "${ETHERSCAN_API_KEY_SCROLL}", chainId = 534352, url = 'https://api.scrollscan.com/api\?' } +celo = { key = "${ETHERSCAN_API_KEY_CELO}", chainId = 42220 } +sepolia = { key = "${ETHERSCAN_API_KEY_MAINNET}", chainId = 11155111 } +mumbai = { key = "${ETHERSCAN_API_KEY_POLYGON}", chainId = 80001 } +amoy = { key = "${ETHERSCAN_API_KEY_POLYGON}", chainId = 80002 } +bnb_testnet = { key = "${ETHERSCAN_API_KEY_BNB}", chainId = 97, url = 'https://api-testnet.bscscan.com/api' } +bnb = { key = "${ETHERSCAN_API_KEY_BNB}", chainId = 56, url = 'https://api.bscscan.com/api' } +base = { key = "${ETHERSCAN_API_KEY_BASE}", chain = 8453 } +gnosis = { key = "${ETHERSCAN_API_KEY_GNOSIS}", chainId = 100 } # See more config options https://github.com/gakonst/foundry/tree/master/config diff --git a/package.json b/package.json index c151c49a..e2047c8f 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "prettier-plugin-solidity": "^1.1.1" }, "dependencies": { - "@bgd-labs/aave-cli": "^0.16.2", + "@bgd-labs/aave-cli": "^0.16.4", "catapulta-verify": "^1.1.1" } } diff --git a/remappings.txt b/remappings.txt index efea71a1..78eeabcf 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,4 +2,6 @@ aave-v3-core/=src/core/ aave-v3-periphery/=src/periphery/ solidity-utils/=lib/solidity-utils/src/ forge-std/=lib/forge-std/src/ -ds-test/=lib/forge-std/lib/ds-test/src/ \ No newline at end of file +ds-test/=lib/forge-std/lib/ds-test/src/ +openzeppelin-contracts-upgradeable/=lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/ +openzeppelin-contracts/=lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/ diff --git a/scripts/misc/DeployAaveV3MarketBatchedBase.sol b/scripts/misc/DeployAaveV3MarketBatchedBase.sol index 25af8793..d06d7e11 100644 --- a/scripts/misc/DeployAaveV3MarketBatchedBase.sol +++ b/scripts/misc/DeployAaveV3MarketBatchedBase.sol @@ -38,7 +38,7 @@ abstract contract DeployAaveV3MarketBatchedBase is DeployUtils, MarketInput, Scr metadataReporter.writeJsonReportMarket(report); } - function _loadWarnings(MarketConfig memory config, DeployFlags memory flags) internal view { + function _loadWarnings(MarketConfig memory config, DeployFlags memory flags) internal pure { if (config.paraswapAugustusRegistry == address(0)) { console.log( 'Warning: Paraswap Adapters will be skipped at deployment due missing config.paraswapAugustusRegistry' diff --git a/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol b/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol index 6d4abb9f..e8d40b76 100644 --- a/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol +++ b/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.0; import '../../interfaces/IMarketReportTypes.sol'; import {TransparentProxyFactory, ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/TransparentProxyFactory.sol'; -import {StaticATokenLM} from 'aave-v3-periphery/contracts/static-a-token/StaticATokenLM.sol'; -import {StaticATokenFactory} from 'aave-v3-periphery/contracts/static-a-token/StaticATokenFactory.sol'; +import {StataTokenV2} from 'aave-v3-periphery/contracts/static-a-token/StataTokenV2.sol'; +import {StataTokenFactory} from 'aave-v3-periphery/contracts/static-a-token/StataTokenFactory.sol'; import {IErrors} from '../../interfaces/IErrors.sol'; contract AaveV3HelpersProcedureTwo is IErrors { @@ -17,10 +17,10 @@ contract AaveV3HelpersProcedureTwo is IErrors { staticATokenReport.transparentProxyFactory = address(new TransparentProxyFactory()); staticATokenReport.staticATokenImplementation = address( - new StaticATokenLM(IPool(pool), IRewardsController(rewardsController)) + new StataTokenV2(IPool(pool), IRewardsController(rewardsController)) ); staticATokenReport.staticATokenFactoryImplementation = address( - new StaticATokenFactory( + new StataTokenFactory( IPool(pool), proxyAdmin, ITransparentProxyFactory(staticATokenReport.transparentProxyFactory), @@ -33,7 +33,7 @@ contract AaveV3HelpersProcedureTwo is IErrors { ).create( staticATokenReport.staticATokenFactoryImplementation, proxyAdmin, - abi.encodeWithSelector(StaticATokenFactory.initialize.selector) + abi.encodeWithSelector(StataTokenFactory.initialize.selector) ); return staticATokenReport; diff --git a/src/periphery/contracts/dependencies/openzeppelin/ECDSA.sol b/src/periphery/contracts/dependencies/openzeppelin/ECDSA.sol deleted file mode 100644 index e58805c6..00000000 --- a/src/periphery/contracts/dependencies/openzeppelin/ECDSA.sol +++ /dev/null @@ -1,180 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/ECDSA.sol) -pragma solidity ^0.8.0; - -/** - * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. - * - * These functions can be used to verify that a message was signed by the holder - * of the private keys of a given address. - */ -library ECDSA { - enum RecoverError { - NoError, - InvalidSignature, - InvalidSignatureLength, - InvalidSignatureS - } - - /** - * @dev The signature derives the `address(0)`. - */ - error ECDSAInvalidSignature(); - - /** - * @dev The signature has an invalid length. - */ - error ECDSAInvalidSignatureLength(uint256 length); - - /** - * @dev The signature has an S value that is in the upper half order. - */ - error ECDSAInvalidSignatureS(bytes32 s); - - /** - * @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not - * return address(0) without also returning an error description. Errors are documented using an enum (error type) - * and a bytes32 providing additional information about the error. - * - * If no error is returned, then the address can be used for verification purposes. - * - * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: - * this function rejects them by requiring the `s` value to be in the lower - * half order, and the `v` value to be either 27 or 28. - * - * IMPORTANT: `hash` _must_ be the result of a hash operation for the - * verification to be secure: it is possible to craft signatures that - * recover to arbitrary addresses for non-hashed data. A safe way to ensure - * this is by receiving a hash of the original message (which may otherwise - * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. - * - * Documentation for signature generation: - * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] - * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] - */ - function tryRecover( - bytes32 hash, - bytes memory signature - ) internal pure returns (address, RecoverError, bytes32) { - if (signature.length == 65) { - bytes32 r; - bytes32 s; - uint8 v; - // ecrecover takes the signature parameters, and the only way to get them - // currently is to use assembly. - /// @solidity memory-safe-assembly - assembly { - r := mload(add(signature, 0x20)) - s := mload(add(signature, 0x40)) - v := byte(0, mload(add(signature, 0x60))) - } - return tryRecover(hash, v, r, s); - } else { - return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length)); - } - } - - /** - * @dev Returns the address that signed a hashed message (`hash`) with - * `signature`. This address can then be used for verification purposes. - * - * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: - * this function rejects them by requiring the `s` value to be in the lower - * half order, and the `v` value to be either 27 or 28. - * - * IMPORTANT: `hash` _must_ be the result of a hash operation for the - * verification to be secure: it is possible to craft signatures that - * recover to arbitrary addresses for non-hashed data. A safe way to ensure - * this is by receiving a hash of the original message (which may otherwise - * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. - */ - function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { - (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature); - _throwError(error, errorArg); - return recovered; - } - - /** - * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately. - * - * See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures] - */ - function tryRecover( - bytes32 hash, - bytes32 r, - bytes32 vs - ) internal pure returns (address, RecoverError, bytes32) { - unchecked { - bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); - // We do not check for an overflow here since the shift operation results in 0 or 1. - uint8 v = uint8((uint256(vs) >> 255) + 27); - return tryRecover(hash, v, r, s); - } - } - - /** - * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately. - */ - function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { - (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs); - _throwError(error, errorArg); - return recovered; - } - - /** - * @dev Overload of {ECDSA-tryRecover} that receives the `v`, - * `r` and `s` signature fields separately. - */ - function tryRecover( - bytes32 hash, - uint8 v, - bytes32 r, - bytes32 s - ) internal pure returns (address, RecoverError, bytes32) { - // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature - // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines - // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most - // signatures from current libraries generate a unique signature with an s-value in the lower half order. - // - // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value - // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or - // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept - // these malleable signatures as well. - if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { - return (address(0), RecoverError.InvalidSignatureS, s); - } - - // If the signature is valid (and not malleable), return the signer address - address signer = ecrecover(hash, v, r, s); - if (signer == address(0)) { - return (address(0), RecoverError.InvalidSignature, bytes32(0)); - } - - return (signer, RecoverError.NoError, bytes32(0)); - } - - /** - * @dev Overload of {ECDSA-recover} that receives the `v`, - * `r` and `s` signature fields separately. - */ - function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { - (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s); - _throwError(error, errorArg); - return recovered; - } - - /** - * @dev Optionally reverts with the corresponding custom error according to the `error` argument provided. - */ - function _throwError(RecoverError error, bytes32 errorArg) private pure { - if (error == RecoverError.NoError) { - return; // no error: do nothing - } else if (error == RecoverError.InvalidSignature) { - revert ECDSAInvalidSignature(); - } else if (error == RecoverError.InvalidSignatureLength) { - revert ECDSAInvalidSignatureLength(uint256(errorArg)); - } else if (error == RecoverError.InvalidSignatureS) { - revert ECDSAInvalidSignatureS(errorArg); - } - } -} diff --git a/src/periphery/contracts/dependencies/solmate/ERC20.sol b/src/periphery/contracts/dependencies/solmate/ERC20.sol deleted file mode 100644 index 546df288..00000000 --- a/src/periphery/contracts/dependencies/solmate/ERC20.sol +++ /dev/null @@ -1,207 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.8.0; - -import {ECDSA} from '../openzeppelin/ECDSA.sol'; - -/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation. -/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol) -/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) -/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. -abstract contract ERC20 { - bytes32 public constant PERMIT_TYPEHASH = - keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)'); - - /* ////////////////////////////////////////////////////////////// - EVENTS - ////////////////////////////////////////////////////////////// */ - - event Transfer(address indexed from, address indexed to, uint256 amount); - - event Approval(address indexed owner, address indexed spender, uint256 amount); - - /* ////////////////////////////////////////////////////////////// - METADATA STORAGE - ////////////////////////////////////////////////////////////// */ - - string public name; - - string public symbol; - - uint8 public decimals; - - /* ////////////////////////////////////////////////////////////// - ERC20 STORAGE - ////////////////////////////////////////////////////////////// */ - - uint256 public totalSupply; - - mapping(address => uint256) public balanceOf; - - mapping(address => mapping(address => uint256)) public allowance; - - /* ////////////////////////////////////////////////////////////// - EIP-2612 STORAGE - ////////////////////////////////////////////////////////////// */ - - mapping(address => uint256) public nonces; - - /* ////////////////////////////////////////////////////////////// - CONSTRUCTOR - ////////////////////////////////////////////////////////////// */ - - constructor(string memory _name, string memory _symbol, uint8 _decimals) { - name = _name; - symbol = _symbol; - decimals = _decimals; - } - - /* ////////////////////////////////////////////////////////////// - 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) { - _beforeTokenTransfer(msg.sender, to, amount); - 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) { - _beforeTokenTransfer(from, to, amount); - 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 { - address signer = ECDSA.recover( - keccak256( - abi.encodePacked( - '\x19\x01', - DOMAIN_SEPARATOR(), - keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) - ) - ), - v, - r, - s - ); - - require(signer == owner, 'INVALID_SIGNER'); - - allowance[signer][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 { - _beforeTokenTransfer(address(0), to, amount); - 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 { - _beforeTokenTransfer(from, address(0), amount); - 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); - } - - /** - * @dev Hook that is called before any transfer of tokens. This includes - * minting and burning. - * - * Calling conditions: - * - * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens - * will be to transferred to `to`. - * - when `from` is zero, `amount` tokens will be minted for `to`. - * - when `to` is zero, `amount` of ``from``'s tokens will be burned. - * - `from` and `to` are never both zero. - * - * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. - */ - function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {} -} diff --git a/src/periphery/contracts/libraries/RayMathExplicitRounding.sol b/src/periphery/contracts/libraries/RayMathExplicitRounding.sol deleted file mode 100644 index 8d3f3dcb..00000000 --- a/src/periphery/contracts/libraries/RayMathExplicitRounding.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: agpl-3.0 -pragma solidity ^0.8.10; - -enum Rounding { - UP, - DOWN -} - -/** - * Simplified version of RayMath that instead of half-up rounding does explicit rounding in a specified direction. - * This is needed to have a 4626 complient implementation, that always predictable rounds in favor of the vault / static a token. - */ -library RayMathExplicitRounding { - uint256 internal constant RAY = 1e27; - uint256 internal constant WAD_RAY_RATIO = 1e9; - - function rayMulRoundDown(uint256 a, uint256 b) internal pure returns (uint256) { - if (a == 0 || b == 0) { - return 0; - } - return (a * b) / RAY; - } - - function rayMulRoundUp(uint256 a, uint256 b) internal pure returns (uint256) { - if (a == 0 || b == 0) { - return 0; - } - return ((a * b) + RAY - 1) / RAY; - } - - function rayDivRoundDown(uint256 a, uint256 b) internal pure returns (uint256) { - return (a * RAY) / b; - } - - function rayDivRoundUp(uint256 a, uint256 b) internal pure returns (uint256) { - return ((a * RAY) + b - 1) / b; - } - - function rayToWadRoundDown(uint256 a) internal pure returns (uint256) { - return a / WAD_RAY_RATIO; - } -} diff --git a/src/periphery/contracts/static-a-token/ERC20AaveLMUpgradeable.sol b/src/periphery/contracts/static-a-token/ERC20AaveLMUpgradeable.sol new file mode 100644 index 00000000..651c2fa0 --- /dev/null +++ b/src/periphery/contracts/static-a-token/ERC20AaveLMUpgradeable.sol @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.17; + +import {ERC20Upgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/interfaces/IERC20.sol'; +import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; +import {SafeCast} from 'solidity-utils/contracts/oz-common/SafeCast.sol'; + +import {IRewardsController} from '../rewards/interfaces/IRewardsController.sol'; +import {IERC20AaveLM} from './interfaces/IERC20AaveLM.sol'; + +/** + * @title ERC20AaveLMUpgradeable.sol + * @notice Wrapper smart contract that supports tracking and claiming liquidity mining rewards from the Aave system + * @dev ERC20 extension, so ERC20 initialization should be done by the children contract/s + * @author BGD labs + */ +abstract contract ERC20AaveLMUpgradeable is ERC20Upgradeable, IERC20AaveLM { + using SafeCast for uint256; + + /// @custom:storage-location erc7201:aave-dao.storage.ERC20AaveLM + struct ERC20AaveLMStorage { + address _referenceAsset; // a/v token to track rewards on INCENTIVES_CONTROLLER + address[] _rewardTokens; + mapping(address user => RewardIndexCache cache) _startIndex; + mapping(address user => mapping(address reward => UserRewardsData cache)) _userRewardsData; + } + + // keccak256(abi.encode(uint256(keccak256("aave-dao.storage.ERC20AaveLM")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant ERC20AaveLMStorageLocation = + 0x4fad66563f105be0bff96185c9058c4934b504d3ba15ca31e86294f0b01fd200; + + function _getERC20AaveLMStorage() private pure returns (ERC20AaveLMStorage storage $) { + assembly { + $.slot := ERC20AaveLMStorageLocation + } + } + + IRewardsController public immutable INCENTIVES_CONTROLLER; + + constructor(IRewardsController rewardsController) { + INCENTIVES_CONTROLLER = rewardsController; + } + + function __ERC20AaveLM_init(address referenceAsset_) internal onlyInitializing { + __ERC20AaveLM_init_unchained(referenceAsset_); + } + function __ERC20AaveLM_init_unchained(address referenceAsset_) internal onlyInitializing { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + $._referenceAsset = referenceAsset_; + + if (INCENTIVES_CONTROLLER != IRewardsController(address(0))) { + refreshRewardTokens(); + } + } + + ///@inheritdoc IERC20AaveLM + function claimRewardsOnBehalf( + address onBehalfOf, + address receiver, + address[] memory rewards + ) external { + address msgSender = _msgSender(); + if (msgSender != onBehalfOf && msgSender != INCENTIVES_CONTROLLER.getClaimer(onBehalfOf)) { + revert InvalidClaimer(msgSender); + } + + _claimRewardsOnBehalf(onBehalfOf, receiver, rewards); + } + + ///@inheritdoc IERC20AaveLM + function claimRewards(address receiver, address[] memory rewards) external { + _claimRewardsOnBehalf(_msgSender(), receiver, rewards); + } + + ///@inheritdoc IERC20AaveLM + function claimRewardsToSelf(address[] memory rewards) external { + _claimRewardsOnBehalf(_msgSender(), _msgSender(), rewards); + } + + ///@inheritdoc IERC20AaveLM + function refreshRewardTokens() public override { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + address[] memory rewards = INCENTIVES_CONTROLLER.getRewardsByAsset($._referenceAsset); + for (uint256 i = 0; i < rewards.length; i++) { + _registerRewardToken(rewards[i]); + } + } + + ///@inheritdoc IERC20AaveLM + function collectAndUpdateRewards(address reward) public returns (uint256) { + if (reward == address(0)) { + return 0; + } + + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + address[] memory assets = new address[](1); + assets[0] = address($._referenceAsset); + + return INCENTIVES_CONTROLLER.claimRewards(assets, type(uint256).max, address(this), reward); + } + + ///@inheritdoc IERC20AaveLM + function isRegisteredRewardToken(address reward) public view override returns (bool) { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + return $._startIndex[reward].isRegistered; + } + + ///@inheritdoc IERC20AaveLM + function getCurrentRewardsIndex(address reward) public view returns (uint256) { + if (address(reward) == address(0)) { + return 0; + } + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + (, uint256 nextIndex) = INCENTIVES_CONTROLLER.getAssetIndex($._referenceAsset, reward); + return nextIndex; + } + + ///@inheritdoc IERC20AaveLM + function getTotalClaimableRewards(address reward) external view returns (uint256) { + if (reward == address(0)) { + return 0; + } + + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + address[] memory assets = new address[](1); + assets[0] = $._referenceAsset; + uint256 freshRewards = INCENTIVES_CONTROLLER.getUserRewards(assets, address(this), reward); + return IERC20(reward).balanceOf(address(this)) + freshRewards; + } + + ///@inheritdoc IERC20AaveLM + function getClaimableRewards(address user, address reward) external view returns (uint256) { + return _getClaimableRewards(user, reward, balanceOf(user), getCurrentRewardsIndex(reward)); + } + + ///@inheritdoc IERC20AaveLM + function getUnclaimedRewards(address user, address reward) external view returns (uint256) { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + return $._userRewardsData[user][reward].unclaimedRewards; + } + + ///@inheritdoc IERC20AaveLM + function getReferenceAsset() external view returns (address) { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + return $._referenceAsset; + } + + ///@inheritdoc IERC20AaveLM + function rewardTokens() external view returns (address[] memory) { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + return $._rewardTokens; + } + + /** + * @notice Updates rewards for senders and receiver in a transfer (not updating rewards for address(0)) + * @param from The address of the sender of tokens + * @param to The address of the receiver of tokens + */ + function _update(address from, address to, uint256 amount) internal virtual override { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + for (uint256 i = 0; i < $._rewardTokens.length; i++) { + address rewardToken = address($._rewardTokens[i]); + uint256 rewardsIndex = getCurrentRewardsIndex(rewardToken); + if (from != address(0)) { + _updateUser(from, rewardsIndex, rewardToken); + } + if (to != address(0) && from != to) { + _updateUser(to, rewardsIndex, rewardToken); + } + } + super._update(from, to, amount); + } + + /** + * @notice Adding the pending rewards to the unclaimed for specific user and updating user index + * @param user The address of the user to update + * @param currentRewardsIndex The current rewardIndex + * @param rewardToken The address of the reward token + */ + function _updateUser(address user, uint256 currentRewardsIndex, address rewardToken) internal { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + uint256 balance = balanceOf(user); + if (balance > 0) { + $._userRewardsData[user][rewardToken].unclaimedRewards = _getClaimableRewards( + user, + rewardToken, + balance, + currentRewardsIndex + ).toUint128(); + } + $._userRewardsData[user][rewardToken].rewardsIndexOnLastInteraction = currentRewardsIndex + .toUint128(); + } + + /** + * @notice Compute the pending in WAD. Pending is the amount to add (not yet unclaimed) rewards in WAD. + * @param balance The balance of the user + * @param rewardsIndexOnLastInteraction The index which was on the last interaction of the user + * @param currentRewardsIndex The current rewards index in the system + * @return The amount of pending rewards in WAD + */ + function _getPendingRewards( + uint256 balance, + uint256 rewardsIndexOnLastInteraction, + uint256 currentRewardsIndex + ) internal view returns (uint256) { + if (balance == 0) { + return 0; + } + return (balance * (currentRewardsIndex - rewardsIndexOnLastInteraction)) / 10 ** decimals(); + } + + /** + * @notice Compute the claimable rewards for a user + * @param user The address of the user + * @param reward The address of the reward + * @param balance The balance of the user in WAD + * @param currentRewardsIndex The current rewards index + * @return The total rewards that can be claimed by the user (if `fresh` flag true, after updating rewards) + */ + function _getClaimableRewards( + address user, + address reward, + uint256 balance, + uint256 currentRewardsIndex + ) internal view returns (uint256) { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + RewardIndexCache memory rewardsIndexCache = $._startIndex[reward]; + if (!rewardsIndexCache.isRegistered) { + revert RewardNotInitialized(reward); + } + + UserRewardsData memory currentUserRewardsData = $._userRewardsData[user][reward]; + return + currentUserRewardsData.unclaimedRewards + + _getPendingRewards( + balance, + currentUserRewardsData.rewardsIndexOnLastInteraction == 0 + ? rewardsIndexCache.lastUpdatedIndex + : currentUserRewardsData.rewardsIndexOnLastInteraction, + currentRewardsIndex + ); + } + + /** + * @notice Claim rewards on behalf of a user and send them to a receiver + * @param onBehalfOf The address to claim on behalf of + * @param rewards The addresses of the rewards + * @param receiver The address to receive the rewards + */ + function _claimRewardsOnBehalf( + address onBehalfOf, + address receiver, + address[] memory rewards + ) internal virtual { + for (uint256 i = 0; i < rewards.length; i++) { + if (address(rewards[i]) == address(0)) { + continue; + } + uint256 currentRewardsIndex = getCurrentRewardsIndex(rewards[i]); + uint256 balance = balanceOf(onBehalfOf); + uint256 userReward = _getClaimableRewards( + onBehalfOf, + rewards[i], + balance, + currentRewardsIndex + ); + uint256 totalRewardTokenBalance = IERC20(rewards[i]).balanceOf(address(this)); + uint256 unclaimedReward = 0; + + if (userReward > totalRewardTokenBalance) { + totalRewardTokenBalance += collectAndUpdateRewards(address(rewards[i])); + } + + if (userReward > totalRewardTokenBalance) { + unclaimedReward = userReward - totalRewardTokenBalance; + userReward = totalRewardTokenBalance; + } + if (userReward > 0) { + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + $._userRewardsData[onBehalfOf][rewards[i]].unclaimedRewards = unclaimedReward.toUint128(); + $ + ._userRewardsData[onBehalfOf][rewards[i]] + .rewardsIndexOnLastInteraction = currentRewardsIndex.toUint128(); + SafeERC20.safeTransfer(IERC20(rewards[i]), receiver, userReward); + } + } + } + + /** + * @notice Initializes a new rewardToken + * @param reward The reward token to be registered + */ + function _registerRewardToken(address reward) internal { + if (isRegisteredRewardToken(reward)) return; + uint256 startIndex = getCurrentRewardsIndex(reward); + + ERC20AaveLMStorage storage $ = _getERC20AaveLMStorage(); + $._rewardTokens.push(reward); + $._startIndex[reward] = RewardIndexCache(true, startIndex.toUint240()); + + emit RewardTokenRegistered(reward, startIndex); + } +} diff --git a/src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol b/src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol new file mode 100644 index 00000000..d6be297a --- /dev/null +++ b/src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.17; + +import {ERC4626Upgradeable, Math, IERC4626} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC4626Upgradeable.sol'; +import {SafeERC20, IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; +import {IERC20Permit} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol'; + +import {IPool, IPoolAddressesProvider} from '../../../core/contracts/interfaces/IPool.sol'; +import {IAaveOracle} from '../../../core/contracts/interfaces/IAaveOracle.sol'; +import {DataTypes, ReserveConfiguration} from '../../../core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; + +import {IAToken} from './interfaces/IAToken.sol'; +import {IERC4626StataToken} from './interfaces/IERC4626StataToken.sol'; + +/** + * @title ERC4626StataTokenUpgradeable + * @notice Wrapper smart contract that allows to deposit tokens on the Aave protocol and receive + * a token which balance doesn't increase automatically, but uses an ever-increasing exchange rate. + * @dev ERC20 extension, so ERC20 initialization should be done by the children contract/s + * @author BGD labs + */ +abstract contract ERC4626StataTokenUpgradeable is ERC4626Upgradeable, IERC4626StataToken { + using Math for uint256; + + /// @custom:storage-location erc7201:aave-dao.storage.ERC4626StataToken + struct ERC4626StataTokenStorage { + IERC20 _aToken; + } + + // keccak256(abi.encode(uint256(keccak256("aave-dao.storage.ERC4626StataToken")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant ERC4626StataTokenStorageLocation = + 0x55029d3f54709e547ed74b2fc842d93107ab1490ab7555dd9dd0bf6451101900; + + function _getERC4626StataTokenStorage() + private + pure + returns (ERC4626StataTokenStorage storage $) + { + assembly { + $.slot := ERC4626StataTokenStorageLocation + } + } + + uint256 public constant RAY = 1e27; + + IPool public immutable POOL; + IPoolAddressesProvider public immutable POOL_ADDRESSES_PROVIDER; + + constructor(IPool pool) { + POOL = pool; + POOL_ADDRESSES_PROVIDER = pool.ADDRESSES_PROVIDER(); + } + + function __ERC4626StataToken_init(address newAToken) internal onlyInitializing { + IERC20 aTokenUnderlying = __ERC4626StataToken_init_unchained(newAToken); + __ERC4626_init_unchained(aTokenUnderlying); + } + + function __ERC4626StataToken_init_unchained( + address newAToken + ) internal onlyInitializing returns (IERC20) { + // sanity check, to be sure that we support that version of the aToken + address poolOfAToken = IAToken(newAToken).POOL(); + if (poolOfAToken != address(POOL)) revert PoolAddressMismatch(poolOfAToken); + + IERC20 aTokenUnderlying = IERC20(IAToken(newAToken).UNDERLYING_ASSET_ADDRESS()); + + ERC4626StataTokenStorage storage $ = _getERC4626StataTokenStorage(); + $._aToken = IERC20(newAToken); + + SafeERC20.forceApprove(aTokenUnderlying, address(POOL), type(uint256).max); + + return aTokenUnderlying; + } + + ///@inheritdoc IERC4626StataToken + function depositATokens(uint256 assets, address receiver) external returns (uint256) { + uint256 shares = previewDeposit(assets); + _deposit(_msgSender(), receiver, assets, shares, false); + + return shares; + } + + ///@inheritdoc IERC4626StataToken + function depositWithPermit( + uint256 assets, + address receiver, + uint256 deadline, + SignatureParams memory sig, + bool depositToAave + ) external returns (uint256) { + IERC20Permit assetToDeposit = IERC20Permit( + depositToAave ? asset() : address(_getERC4626StataTokenStorage()._aToken) + ); + + try + assetToDeposit.permit(_msgSender(), address(this), assets, deadline, sig.v, sig.r, sig.s) + {} catch {} + + uint256 shares = previewDeposit(assets); + _deposit(_msgSender(), receiver, assets, shares, depositToAave); + return shares; + } + + ///@inheritdoc IERC4626StataToken + function redeemATokens( + uint256 shares, + address receiver, + address owner + ) external returns (uint256) { + uint256 assets = previewRedeem(shares); + _withdraw(_msgSender(), receiver, owner, assets, shares, false); + + return assets; + } + + ///@inheritdoc IERC4626StataToken + function aToken() external view returns (IERC20) { + ERC4626StataTokenStorage storage $ = _getERC4626StataTokenStorage(); + return $._aToken; + } + + ///@inheritdoc IERC4626 + function maxMint(address) public view override returns (uint256) { + uint256 assets = maxDeposit(address(0)); + if (assets == type(uint256).max) return type(uint256).max; + return convertToShares(assets); + } + + ///@inheritdoc IERC4626 + function maxWithdraw(address owner) public view override returns (uint256) { + return convertToAssets(maxRedeem(owner)); + } + + ///@inheritdoc IERC4626 + function totalAssets() public view override returns (uint256) { + return _convertToAssets(totalSupply(), Math.Rounding.Floor); + } + + ///@inheritdoc IERC4626 + function maxRedeem(address owner) public view override returns (uint256) { + DataTypes.ReserveData memory reserveData = POOL.getReserveDataExtended(asset()); + + // if paused or inactive users cannot withdraw underlying + if ( + !ReserveConfiguration.getActive(reserveData.configuration) || + ReserveConfiguration.getPaused(reserveData.configuration) + ) { + return 0; + } + + // otherwise users can withdraw up to the available amount + uint256 underlyingTokenBalanceInShares = convertToShares(reserveData.virtualUnderlyingBalance); + uint256 cachedUserBalance = balanceOf(owner); + return + underlyingTokenBalanceInShares >= cachedUserBalance + ? cachedUserBalance + : underlyingTokenBalanceInShares; + } + + ///@inheritdoc IERC4626 + function maxDeposit(address) public view override returns (uint256) { + DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(asset()); + + // if inactive, paused or frozen users cannot deposit underlying + if ( + !ReserveConfiguration.getActive(reserveData.configuration) || + ReserveConfiguration.getPaused(reserveData.configuration) || + ReserveConfiguration.getFrozen(reserveData.configuration) + ) { + return 0; + } + + uint256 supplyCap = ReserveConfiguration.getSupplyCap(reserveData.configuration) * + (10 ** ReserveConfiguration.getDecimals(reserveData.configuration)); + // if no supply cap deposit is unlimited + if (supplyCap == 0) return type(uint256).max; + + // return remaining supply cap margin + uint256 currentSupply = (IAToken(reserveData.aTokenAddress).scaledTotalSupply() + + reserveData.accruedToTreasury).mulDiv(_rate(), RAY, Math.Rounding.Ceil); + return currentSupply > supplyCap ? 0 : supplyCap - currentSupply; + } + + ///@inheritdoc IERC4626StataToken + function latestAnswer() external view returns (int256) { + uint256 aTokenUnderlyingAssetPrice = IAaveOracle(POOL_ADDRESSES_PROVIDER.getPriceOracle()) + .getAssetPrice(asset()); + // @notice aTokenUnderlyingAssetPrice * rate / RAY + return int256(aTokenUnderlyingAssetPrice.mulDiv(_rate(), RAY, Math.Rounding.Floor)); + } + + function _deposit( + address caller, + address receiver, + uint256 assets, + uint256 shares, + bool depositToAave + ) internal virtual { + if (shares == 0) { + revert StaticATokenInvalidZeroShares(); + } + // If _asset is ERC777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the + // `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer, + // calls the vault, which is assumed not malicious. + // + // Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the + // assets are transferred and before the shares are minted, which is a valid state. + // slither-disable-next-line reentrancy-no-eth + + if (depositToAave) { + address cachedAsset = asset(); + SafeERC20.safeTransferFrom(IERC20(cachedAsset), caller, address(this), assets); + POOL.deposit(cachedAsset, assets, address(this), 0); + } else { + ERC4626StataTokenStorage storage $ = _getERC4626StataTokenStorage(); + SafeERC20.safeTransferFrom($._aToken, caller, address(this), assets); + } + _mint(receiver, shares); + + emit Deposit(caller, receiver, assets, shares); + } + + function _deposit( + address caller, + address receiver, + uint256 assets, + uint256 shares + ) internal virtual override { + _deposit(caller, receiver, assets, shares, true); + } + + function _withdraw( + address caller, + address receiver, + address owner, + uint256 assets, + uint256 shares, + bool withdrawFromAave + ) internal virtual { + if (caller != owner) { + _spendAllowance(owner, caller, shares); + } + + // If _asset is ERC777, `transfer` can trigger a reentrancy AFTER the transfer happens through the + // `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer, + // calls the vault, which is assumed not malicious. + // + // Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the + // shares are burned and after the assets are transferred, which is a valid state. + _burn(owner, shares); + if (withdrawFromAave) { + POOL.withdraw(asset(), assets, receiver); + } else { + ERC4626StataTokenStorage storage $ = _getERC4626StataTokenStorage(); + SafeERC20.safeTransfer($._aToken, receiver, assets); + } + + emit Withdraw(caller, receiver, owner, assets, shares); + } + + function _withdraw( + address caller, + address receiver, + address owner, + uint256 assets, + uint256 shares + ) internal virtual override { + _withdraw(caller, receiver, owner, assets, shares, true); + } + + function _convertToShares( + uint256 assets, + Math.Rounding rounding + ) internal view virtual override returns (uint256) { + // * @notice assets * RAY / exchangeRate + return assets.mulDiv(RAY, _rate(), rounding); + } + + function _convertToAssets( + uint256 shares, + Math.Rounding rounding + ) internal view virtual override returns (uint256) { + // * @notice share * exchangeRate / RAY + return shares.mulDiv(_rate(), RAY, rounding); + } + + function _rate() internal view returns (uint256) { + return POOL.getReserveNormalizedIncome(asset()); + } +} diff --git a/src/periphery/contracts/static-a-token/README.md b/src/periphery/contracts/static-a-token/README.md index 9ced57a6..90d998df 100644 --- a/src/periphery/contracts/static-a-token/README.md +++ b/src/periphery/contracts/static-a-token/README.md @@ -6,23 +6,22 @@ ## About -The static-a-token contains an [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626) generic token vault/wrapper for all Aave v3 pools. +The StataToken in an [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626) generic token vault/wrapper intended to be used with aave v3 aTokens. ## Features - **Full [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626) compatibility.** - **Accounting for any potential liquidity mining rewards.** Let’s say some team of the Aave ecosystem (or the Aave community itself) decides to incentivize deposits of USDC on Aave v3 Ethereum. By holding `stataUSDC`, the user will still be eligible for those incentives. It is important to highlight that while currently the wrapper supports infinite reward tokens by design (e.g. AAVE incentivizing stETH & Lido incentivizing stETH as well), each reward needs to be permissionlessly registered which bears some [⁽¹⁾](#limitations). -- **Meta-transactions support.** To enable interfaces to offer gas-less transactions to deposit/withdraw on the wrapper/Aave protocol (also supported on Aave v3). Including permit() for transfers of the `stataAToken` itself. -- **Upgradable by the Aave governance.** Similar to other contracts of the Aave ecosystem, the Level 1 executor (short executor) will be able to add new features to the deployed instances of the `stataTokens`. -- **Powered by a stataToken Factory.** Whenever a token will be listed on Aave v3, anybody will be able to call the stataToken Factory to deploy an instance for the new asset, permissionless, but still assuring the code used and permissions are properly configured without any extra headache. +- **Upgradable by the Aave governance.** Similar to other contracts of the Aave ecosystem, the Level 1 executor (short executor) will be able to add new features to the deployed instances of the `StataTokens`. +- **Powered by a StataTokenFactory.** Whenever a token will be listed on Aave v3, anybody will be able to call the StataTokenFactory to deploy an instance for the new asset, permissionless, but still assuring the code used and permissions are properly configured without any extra headache. -See [IStaticATokenLM.sol](./interfaces/IStaticATokenLM.sol) for detailed method documentation. +See [IStata4626LM.sol](./interfaces/IERC20AaveLM.sol) for detailed method documentation. ## Deployed Addresses -The staticATokenFactory is deployed for all major Aave v3 pools. -An up to date address can be fetched from the respective [address-book pool library](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol). +The StataTokenFactory is deployed for all major Aave v3 pools. +An up to date address can be fetched from the respective [address-book pool library](https://search.onaave.com/?q=stata%20factory). ## Limitations @@ -36,3 +35,60 @@ For this project, the security procedures applied/being finished are: - The test suite of the codebase itself. - Certora audit/property checking for all the dynamics of the `stataToken`, including respecting all the specs of [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626). + +--- + +## Upgrade Notes StataTokenV2 + +### Inheritance + +The `StaticATokenLM`(v1) was based on solmate. +To allow more flexibility the new `StataTokenV2`(v2) is based on [openzeppelin-contracts-upgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable) which relies on [`ERC-7201`](https://eips.ethereum.org/EIPS/eip-7201) which isolates storage per contract. + +The implementation is seperated in two ERC20 extentions and one actual "merger" contract stitching functionality together. + +1. `ERC20AaveLM` is an abstract contract implementing the forwarding of liquidity mining from an underlying AaveERC20 - an ERC20 implementing `scaled` functions - to holders of a wrapper contract. + The abstract contract is following `ERC-7201` and acts as erc20 extension. +2. `ERC4626StataToken` is an abstract contract implementing the [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626) methods for an underlying aToken. + The abstract contract is following `ERC-7201` and acts as erc20 extension. + The extension considers pool limitations like pausing, caps and available liquidity. + In addition it adds a `latestAnswer` priceFeed, which returns the share price based on how aave prices the underlying. +3. `StataTokenV2` is the main contract inheriting `ERC20AaveLM` and `ERC4626StataToken`, while also adding `Pausability`, `Rescuability`, `Permit` and the actual initialization. + +![inheritance graph](./inheritance.png) + +### Libraries + +The previous `StaticATokenLM` relied on `WadRayMath` and `WadRayMathExplicitRounding` - a custom version where one can specify the rounding behavior - for math operations. +To align the system with the other open zeppelin contracts, all usage has been replaced by the [openzeppelin math](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/Math.sol) library. + +### MetaTransactions + +MetaTransactions have been removed as there was no clear use-case besides permit based deposits ever used. +To account for that specific use-case a dedicated `depositWithPermit` was added. + +### Direct AToken Interaction + +In v1 deposit was overleaded to allow underlying & aToken deposits. +While this appraoch was fine it seemed unclean and caused some confusion with integrators. +Therefore v2 introduces dedicated `depositATokens` and `redeemATokens` methods. + +#### Rescuable + +[Rescuable](https://github.com/bgd-labs/solidity-utils/blob/main/src/contracts/utils/Rescuable.sol) has been applied to +the `StataTokenV2` which will allow the ACL_ADMIN of the corresponding `POOL` to rescue any tokens on the contract. + +#### Pausability + +The `StataTokenV2` implements the [PausableUpgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/9a47a37c4b8ce2ac465e8656f31d32ac6fe26eaa/contracts/utils/PausableUpgradeable.sol) allowing any emergency admin to pause the vault in case of an emergency. +As long as the vault is paused, minting, burning, transfers and claiming of rewards is impossible. + +#### LatestAnswer + +While there are already mechanisms to price the `StataTokenV2` implemented by 3th parties for improved UX/DX the `StataTokenV2` now exposes `latestAnswer`. +`latestAnswer` returns the asset price priced as `underlying_price * exchangeRate`. +It is important to note that: + +- `underlying_price` is fetched from the AaveOracle, which means it is subject to mechanisms implemented by the DAO on top of the Chainlink price feeds. +- the `latestAnswer` is a scaled response returning the price in the same denomination as `underlying_price` which means the sprice can be undervalued by up to 1 wei +- while this should be obvious deviations in the price - even when limited to 1 wei per share - will compound per full share diff --git a/src/periphery/contracts/static-a-token/StataOracle.sol b/src/periphery/contracts/static-a-token/StataOracle.sol deleted file mode 100644 index d1d7e7ca..00000000 --- a/src/periphery/contracts/static-a-token/StataOracle.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -import {IPool} from '../../../core/contracts/interfaces/IPool.sol'; -import {IPoolAddressesProvider} from '../../../core/contracts/interfaces/IPoolAddressesProvider.sol'; -import {IAaveOracle} from '../../../core/contracts/interfaces/IAaveOracle.sol'; -import {IStataOracle} from './interfaces/IStataOracle.sol'; -import {IERC4626} from './interfaces/IERC4626.sol'; - -/** - * @title StataOracle - * @author BGD Labs - * @notice Contract to get asset prices of stata tokens - */ -contract StataOracle is IStataOracle { - /// @inheritdoc IStataOracle - IPool public immutable POOL; - /// @inheritdoc IStataOracle - IAaveOracle public immutable AAVE_ORACLE; - - constructor(IPoolAddressesProvider provider) { - POOL = IPool(provider.getPool()); - AAVE_ORACLE = IAaveOracle(provider.getPriceOracle()); - } - - /// @inheritdoc IStataOracle - function getAssetPrice(address asset) public view returns (uint256) { - address underlying = IERC4626(asset).asset(); - return - (AAVE_ORACLE.getAssetPrice(underlying) * POOL.getReserveNormalizedIncome(underlying)) / 1e27; - } - - /// @inheritdoc IStataOracle - function getAssetsPrices(address[] calldata assets) external view returns (uint256[] memory) { - uint256[] memory prices = new uint256[](assets.length); - for (uint256 i = 0; i < assets.length; i++) { - prices[i] = getAssetPrice(assets[i]); - } - return prices; - } -} diff --git a/src/periphery/contracts/static-a-token/StataTokenFactory.sol b/src/periphery/contracts/static-a-token/StataTokenFactory.sol new file mode 100644 index 00000000..e7ba0929 --- /dev/null +++ b/src/periphery/contracts/static-a-token/StataTokenFactory.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IPool, DataTypes} from '../../../core/contracts/interfaces/IPool.sol'; +import {IERC20Metadata} from 'solidity-utils/contracts/oz-common/interfaces/IERC20Metadata.sol'; +import {ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/interfaces/ITransparentProxyFactory.sol'; +import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol'; +import {StataTokenV2} from './StataTokenV2.sol'; +import {IStataTokenFactory} from './interfaces/IStataTokenFactory.sol'; + +/** + * @title StataTokenFactory + * @notice Factory contract that keeps track of all deployed StataTokens for a specified pool. + * This registry also acts as a factory, allowing to deploy new StataTokens on demand. + * There can only be one StataToken per underlying on the registry at any time. + * @author BGD labs + */ +contract StataTokenFactory is Initializable, IStataTokenFactory { + IPool public immutable POOL; + address public immutable PROXY_ADMIN; + ITransparentProxyFactory public immutable TRANSPARENT_PROXY_FACTORY; + address public immutable STATA_TOKEN_IMPL; + + mapping(address => address) internal _underlyingToStataToken; + address[] internal _stataTokens; + + event StataTokenCreated(address indexed stataToken, address indexed underlying); + + constructor( + IPool pool, + address proxyAdmin, + ITransparentProxyFactory transparentProxyFactory, + address stataTokenImpl + ) { + POOL = pool; + PROXY_ADMIN = proxyAdmin; + TRANSPARENT_PROXY_FACTORY = transparentProxyFactory; + STATA_TOKEN_IMPL = stataTokenImpl; + } + + function initialize() external initializer {} + + ///@inheritdoc IStataTokenFactory + function createStataTokens(address[] memory underlyings) external returns (address[] memory) { + address[] memory stataTokens = new address[](underlyings.length); + for (uint256 i = 0; i < underlyings.length; i++) { + address cachedStataToken = _underlyingToStataToken[underlyings[i]]; + if (cachedStataToken == address(0)) { + DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(underlyings[i]); + if (reserveData.aTokenAddress == address(0)) + revert NotListedUnderlying(reserveData.aTokenAddress); + bytes memory symbol = abi.encodePacked( + 'stat', + IERC20Metadata(reserveData.aTokenAddress).symbol(), + 'v2' + ); + address stataToken = TRANSPARENT_PROXY_FACTORY.createDeterministic( + STATA_TOKEN_IMPL, + PROXY_ADMIN, + abi.encodeWithSelector( + StataTokenV2.initialize.selector, + reserveData.aTokenAddress, + string( + abi.encodePacked('Static ', IERC20Metadata(reserveData.aTokenAddress).name(), ' v2') + ), + string(symbol) + ), + bytes32(uint256(uint160(underlyings[i]))) + ); + + _underlyingToStataToken[underlyings[i]] = stataToken; + stataTokens[i] = stataToken; + _stataTokens.push(stataToken); + emit StataTokenCreated(stataToken, underlyings[i]); + } else { + stataTokens[i] = cachedStataToken; + } + } + return stataTokens; + } + + ///@inheritdoc IStataTokenFactory + function getStataTokens() external view returns (address[] memory) { + return _stataTokens; + } + + ///@inheritdoc IStataTokenFactory + function getStataToken(address underlying) external view returns (address) { + return _underlyingToStataToken[underlying]; + } +} diff --git a/src/periphery/contracts/static-a-token/StataTokenV2.sol b/src/periphery/contracts/static-a-token/StataTokenV2.sol new file mode 100644 index 00000000..e984b2b6 --- /dev/null +++ b/src/periphery/contracts/static-a-token/StataTokenV2.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {ERC20Upgradeable, ERC20PermitUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol'; +import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol'; +import {IRescuable, Rescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; + +import {IACLManager} from '../../../core/contracts/interfaces/IACLManager.sol'; +import {ERC4626Upgradeable, ERC4626StataTokenUpgradeable, IPool} from './ERC4626StataTokenUpgradeable.sol'; +import {ERC20AaveLMUpgradeable, IRewardsController} from './ERC20AaveLMUpgradeable.sol'; +import {IStataTokenV2} from './interfaces/IStataTokenV2.sol'; + +/** + * @title StataTokenV2 + * @notice A 4626 Vault which wrapps aTokens in order to translate the rebasing nature of yield accrual into a non-rebasing value accrual. + * @author BGD labs + */ +contract StataTokenV2 is + ERC20PermitUpgradeable, + ERC20AaveLMUpgradeable, + ERC4626StataTokenUpgradeable, + PausableUpgradeable, + Rescuable, + IStataTokenV2 +{ + constructor( + IPool pool, + IRewardsController rewardsController + ) ERC20AaveLMUpgradeable(rewardsController) ERC4626StataTokenUpgradeable(pool) { + _disableInitializers(); + } + + modifier onlyPauseGuardian() { + if (!canPause(_msgSender())) revert OnlyPauseGuardian(_msgSender()); + _; + } + + function initialize( + address aToken, + string calldata staticATokenName, + string calldata staticATokenSymbol + ) external initializer { + __ERC20_init(staticATokenName, staticATokenSymbol); + __ERC20Permit_init(staticATokenName); + __ERC20AaveLM_init(aToken); + __ERC4626StataToken_init(aToken); + __Pausable_init(); + } + + ///@inheritdoc IStataTokenV2 + function setPaused(bool paused) external onlyPauseGuardian { + if (paused) _pause(); + else _unpause(); + } + + /// @inheritdoc IRescuable + function whoCanRescue() public view override returns (address) { + return POOL_ADDRESSES_PROVIDER.getACLAdmin(); + } + + ///@inheritdoc IStataTokenV2 + function canPause(address actor) public view returns (bool) { + return IACLManager(POOL_ADDRESSES_PROVIDER.getACLManager()).isEmergencyAdmin(actor); + } + + function decimals() public view override(ERC20Upgradeable, ERC4626Upgradeable) returns (uint8) { + /// @notice The initialization of ERC4626Upgradeable already assures that decimal are + /// the same as the underlying asset of the StataTokenV2, e.g. decimals of WETH for stataWETH + return ERC4626Upgradeable.decimals(); + } + + function _claimRewardsOnBehalf( + address onBehalfOf, + address receiver, + address[] memory rewards + ) internal virtual override whenNotPaused { + super._claimRewardsOnBehalf(onBehalfOf, receiver, rewards); + } + + // @notice to merge inheritance with ERC20AaveLMUpgradeable.sol properly we put + // `whenNotPaused` here instead of using ERC20PausableUpgradeable + function _update( + address from, + address to, + uint256 amount + ) internal virtual override(ERC20AaveLMUpgradeable, ERC20Upgradeable) whenNotPaused { + ERC20AaveLMUpgradeable._update(from, to, amount); + } +} diff --git a/src/periphery/contracts/static-a-token/StaticATokenErrors.sol b/src/periphery/contracts/static-a-token/StaticATokenErrors.sol deleted file mode 100644 index bec417df..00000000 --- a/src/periphery/contracts/static-a-token/StaticATokenErrors.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -library StaticATokenErrors { - string public constant INVALID_OWNER = '1'; - string public constant INVALID_EXPIRATION = '2'; - string public constant INVALID_SIGNATURE = '3'; - string public constant INVALID_DEPOSITOR = '4'; - string public constant INVALID_RECIPIENT = '5'; - string public constant INVALID_CLAIMER = '6'; - string public constant ONLY_ONE_AMOUNT_FORMAT_ALLOWED = '7'; - string public constant INVALID_ZERO_AMOUNT = '8'; - string public constant REWARD_NOT_INITIALIZED = '9'; -} diff --git a/src/periphery/contracts/static-a-token/StaticATokenFactory.sol b/src/periphery/contracts/static-a-token/StaticATokenFactory.sol deleted file mode 100644 index c48e339a..00000000 --- a/src/periphery/contracts/static-a-token/StaticATokenFactory.sol +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -import {IPool, DataTypes} from '../../../core/contracts/interfaces/IPool.sol'; -import {IERC20Metadata} from 'solidity-utils/contracts/oz-common/interfaces/IERC20Metadata.sol'; -import {ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/interfaces/ITransparentProxyFactory.sol'; -import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol'; -import {StaticATokenLM} from './StaticATokenLM.sol'; -import {IStaticATokenFactory} from './interfaces/IStaticATokenFactory.sol'; - -/** - * @title StaticATokenFactory - * @notice Factory contract that keeps track of all deployed static aToken wrappers for a specified pool. - * This registry also acts as a factory, allowing to deploy new static aTokens on demand. - * There can only be one static aToken per underlying on the registry at a time. - * @author BGD labs - */ -contract StaticATokenFactory is Initializable, IStaticATokenFactory { - IPool public immutable POOL; - address public immutable ADMIN; - ITransparentProxyFactory public immutable TRANSPARENT_PROXY_FACTORY; - address public immutable STATIC_A_TOKEN_IMPL; - - mapping(address => address) internal _underlyingToStaticAToken; - address[] internal _staticATokens; - - event StaticTokenCreated(address indexed staticAToken, address indexed underlying); - - constructor( - IPool pool, - address proxyAdmin, - ITransparentProxyFactory transparentProxyFactory, - address staticATokenImpl - ) { - POOL = pool; - ADMIN = proxyAdmin; - TRANSPARENT_PROXY_FACTORY = transparentProxyFactory; - STATIC_A_TOKEN_IMPL = staticATokenImpl; - } - - function initialize() external initializer {} - - ///@inheritdoc IStaticATokenFactory - function createStaticATokens(address[] memory underlyings) external returns (address[] memory) { - address[] memory staticATokens = new address[](underlyings.length); - for (uint256 i = 0; i < underlyings.length; i++) { - address cachedStaticAToken = _underlyingToStaticAToken[underlyings[i]]; - if (cachedStaticAToken == address(0)) { - DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(underlyings[i]); - require(reserveData.aTokenAddress != address(0), 'UNDERLYING_NOT_LISTED'); - bytes memory symbol = abi.encodePacked( - 'stat', - IERC20Metadata(reserveData.aTokenAddress).symbol() - ); - address staticAToken = TRANSPARENT_PROXY_FACTORY.createDeterministic( - STATIC_A_TOKEN_IMPL, - ADMIN, - abi.encodeWithSelector( - StaticATokenLM.initialize.selector, - reserveData.aTokenAddress, - string(abi.encodePacked('Static ', IERC20Metadata(reserveData.aTokenAddress).name())), - string(symbol) - ), - bytes32(uint256(uint160(underlyings[i]))) - ); - _underlyingToStaticAToken[underlyings[i]] = staticAToken; - staticATokens[i] = staticAToken; - _staticATokens.push(staticAToken); - emit StaticTokenCreated(staticAToken, underlyings[i]); - } else { - staticATokens[i] = cachedStaticAToken; - } - } - return staticATokens; - } - - ///@inheritdoc IStaticATokenFactory - function getStaticATokens() external view returns (address[] memory) { - return _staticATokens; - } - - ///@inheritdoc IStaticATokenFactory - function getStaticAToken(address underlying) external view returns (address) { - return _underlyingToStaticAToken[underlying]; - } -} diff --git a/src/periphery/contracts/static-a-token/StaticATokenLM.sol b/src/periphery/contracts/static-a-token/StaticATokenLM.sol deleted file mode 100644 index 9a55fe50..00000000 --- a/src/periphery/contracts/static-a-token/StaticATokenLM.sol +++ /dev/null @@ -1,712 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -import {IPool} from '../../../core/contracts/interfaces/IPool.sol'; -import {DataTypes, ReserveConfiguration} from '../../../core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; -import {IRewardsController} from '../rewards/interfaces/IRewardsController.sol'; -import {WadRayMath} from '../../../core/contracts/protocol/libraries/math/WadRayMath.sol'; -import {MathUtils} from '../../../core/contracts/protocol/libraries/math/MathUtils.sol'; -import {SafeCast} from 'solidity-utils/contracts/oz-common/SafeCast.sol'; -import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol'; -import {SafeERC20} from 'solidity-utils/contracts/oz-common/SafeERC20.sol'; -import {IERC20Metadata} from 'solidity-utils/contracts/oz-common/interfaces/IERC20Metadata.sol'; -import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; -import {IERC20WithPermit} from 'solidity-utils/contracts/oz-common/interfaces/IERC20WithPermit.sol'; - -import {IStaticATokenLM} from './interfaces/IStaticATokenLM.sol'; -import {IAToken} from './interfaces/IAToken.sol'; -import {ERC20} from '../dependencies/solmate/ERC20.sol'; -import {IInitializableStaticATokenLM} from './interfaces/IInitializableStaticATokenLM.sol'; -import {StaticATokenErrors} from './StaticATokenErrors.sol'; -import {RayMathExplicitRounding, Rounding} from '../libraries/RayMathExplicitRounding.sol'; -import {IERC4626} from './interfaces/IERC4626.sol'; - -/** - * @title StaticATokenLM - * @notice Wrapper smart contract that allows to deposit tokens on the Aave protocol and receive - * a token which balance doesn't increase automatically, but uses an ever-increasing exchange rate. - * It supports claiming liquidity mining rewards from the Aave system. - * @author BGD labs - */ -contract StaticATokenLM is - Initializable, - ERC20('STATIC__aToken_IMPL', 'STATIC__aToken_IMPL', 18), - IStaticATokenLM, - IERC4626 -{ - using SafeERC20 for IERC20; - using SafeCast for uint256; - using WadRayMath for uint256; - using RayMathExplicitRounding for uint256; - - bytes32 public constant METADEPOSIT_TYPEHASH = - keccak256( - 'Deposit(address depositor,address receiver,uint256 assets,uint16 referralCode,bool depositToAave,uint256 nonce,uint256 deadline,PermitParams permit)' - ); - bytes32 public constant METAWITHDRAWAL_TYPEHASH = - keccak256( - 'Withdraw(address owner,address receiver,uint256 shares,uint256 assets,bool withdrawFromAave,uint256 nonce,uint256 deadline)' - ); - - uint256 public constant STATIC__ATOKEN_LM_REVISION = 2; - - IPool public immutable POOL; - IRewardsController public immutable INCENTIVES_CONTROLLER; - - IERC20 internal _aToken; - address internal _aTokenUnderlying; - address[] internal _rewardTokens; - mapping(address => RewardIndexCache) internal _startIndex; - mapping(address => mapping(address => UserRewardsData)) internal _userRewardsData; - - constructor(IPool pool, IRewardsController rewardsController) { - POOL = pool; - INCENTIVES_CONTROLLER = rewardsController; - } - - ///@inheritdoc IInitializableStaticATokenLM - function initialize( - address newAToken, - string calldata staticATokenName, - string calldata staticATokenSymbol - ) external initializer { - require(IAToken(newAToken).POOL() == address(POOL)); - _aToken = IERC20(newAToken); - - name = staticATokenName; - symbol = staticATokenSymbol; - decimals = IERC20Metadata(newAToken).decimals(); - - _aTokenUnderlying = IAToken(newAToken).UNDERLYING_ASSET_ADDRESS(); - IERC20(_aTokenUnderlying).forceApprove(address(POOL), type(uint256).max); - - if (INCENTIVES_CONTROLLER != IRewardsController(address(0))) { - refreshRewardTokens(); - } - - emit Initialized(newAToken, staticATokenName, staticATokenSymbol); - } - - ///@inheritdoc IStaticATokenLM - function refreshRewardTokens() public override { - address[] memory rewards = INCENTIVES_CONTROLLER.getRewardsByAsset(address(_aToken)); - for (uint256 i = 0; i < rewards.length; i++) { - _registerRewardToken(rewards[i]); - } - } - - ///@inheritdoc IStaticATokenLM - function isRegisteredRewardToken(address reward) public view override returns (bool) { - return _startIndex[reward].isRegistered; - } - - ///@inheritdoc IStaticATokenLM - function deposit( - uint256 assets, - address receiver, - uint16 referralCode, - bool depositToAave - ) external returns (uint256) { - (uint256 shares, ) = _deposit(msg.sender, receiver, 0, assets, referralCode, depositToAave); - return shares; - } - - ///@inheritdoc IStaticATokenLM - function metaDeposit( - address depositor, - address receiver, - uint256 assets, - uint16 referralCode, - bool depositToAave, - uint256 deadline, - PermitParams calldata permit, - SignatureParams calldata sigParams - ) external returns (uint256) { - require(depositor != address(0), StaticATokenErrors.INVALID_DEPOSITOR); - //solium-disable-next-line - require(deadline >= block.timestamp, StaticATokenErrors.INVALID_EXPIRATION); - uint256 nonce = nonces[depositor]; - - // 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( - METADEPOSIT_TYPEHASH, - depositor, - receiver, - assets, - referralCode, - depositToAave, - nonce, - deadline, - permit - ) - ) - ) - ); - nonces[depositor] = nonce + 1; - require( - depositor == ecrecover(digest, sigParams.v, sigParams.r, sigParams.s), - StaticATokenErrors.INVALID_SIGNATURE - ); - } - // assume if deadline 0 no permit was supplied - if (permit.deadline != 0) { - try - IERC20WithPermit(depositToAave ? address(_aTokenUnderlying) : address(_aToken)).permit( - depositor, - address(this), - permit.value, - permit.deadline, - permit.v, - permit.r, - permit.s - ) - {} catch {} - } - (uint256 shares, ) = _deposit(depositor, receiver, 0, assets, referralCode, depositToAave); - return shares; - } - - ///@inheritdoc IStaticATokenLM - function metaWithdraw( - address owner, - address receiver, - uint256 shares, - uint256 assets, - bool withdrawFromAave, - uint256 deadline, - SignatureParams calldata sigParams - ) external returns (uint256, uint256) { - require(owner != address(0), StaticATokenErrors.INVALID_OWNER); - //solium-disable-next-line - require(deadline >= block.timestamp, StaticATokenErrors.INVALID_EXPIRATION); - uint256 nonce = nonces[owner]; - // 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( - METAWITHDRAWAL_TYPEHASH, - owner, - receiver, - shares, - assets, - withdrawFromAave, - nonce, - deadline - ) - ) - ) - ); - nonces[owner] = nonce + 1; - require( - owner == ecrecover(digest, sigParams.v, sigParams.r, sigParams.s), - StaticATokenErrors.INVALID_SIGNATURE - ); - } - return _withdraw(owner, receiver, shares, assets, withdrawFromAave); - } - - ///@inheritdoc IERC4626 - function previewRedeem(uint256 shares) public view virtual returns (uint256) { - return _convertToAssets(shares, Rounding.DOWN); - } - - ///@inheritdoc IERC4626 - function previewMint(uint256 shares) public view virtual returns (uint256) { - return _convertToAssets(shares, Rounding.UP); - } - - ///@inheritdoc IERC4626 - function previewWithdraw(uint256 assets) public view virtual returns (uint256) { - return _convertToShares(assets, Rounding.UP); - } - - ///@inheritdoc IERC4626 - function previewDeposit(uint256 assets) public view virtual returns (uint256) { - return _convertToShares(assets, Rounding.DOWN); - } - - ///@inheritdoc IStaticATokenLM - function rate() public view returns (uint256) { - return POOL.getReserveNormalizedIncome(_aTokenUnderlying); - } - - ///@inheritdoc IStaticATokenLM - function collectAndUpdateRewards(address reward) public returns (uint256) { - if (reward == address(0)) { - return 0; - } - - address[] memory assets = new address[](1); - assets[0] = address(_aToken); - - return INCENTIVES_CONTROLLER.claimRewards(assets, type(uint256).max, address(this), reward); - } - - ///@inheritdoc IStaticATokenLM - function claimRewardsOnBehalf( - address onBehalfOf, - address receiver, - address[] memory rewards - ) external { - require( - msg.sender == onBehalfOf || msg.sender == INCENTIVES_CONTROLLER.getClaimer(onBehalfOf), - StaticATokenErrors.INVALID_CLAIMER - ); - _claimRewardsOnBehalf(onBehalfOf, receiver, rewards); - } - - ///@inheritdoc IStaticATokenLM - function claimRewards(address receiver, address[] memory rewards) external { - _claimRewardsOnBehalf(msg.sender, receiver, rewards); - } - - ///@inheritdoc IStaticATokenLM - function claimRewardsToSelf(address[] memory rewards) external { - _claimRewardsOnBehalf(msg.sender, msg.sender, rewards); - } - - ///@inheritdoc IStaticATokenLM - function getCurrentRewardsIndex(address reward) public view returns (uint256) { - if (address(reward) == address(0)) { - return 0; - } - (, uint256 nextIndex) = INCENTIVES_CONTROLLER.getAssetIndex(address(_aToken), reward); - return nextIndex; - } - - ///@inheritdoc IStaticATokenLM - function getTotalClaimableRewards(address reward) external view returns (uint256) { - if (reward == address(0)) { - return 0; - } - - address[] memory assets = new address[](1); - assets[0] = address(_aToken); - uint256 freshRewards = INCENTIVES_CONTROLLER.getUserRewards(assets, address(this), reward); - return IERC20(reward).balanceOf(address(this)) + freshRewards; - } - - ///@inheritdoc IStaticATokenLM - function getClaimableRewards(address user, address reward) external view returns (uint256) { - return _getClaimableRewards(user, reward, balanceOf[user], getCurrentRewardsIndex(reward)); - } - - ///@inheritdoc IStaticATokenLM - function getUnclaimedRewards(address user, address reward) external view returns (uint256) { - return _userRewardsData[user][reward].unclaimedRewards; - } - - ///@inheritdoc IERC4626 - function asset() external view returns (address) { - return address(_aTokenUnderlying); - } - - ///@inheritdoc IStaticATokenLM - function aToken() external view returns (IERC20) { - return _aToken; - } - - ///@inheritdoc IStaticATokenLM - function rewardTokens() external view returns (address[] memory) { - return _rewardTokens; - } - - ///@inheritdoc IERC4626 - function totalAssets() external view returns (uint256) { - return _aToken.balanceOf(address(this)); - } - - ///@inheritdoc IERC4626 - function convertToShares(uint256 assets) external view returns (uint256) { - return _convertToShares(assets, Rounding.DOWN); - } - - ///@inheritdoc IERC4626 - function convertToAssets(uint256 shares) external view returns (uint256) { - return _convertToAssets(shares, Rounding.DOWN); - } - - ///@inheritdoc IERC4626 - function maxMint(address) public view virtual returns (uint256) { - uint256 assets = maxDeposit(address(0)); - if (assets == type(uint256).max) return type(uint256).max; - return _convertToShares(assets, Rounding.DOWN); - } - - ///@inheritdoc IERC4626 - function maxWithdraw(address owner) public view virtual returns (uint256) { - uint256 shares = maxRedeem(owner); - return _convertToAssets(shares, Rounding.DOWN); - } - - ///@inheritdoc IERC4626 - function maxRedeem(address owner) public view virtual returns (uint256) { - address cachedATokenUnderlying = _aTokenUnderlying; - DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(cachedATokenUnderlying); - - // if paused or inactive users cannot withdraw underlying - if ( - !ReserveConfiguration.getActive(reserveData.configuration) || - ReserveConfiguration.getPaused(reserveData.configuration) - ) { - return 0; - } - - // otherwise users can withdraw up to the available amount - uint256 underlyingTokenBalanceInShares = _convertToShares( - IERC20(cachedATokenUnderlying).balanceOf(reserveData.aTokenAddress), - Rounding.DOWN - ); - uint256 cachedUserBalance = balanceOf[owner]; - return - underlyingTokenBalanceInShares >= cachedUserBalance - ? cachedUserBalance - : underlyingTokenBalanceInShares; - } - - ///@inheritdoc IERC4626 - function maxDeposit(address) public view virtual returns (uint256) { - DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(_aTokenUnderlying); - - // if inactive, paused or frozen users cannot deposit underlying - if ( - !ReserveConfiguration.getActive(reserveData.configuration) || - ReserveConfiguration.getPaused(reserveData.configuration) || - ReserveConfiguration.getFrozen(reserveData.configuration) - ) { - return 0; - } - - uint256 supplyCap = ReserveConfiguration.getSupplyCap(reserveData.configuration) * - (10 ** ReserveConfiguration.getDecimals(reserveData.configuration)); - // if no supply cap deposit is unlimited - if (supplyCap == 0) return type(uint256).max; - // return remaining supply cap margin - uint256 currentSupply = (IAToken(reserveData.aTokenAddress).scaledTotalSupply() + - reserveData.accruedToTreasury).rayMulRoundUp(_getNormalizedIncome(reserveData)); - return currentSupply > supplyCap ? 0 : supplyCap - currentSupply; - } - - ///@inheritdoc IERC4626 - function deposit(uint256 assets, address receiver) external virtual returns (uint256) { - (uint256 shares, ) = _deposit(msg.sender, receiver, 0, assets, 0, true); - return shares; - } - - ///@inheritdoc IERC4626 - function mint(uint256 shares, address receiver) external virtual returns (uint256) { - (, uint256 assets) = _deposit(msg.sender, receiver, shares, 0, 0, true); - - return assets; - } - - ///@inheritdoc IERC4626 - function withdraw( - uint256 assets, - address receiver, - address owner - ) external virtual returns (uint256) { - (uint256 shares, ) = _withdraw(owner, receiver, 0, assets, true); - - return shares; - } - - ///@inheritdoc IERC4626 - function redeem( - uint256 shares, - address receiver, - address owner - ) external virtual returns (uint256) { - (, uint256 assets) = _withdraw(owner, receiver, shares, 0, true); - - return assets; - } - - ///@inheritdoc IStaticATokenLM - function redeem( - uint256 shares, - address receiver, - address owner, - bool withdrawFromAave - ) external virtual returns (uint256, uint256) { - return _withdraw(owner, receiver, shares, 0, withdrawFromAave); - } - - function _deposit( - address depositor, - address receiver, - uint256 _shares, - uint256 _assets, - uint16 referralCode, - bool depositToAave - ) internal returns (uint256, uint256) { - require(receiver != address(0), StaticATokenErrors.INVALID_RECIPIENT); - require(_shares == 0 || _assets == 0, StaticATokenErrors.ONLY_ONE_AMOUNT_FORMAT_ALLOWED); - - uint256 assets = _assets; - uint256 shares = _shares; - if (shares > 0) { - if (depositToAave) { - require(shares <= maxMint(receiver), 'ERC4626: mint more than max'); - } - assets = previewMint(shares); - } else { - if (depositToAave) { - require(assets <= maxDeposit(receiver), 'ERC4626: deposit more than max'); - } - shares = previewDeposit(assets); - } - require(shares != 0, StaticATokenErrors.INVALID_ZERO_AMOUNT); - - if (depositToAave) { - address cachedATokenUnderlying = _aTokenUnderlying; - IERC20(cachedATokenUnderlying).safeTransferFrom(depositor, address(this), assets); - POOL.deposit(cachedATokenUnderlying, assets, address(this), referralCode); - } else { - _aToken.safeTransferFrom(depositor, address(this), assets); - } - - _mint(receiver, shares); - - emit Deposit(depositor, receiver, assets, shares); - - return (shares, assets); - } - - function _withdraw( - address owner, - address receiver, - uint256 _shares, - uint256 _assets, - bool withdrawFromAave - ) internal returns (uint256, uint256) { - require(receiver != address(0), StaticATokenErrors.INVALID_RECIPIENT); - require(_shares == 0 || _assets == 0, StaticATokenErrors.ONLY_ONE_AMOUNT_FORMAT_ALLOWED); - require(_shares != _assets, StaticATokenErrors.INVALID_ZERO_AMOUNT); - - uint256 assets = _assets; - uint256 shares = _shares; - - if (shares > 0) { - if (withdrawFromAave) { - require(shares <= maxRedeem(owner), 'ERC4626: redeem more than max'); - } - assets = previewRedeem(shares); - } else { - if (withdrawFromAave) { - require(assets <= maxWithdraw(owner), 'ERC4626: withdraw more than max'); - } - shares = previewWithdraw(assets); - } - - if (msg.sender != owner) { - uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. - - if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; - } - - _burn(owner, shares); - - emit Withdraw(msg.sender, receiver, owner, assets, shares); - - if (withdrawFromAave) { - POOL.withdraw(_aTokenUnderlying, assets, receiver); - } else { - _aToken.safeTransfer(receiver, assets); - } - - return (shares, assets); - } - - /** - * @notice Updates rewards for senders and receiver in a transfer (not updating rewards for address(0)) - * @param from The address of the sender of tokens - * @param to The address of the receiver of tokens - */ - function _beforeTokenTransfer(address from, address to, uint256) internal override { - for (uint256 i = 0; i < _rewardTokens.length; i++) { - address rewardToken = address(_rewardTokens[i]); - uint256 rewardsIndex = getCurrentRewardsIndex(rewardToken); - if (from != address(0)) { - _updateUser(from, rewardsIndex, rewardToken); - } - if (to != address(0) && from != to) { - _updateUser(to, rewardsIndex, rewardToken); - } - } - } - - /** - * @notice Adding the pending rewards to the unclaimed for specific user and updating user index - * @param user The address of the user to update - * @param currentRewardsIndex The current rewardIndex - * @param rewardToken The address of the reward token - */ - function _updateUser(address user, uint256 currentRewardsIndex, address rewardToken) internal { - uint256 balance = balanceOf[user]; - if (balance > 0) { - _userRewardsData[user][rewardToken].unclaimedRewards = _getClaimableRewards( - user, - rewardToken, - balance, - currentRewardsIndex - ).toUint128(); - } - _userRewardsData[user][rewardToken].rewardsIndexOnLastInteraction = currentRewardsIndex - .toUint128(); - } - - /** - * @notice Compute the pending in WAD. Pending is the amount to add (not yet unclaimed) rewards in WAD. - * @param balance The balance of the user - * @param rewardsIndexOnLastInteraction The index which was on the last interaction of the user - * @param currentRewardsIndex The current rewards index in the system - * @param assetUnit One unit of asset (10**decimals) - * @return The amount of pending rewards in WAD - */ - function _getPendingRewards( - uint256 balance, - uint256 rewardsIndexOnLastInteraction, - uint256 currentRewardsIndex, - uint256 assetUnit - ) internal pure returns (uint256) { - if (balance == 0) { - return 0; - } - return (balance * (currentRewardsIndex - rewardsIndexOnLastInteraction)) / assetUnit; - } - - /** - * @notice Compute the claimable rewards for a user - * @param user The address of the user - * @param reward The address of the reward - * @param balance The balance of the user in WAD - * @param currentRewardsIndex The current rewards index - * @return The total rewards that can be claimed by the user (if `fresh` flag true, after updating rewards) - */ - function _getClaimableRewards( - address user, - address reward, - uint256 balance, - uint256 currentRewardsIndex - ) internal view returns (uint256) { - RewardIndexCache memory rewardsIndexCache = _startIndex[reward]; - require(rewardsIndexCache.isRegistered == true, StaticATokenErrors.REWARD_NOT_INITIALIZED); - UserRewardsData memory currentUserRewardsData = _userRewardsData[user][reward]; - uint256 assetUnit = 10 ** decimals; - return - currentUserRewardsData.unclaimedRewards + - _getPendingRewards( - balance, - currentUserRewardsData.rewardsIndexOnLastInteraction == 0 - ? rewardsIndexCache.lastUpdatedIndex - : currentUserRewardsData.rewardsIndexOnLastInteraction, - currentRewardsIndex, - assetUnit - ); - } - - /** - * @notice Claim rewards on behalf of a user and send them to a receiver - * @param onBehalfOf The address to claim on behalf of - * @param rewards The addresses of the rewards - * @param receiver The address to receive the rewards - */ - function _claimRewardsOnBehalf( - address onBehalfOf, - address receiver, - address[] memory rewards - ) internal { - for (uint256 i = 0; i < rewards.length; i++) { - if (address(rewards[i]) == address(0)) { - continue; - } - uint256 currentRewardsIndex = getCurrentRewardsIndex(rewards[i]); - uint256 balance = balanceOf[onBehalfOf]; - uint256 userReward = _getClaimableRewards( - onBehalfOf, - rewards[i], - balance, - currentRewardsIndex - ); - uint256 totalRewardTokenBalance = IERC20(rewards[i]).balanceOf(address(this)); - uint256 unclaimedReward = 0; - - if (userReward > totalRewardTokenBalance) { - totalRewardTokenBalance += collectAndUpdateRewards(address(rewards[i])); - } - - if (userReward > totalRewardTokenBalance) { - unclaimedReward = userReward - totalRewardTokenBalance; - userReward = totalRewardTokenBalance; - } - if (userReward > 0) { - _userRewardsData[onBehalfOf][rewards[i]].unclaimedRewards = unclaimedReward.toUint128(); - _userRewardsData[onBehalfOf][rewards[i]].rewardsIndexOnLastInteraction = currentRewardsIndex - .toUint128(); - IERC20(rewards[i]).safeTransfer(receiver, userReward); - } - } - } - - function _convertToShares(uint256 assets, Rounding rounding) internal view returns (uint256) { - if (rounding == Rounding.UP) return assets.rayDivRoundUp(rate()); - return assets.rayDivRoundDown(rate()); - } - - function _convertToAssets(uint256 shares, Rounding rounding) internal view returns (uint256) { - if (rounding == Rounding.UP) return shares.rayMulRoundUp(rate()); - return shares.rayMulRoundDown(rate()); - } - - /** - * @notice Initializes a new rewardToken - * @param reward The reward token to be registered - */ - function _registerRewardToken(address reward) internal { - if (isRegisteredRewardToken(reward)) return; - uint256 startIndex = getCurrentRewardsIndex(reward); - - _rewardTokens.push(reward); - _startIndex[reward] = RewardIndexCache(true, startIndex.toUint240()); - - emit RewardTokenRegistered(reward, startIndex); - } - - /** - * Copy of https://github.com/aave/aave-v3-core/blob/29ff9b9f89af7cd8255231bc5faf26c3ce0fb7ce/contracts/protocol/libraries/logic/ReserveLogic.sol#L47 with memory instead of calldata - * @notice Returns the ongoing normalized income for the reserve. - * @dev A value of 1e27 means there is no income. As time passes, the income is accrued - * @dev A value of 2*1e27 means for each unit of asset one unit of income has been accrued - * @param reserve The reserve object - * @return The normalized income, expressed in ray - */ - function _getNormalizedIncome( - DataTypes.ReserveDataLegacy memory reserve - ) internal view returns (uint256) { - uint40 timestamp = reserve.lastUpdateTimestamp; - - //solium-disable-next-line - if (timestamp == block.timestamp) { - //if the index was updated in the same block, no need to perform any calculation - return reserve.liquidityIndex; - } else { - return - MathUtils.calculateLinearInterest(reserve.currentLiquidityRate, timestamp).rayMul( - reserve.liquidityIndex - ); - } - } -} diff --git a/src/periphery/contracts/static-a-token/inheritance.png b/src/periphery/contracts/static-a-token/inheritance.png new file mode 100644 index 00000000..ba79976a Binary files /dev/null and b/src/periphery/contracts/static-a-token/inheritance.png differ diff --git a/src/periphery/contracts/static-a-token/interfaces/IERC20AaveLM.sol b/src/periphery/contracts/static-a-token/interfaces/IERC20AaveLM.sol new file mode 100644 index 00000000..79eb163c --- /dev/null +++ b/src/periphery/contracts/static-a-token/interfaces/IERC20AaveLM.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +interface IERC20AaveLM { + struct UserRewardsData { + uint128 rewardsIndexOnLastInteraction; // (in RAYs) + uint128 unclaimedRewards; // (in RAYs) + } + + struct RewardIndexCache { + bool isRegistered; + uint248 lastUpdatedIndex; + } + + error InvalidClaimer(address claimer); + error RewardNotInitialized(address reward); + + event RewardTokenRegistered(address indexed reward, uint256 startIndex); + + /** + * @notice Claims rewards from `INCENTIVES_CONTROLLER` and updates internal accounting of rewards. + * @param reward The reward to claim + * @return uint256 Amount collected + */ + function collectAndUpdateRewards(address reward) external returns (uint256); + + /** + * @notice Claim rewards on behalf of a user and send them to a receiver + * @dev Only callable by if sender is onBehalfOf or sender is approved claimer + * @param onBehalfOf The address to claim on behalf of + * @param receiver The address to receive the rewards + * @param rewards The rewards to claim + */ + function claimRewardsOnBehalf( + address onBehalfOf, + address receiver, + address[] memory rewards + ) external; + + /** + * @notice Claim rewards and send them to a receiver + * @param receiver The address to receive the rewards + * @param rewards The rewards to claim + */ + function claimRewards(address receiver, address[] memory rewards) external; + + /** + * @notice Claim rewards + * @param rewards The rewards to claim + */ + function claimRewardsToSelf(address[] memory rewards) external; + + /** + * @notice Get the total claimable rewards of the contract. + * @param reward The reward to claim + * @return uint256 The current balance + pending rewards from the `_incentivesController` + */ + function getTotalClaimableRewards(address reward) external view returns (uint256); + + /** + * @notice Get the total claimable rewards for a user in WAD + * @param user The address of the user + * @param reward The reward to claim + * @return uint256 The claimable amount of rewards in WAD + */ + function getClaimableRewards(address user, address reward) external view returns (uint256); + + /** + * @notice The unclaimed rewards for a user in WAD + * @param user The address of the user + * @param reward The reward to claim + * @return uint256 The unclaimed amount of rewards in WAD + */ + function getUnclaimedRewards(address user, address reward) external view returns (uint256); + + /** + * @notice The underlying asset reward index in RAY + * @param reward The reward to claim + * @return uint256 The underlying asset reward index in RAY + */ + function getCurrentRewardsIndex(address reward) external view returns (uint256); + + /** + * @notice Returns reference a/v token address used on INCENTIVES_CONTROLLER for tracking + * @return address of reference token + */ + function getReferenceAsset() external view returns (address); + + /** + * @notice The IERC20s that are currently rewarded to addresses of the vault via LM on incentivescontroller. + * @return IERC20 The IERC20s of the rewards. + */ + function rewardTokens() external view returns (address[] memory); + + /** + * @notice Fetches all rewardTokens from the incentivecontroller and registers the missing ones. + */ + function refreshRewardTokens() external; + + /** + * @notice Checks if the passed token is a registered reward. + * @param reward The reward to claim + * @return bool signaling if token is a registered reward. + */ + function isRegisteredRewardToken(address reward) external view returns (bool); +} diff --git a/src/periphery/contracts/static-a-token/interfaces/IERC4626.sol b/src/periphery/contracts/static-a-token/interfaces/IERC4626.sol deleted file mode 100644 index 08f14f90..00000000 --- a/src/periphery/contracts/static-a-token/interfaces/IERC4626.sol +++ /dev/null @@ -1,241 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (interfaces/IERC4626.sol) - -pragma solidity ^0.8.10; - -/** - * @dev Interface of the ERC4626 "Tokenized Vault Standard", as defined in - * https://eips.ethereum.org/EIPS/eip-4626[ERC-4626]. - * - * _Available since v4.7._ - */ -interface IERC4626 { - event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); - - event Withdraw( - address indexed sender, - address indexed receiver, - address indexed owner, - uint256 assets, - uint256 shares - ); - - /** - * @dev Returns the address of the underlying token used for the Vault for accounting, depositing, and withdrawing. - * - * - MUST be an ERC-20 token contract. - * - MUST NOT revert. - */ - function asset() external view returns (address assetTokenAddress); - - /** - * @dev Returns the total amount of the underlying asset that is “managed” by Vault. - * - * - SHOULD include any compounding that occurs from yield. - * - MUST be inclusive of any fees that are charged against assets in the Vault. - * - MUST NOT revert. - */ - function totalAssets() external view returns (uint256 totalManagedAssets); - - /** - * @dev Returns the amount of shares that the Vault would exchange for the amount of assets provided, in an ideal - * scenario where all the conditions are met. - * - * - MUST NOT be inclusive of any fees that are charged against assets in the Vault. - * - MUST NOT show any variations depending on the caller. - * - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. - * - MUST NOT revert. - * - * NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the - * “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and - * from. - */ - function convertToShares(uint256 assets) external view returns (uint256 shares); - - /** - * @dev Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an ideal - * scenario where all the conditions are met. - * - * - MUST NOT be inclusive of any fees that are charged against assets in the Vault. - * - MUST NOT show any variations depending on the caller. - * - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. - * - MUST NOT revert unless due to integer overflow caused by an unreasonably large input. - * - * NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the - * “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and - * from. - */ - function convertToAssets(uint256 shares) external view returns (uint256 assets); - - /** - * @dev Returns the maximum amount of the underlying asset that can be deposited into the Vault for the receiver, - * through a deposit call. - * While deposit of aToken is not affected by aave pool configrations, deposit of the aTokenUnderlying will need to deposit to aave - * so it is affected by current aave pool configuration. - * Reference: https://github.com/aave/aave-v3-core/blob/29ff9b9f89af7cd8255231bc5faf26c3ce0fb7ce/contracts/protocol/libraries/logic/ValidationLogic.sol#L57 - * - MUST return a limited value if receiver is subject to some deposit limit. - * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited. - * - MUST NOT revert unless due to integer overflow caused by an unreasonably large input. - */ - function maxDeposit(address receiver) external view returns (uint256 maxAssets); - - /** - * @dev Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given - * current on-chain conditions. - * - * - MUST return as close to and no more than the exact amount of Vault shares that would be minted in a deposit - * call in the same transaction. I.e. deposit should return the same or more shares as previewDeposit if called - * in the same transaction. - * - MUST NOT account for deposit limits like those returned from maxDeposit and should always act as though the - * deposit would be accepted, regardless if the user has enough tokens approved, etc. - * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees. - * - MUST NOT revert. - * - * NOTE: any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage in - * share price or some other type of condition, meaning the depositor will lose assets by depositing. - */ - function previewDeposit(uint256 assets) external view returns (uint256 shares); - - /** - * @dev Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens. - * - * - MUST emit the Deposit event. - * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the - * deposit execution, and are accounted for during deposit. - * - MUST revert if all of assets cannot be deposited (due to deposit limit being reached, slippage, the user not - * approving enough underlying tokens to the Vault contract, etc). - * - * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. - */ - function deposit(uint256 assets, address receiver) external returns (uint256 shares); - - /** - * @dev Returns the maximum amount of the Vault shares that can be minted for the receiver, through a mint call. - * - MUST return a limited value if receiver is subject to some mint limit. - * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted. - * - MUST NOT revert. - */ - function maxMint(address receiver) external view returns (uint256 maxShares); - - /** - * @dev Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given - * current on-chain conditions. - * - * - MUST return as close to and no fewer than the exact amount of assets that would be deposited in a mint call - * in the same transaction. I.e. mint should return the same or fewer assets as previewMint if called in the - * same transaction. - * - MUST NOT account for mint limits like those returned from maxMint and should always act as though the mint - * would be accepted, regardless if the user has enough tokens approved, etc. - * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees. - * - MUST NOT revert. - * - * NOTE: any unfavorable discrepancy between convertToAssets and previewMint SHOULD be considered slippage in - * share price or some other type of condition, meaning the depositor will lose assets by minting. - */ - function previewMint(uint256 shares) external view returns (uint256 assets); - - /** - * @dev Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. - * - * - MUST emit the Deposit event. - * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the mint - * execution, and are accounted for during mint. - * - MUST revert if all of shares cannot be minted (due to deposit limit being reached, slippage, the user not - * approving enough underlying tokens to the Vault contract, etc). - * - * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. - */ - function mint(uint256 shares, address receiver) external returns (uint256 assets); - - /** - * @dev Returns the maximum amount of the underlying asset that can be withdrawn from the owner balance in the - * Vault, through a withdraw call. - * - * - MUST return a limited value if owner is subject to some withdrawal limit or timelock. - * - MUST NOT revert. - */ - function maxWithdraw(address owner) external view returns (uint256 maxAssets); - - /** - * @dev Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, - * given current on-chain conditions. - * - * - MUST return as close to and no fewer than the exact amount of Vault shares that would be burned in a withdraw - * call in the same transaction. I.e. withdraw should return the same or fewer shares as previewWithdraw if - * called - * in the same transaction. - * - MUST NOT account for withdrawal limits like those returned from maxWithdraw and should always act as though - * the withdrawal would be accepted, regardless if the user has enough shares, etc. - * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees. - * - MUST NOT revert. - * - * NOTE: any unfavorable discrepancy between convertToShares and previewWithdraw SHOULD be considered slippage in - * share price or some other type of condition, meaning the depositor will lose assets by depositing. - */ - function previewWithdraw(uint256 assets) external view returns (uint256 shares); - - /** - * @dev Burns shares from owner and sends exactly assets of underlying tokens to receiver. - * - * - MUST emit the Withdraw event. - * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the - * withdraw execution, and are accounted for during withdraw. - * - MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, slippage, the owner - * not having enough shares, etc). - * - * Note that some implementations will require pre-requesting to the Vault before a withdrawal may be performed. - * Those methods should be performed separately. - */ - function withdraw( - uint256 assets, - address receiver, - address owner - ) external returns (uint256 shares); - - /** - * @dev Returns the maximum amount of Vault shares that can be redeemed from the owner balance in the Vault, - * through a redeem call to the aToken underlying. - * While redeem of aToken is not affected by aave pool configrations, redeeming of the aTokenUnderlying will need to redeem from aave - * so it is affected by current aave pool configuration. - * Reference: https://github.com/aave/aave-v3-core/blob/29ff9b9f89af7cd8255231bc5faf26c3ce0fb7ce/contracts/protocol/libraries/logic/ValidationLogic.sol#L87 - * - MUST return a limited value if owner is subject to some withdrawal limit or timelock. - * - MUST return balanceOf(owner) if owner is not subject to any withdrawal limit or timelock. - * - MUST NOT revert. - */ - function maxRedeem(address owner) external view returns (uint256 maxShares); - - /** - * @dev Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, - * given current on-chain conditions. - * - * - MUST return as close to and no more than the exact amount of assets that would be withdrawn in a redeem call - * in the same transaction. I.e. redeem should return the same or more assets as previewRedeem if called in the - * same transaction. - * - MUST NOT account for redemption limits like those returned from maxRedeem and should always act as though the - * redemption would be accepted, regardless if the user has enough shares, etc. - * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees. - * - MUST NOT revert. - * - * NOTE: any unfavorable discrepancy between convertToAssets and previewRedeem SHOULD be considered slippage in - * share price or some other type of condition, meaning the depositor will lose assets by redeeming. - */ - function previewRedeem(uint256 shares) external view returns (uint256 assets); - - /** - * @dev Burns exactly shares from owner and sends assets of underlying tokens to receiver. - * - * - MUST emit the Withdraw event. - * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the - * redeem execution, and are accounted for during redeem. - * - MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, slippage, the owner - * not having enough shares, etc). - * - * NOTE: some implementations will require pre-requesting to the Vault before a withdrawal may be performed. - * Those methods should be performed separately. - */ - function redeem( - uint256 shares, - address receiver, - address owner - ) external returns (uint256 assets); -} diff --git a/src/periphery/contracts/static-a-token/interfaces/IERC4626StataToken.sol b/src/periphery/contracts/static-a-token/interfaces/IERC4626StataToken.sol new file mode 100644 index 00000000..3cc4e9ca --- /dev/null +++ b/src/periphery/contracts/static-a-token/interfaces/IERC4626StataToken.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {IERC20} from 'openzeppelin-contracts/contracts/interfaces/IERC20.sol'; + +interface IERC4626StataToken { + struct SignatureParams { + uint8 v; + bytes32 r; + bytes32 s; + } + + error PoolAddressMismatch(address pool); + + error StaticATokenInvalidZeroShares(); + + error OnlyPauseGuardian(address caller); + + /** + * @notice Burns `shares` of static aToken, with receiver receiving the corresponding amount of aToken + * @param shares The shares to withdraw, in static balance of StaticAToken + * @param receiver The address that will receive the amount of `ASSET` withdrawn from the Aave protocol + * @return amountToWithdraw: aToken send to `receiver`, dynamic balance + **/ + function redeemATokens( + uint256 shares, + address receiver, + address owner + ) external returns (uint256); + + /** + * @notice Deposits aTokens and mints static aTokens to msg.sender + * @param assets The amount of aTokens to deposit (e.g. deposit of 100 aUSDC) + * @param receiver The address that will receive the static aTokens + * @return uint256 The amount of StaticAToken minted, static balance + **/ + function depositATokens(uint256 assets, address receiver) external returns (uint256); + + /** + * @notice Universal deposit method for proving aToken or underlying liquidity with permit + * @param assets The amount of aTokens or underlying to deposit + * @param receiver The address that will receive the static aTokens + * @param deadline Must be a timestamp in the future + * @param sig A `secp256k1` signature params from `msgSender()` + * @return uint256 The amount of StaticAToken minted, static balance + **/ + function depositWithPermit( + uint256 assets, + address receiver, + uint256 deadline, + SignatureParams memory sig, + bool depositToAave + ) external returns (uint256); + + /** + * @notice The aToken used inside the 4626 vault. + * @return IERC20 The aToken IERC20. + */ + function aToken() external view returns (IERC20); + + /** + * @notice Returns the current asset price of the stataToken. + * The price is calculated as `underlying_price * exchangeRate`. + * It is important to note that: + * - `underlying_price` is the price obtained by the aave-oracle and is subject to it's internal pricing mechanisms. + * - as the price is scaled over the exchangeRate, but maintains the same precision as the underlying the price might be underestimated by 1 unit. + * - when pricing multiple `shares` as `shares * price` keep in mind that the error compounds. + * @return price the current asset price. + */ + function latestAnswer() external view returns (int256); +} diff --git a/src/periphery/contracts/static-a-token/interfaces/IInitializableStaticATokenLM.sol b/src/periphery/contracts/static-a-token/interfaces/IInitializableStaticATokenLM.sol deleted file mode 100644 index 0eeb8955..00000000 --- a/src/periphery/contracts/static-a-token/interfaces/IInitializableStaticATokenLM.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; - -import {IPool} from '../../../../core/contracts/interfaces/IPool.sol'; -import {IAaveIncentivesController} from '../../../../core/contracts/interfaces/IAaveIncentivesController.sol'; - -/** - * @title IInitializableStaticATokenLM - * @notice Interface for the initialize function on StaticATokenLM - * @author Aave - **/ -interface IInitializableStaticATokenLM { - /** - * @dev Emitted when a StaticATokenLM is initialized - * @param aToken The address of the underlying aToken (aWETH) - * @param staticATokenName The name of the Static aToken - * @param staticATokenSymbol The symbol of the Static aToken - **/ - event Initialized(address indexed aToken, string staticATokenName, string staticATokenSymbol); - - /** - * @dev Initializes the StaticATokenLM - * @param aToken The address of the underlying aToken (aWETH) - * @param staticATokenName The name of the Static aToken - * @param staticATokenSymbol The symbol of the Static aToken - */ - function initialize( - address aToken, - string calldata staticATokenName, - string calldata staticATokenSymbol - ) external; -} diff --git a/src/periphery/contracts/static-a-token/interfaces/IStataOracle.sol b/src/periphery/contracts/static-a-token/interfaces/IStataOracle.sol deleted file mode 100644 index acd4fc4f..00000000 --- a/src/periphery/contracts/static-a-token/interfaces/IStataOracle.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; - -import {IPool} from '../../../../core/contracts/interfaces/IPool.sol'; -import {IAaveOracle} from '../../../../core/contracts/interfaces/IAaveOracle.sol'; - -interface IStataOracle { - /** - * @return The pool used for fetching the rate on the aggregator oracle - */ - function POOL() external view returns (IPool); - - /** - * @return The aave oracle used for fetching the price of the underlying - */ - function AAVE_ORACLE() external view returns (IAaveOracle); - - /** - * @notice Returns the prices of an asset address - * @param asset The asset address - * @return The prices of the given asset - */ - function getAssetPrice(address asset) external view returns (uint256); - - /** - * @notice Returns a list of prices from a list of assets addresses - * @param assets The list of assets addresses - * @return The prices of the given assets - */ - function getAssetsPrices(address[] calldata assets) external view returns (uint256[] memory); -} diff --git a/src/periphery/contracts/static-a-token/interfaces/IStataTokenFactory.sol b/src/periphery/contracts/static-a-token/interfaces/IStataTokenFactory.sol new file mode 100644 index 00000000..2eaf187b --- /dev/null +++ b/src/periphery/contracts/static-a-token/interfaces/IStataTokenFactory.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +interface IStataTokenFactory { + error NotListedUnderlying(address underlying); + + /** + * @notice Creates new StataTokens + * @param underlyings the addresses of the underlyings to create. + * @return address[] addresses of the new StataTokens. + */ + function createStataTokens(address[] memory underlyings) external returns (address[] memory); + + /** + * @notice Returns all StataTokens deployed via this registry. + * @return address[] list of StataTokens + */ + function getStataTokens() external view returns (address[] memory); + + /** + * @notice Returns the StataToken for a given underlying. + * @param underlying the address of the underlying. + * @return address the StataToken address. + */ + function getStataToken(address underlying) external view returns (address); +} diff --git a/src/periphery/contracts/static-a-token/interfaces/IStataTokenV2.sol b/src/periphery/contracts/static-a-token/interfaces/IStataTokenV2.sol new file mode 100644 index 00000000..6c5227a8 --- /dev/null +++ b/src/periphery/contracts/static-a-token/interfaces/IStataTokenV2.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC4626StataToken} from './IERC4626StataToken.sol'; +import {IERC20AaveLM} from './IERC20AaveLM.sol'; + +interface IStataTokenV2 is IERC4626StataToken, IERC20AaveLM { + /** + * @notice Checks if the passed actor is permissioned emergency admin. + * @param actor The reward to claim + * @return bool signaling if actor can pause the vault. + */ + function canPause(address actor) external view returns (bool); + + /** + * @notice Pauses/unpauses all system's operations + * @param paused boolean determining if the token should be paused or unpaused + */ + function setPaused(bool paused) external; +} diff --git a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenFactory.sol b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenFactory.sol deleted file mode 100644 index 7532e92c..00000000 --- a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenFactory.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; - -interface IStaticATokenFactory { - /** - * @notice Creates new staticATokens - * @param underlyings the addresses of the underlyings to create. - * @return address[] addresses of the new staticATokens. - */ - function createStaticATokens(address[] memory underlyings) external returns (address[] memory); - - /** - * @notice Returns all tokens deployed via this registry. - * @return address[] list of tokens - */ - function getStaticATokens() external view returns (address[] memory); - - /** - * @notice Returns the staticAToken for a given underlying. - * @param underlying the address of the underlying. - * @return address the staticAToken address. - */ - function getStaticAToken(address underlying) external view returns (address); -} diff --git a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol deleted file mode 100644 index eed469f3..00000000 --- a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol +++ /dev/null @@ -1,214 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; - -import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; -import {IInitializableStaticATokenLM} from './IInitializableStaticATokenLM.sol'; - -interface IStaticATokenLM is IInitializableStaticATokenLM { - struct SignatureParams { - uint8 v; - bytes32 r; - bytes32 s; - } - - struct PermitParams { - address owner; - address spender; - uint256 value; - uint256 deadline; - uint8 v; - bytes32 r; - bytes32 s; - } - - struct UserRewardsData { - uint128 rewardsIndexOnLastInteraction; // (in RAYs) - uint128 unclaimedRewards; // (in RAYs) - } - - struct RewardIndexCache { - bool isRegistered; - uint248 lastUpdatedIndex; - } - - event RewardTokenRegistered(address indexed reward, uint256 startIndex); - - /** - * @notice Burns `amount` of static aToken, with receiver receiving the corresponding amount of `ASSET` - * @param shares The amount to withdraw, in static balance of StaticAToken - * @param receiver The address that will receive the amount of `ASSET` withdrawn from the Aave protocol - * @param withdrawFromAave bool - * - `true` for the receiver to get underlying tokens (e.g. USDC) - * - `false` for the receiver to get aTokens (e.g. aUSDC) - * @return amountToBurn: StaticATokens burnt, static balance - * @return amountToWithdraw: underlying/aToken send to `receiver`, dynamic balance - **/ - function redeem( - uint256 shares, - address receiver, - address owner, - bool withdrawFromAave - ) external returns (uint256, uint256); - - /** - * @notice Deposits `ASSET` in the Aave protocol and mints static aTokens to msg.sender - * @param assets The amount of underlying `ASSET` to deposit (e.g. deposit of 100 USDC) - * @param receiver The address that will receive the static aTokens - * @param referralCode Code used to register the integrator originating the operation, for potential rewards. - * 0 if the action is executed directly by the user, without any middle-man - * @param depositToAave bool - * - `true` if the msg.sender comes with underlying tokens (e.g. USDC) - * - `false` if the msg.sender comes already with aTokens (e.g. aUSDC) - * @return uint256 The amount of StaticAToken minted, static balance - **/ - function deposit( - uint256 assets, - address receiver, - uint16 referralCode, - bool depositToAave - ) external returns (uint256); - - /** - * @notice Allows to deposit on Aave via meta-transaction - * https://github.com/ethereum/EIPs/blob/8a34d644aacf0f9f8f00815307fd7dd5da07655f/EIPS/eip-2612.md - * @param depositor Address from which the funds to deposit are going to be pulled - * @param receiver Address that will receive the staticATokens, in the average case, same as the `depositor` - * @param assets The amount to deposit - * @param referralCode Code used to register the integrator originating the operation, for potential rewards. - * 0 if the action is executed directly by the user, without any middle-man - * @param depositToAave bool - * - `true` if the msg.sender comes with underlying tokens (e.g. USDC) - * - `false` if the msg.sender comes already with aTokens (e.g. aUSDC) - * @param deadline The deadline timestamp, type(uint256).max for max deadline - * @param sigParams Signature params: v,r,s - * @return uint256 The amount of StaticAToken minted, static balance - */ - function metaDeposit( - address depositor, - address receiver, - uint256 assets, - uint16 referralCode, - bool depositToAave, - uint256 deadline, - PermitParams calldata permit, - SignatureParams calldata sigParams - ) external returns (uint256); - - /** - * @notice Allows to withdraw from Aave via meta-transaction - * https://github.com/ethereum/EIPs/blob/8a34d644aacf0f9f8f00815307fd7dd5da07655f/EIPS/eip-2612.md - * @param owner Address owning the staticATokens - * @param receiver Address that will receive the underlying withdrawn from Aave - * @param shares The amount of staticAToken to withdraw. If > 0, `assets` needs to be 0 - * @param assets The amount of underlying/aToken to withdraw. If > 0, `shares` needs to be 0 - * @param withdrawFromAave bool - * - `true` for the receiver to get underlying tokens (e.g. USDC) - * - `false` for the receiver to get aTokens (e.g. aUSDC) - * @param deadline The deadline timestamp, type(uint256).max for max deadline - * @param sigParams Signature params: v,r,s - * @return amountToBurn: StaticATokens burnt, static balance - * @return amountToWithdraw: underlying/aToken send to `receiver`, dynamic balance - */ - function metaWithdraw( - address owner, - address receiver, - uint256 shares, - uint256 assets, - bool withdrawFromAave, - uint256 deadline, - SignatureParams calldata sigParams - ) external returns (uint256, uint256); - - /** - * @notice Returns the Aave liquidity index of the underlying aToken, denominated rate here - * as it can be considered as an ever-increasing exchange rate - * @return The liquidity index - **/ - function rate() external view returns (uint256); - - /** - * @notice Claims rewards from `INCENTIVES_CONTROLLER` and updates internal accounting of rewards. - * @param reward The reward to claim - * @return uint256 Amount collected - */ - function collectAndUpdateRewards(address reward) external returns (uint256); - - /** - * @notice Claim rewards on behalf of a user and send them to a receiver - * @dev Only callable by if sender is onBehalfOf or sender is approved claimer - * @param onBehalfOf The address to claim on behalf of - * @param receiver The address to receive the rewards - * @param rewards The rewards to claim - */ - function claimRewardsOnBehalf( - address onBehalfOf, - address receiver, - address[] memory rewards - ) external; - - /** - * @notice Claim rewards and send them to a receiver - * @param receiver The address to receive the rewards - * @param rewards The rewards to claim - */ - function claimRewards(address receiver, address[] memory rewards) external; - - /** - * @notice Claim rewards - * @param rewards The rewards to claim - */ - function claimRewardsToSelf(address[] memory rewards) external; - - /** - * @notice Get the total claimable rewards of the contract. - * @param reward The reward to claim - * @return uint256 The current balance + pending rewards from the `_incentivesController` - */ - function getTotalClaimableRewards(address reward) external view returns (uint256); - - /** - * @notice Get the total claimable rewards for a user in WAD - * @param user The address of the user - * @param reward The reward to claim - * @return uint256 The claimable amount of rewards in WAD - */ - function getClaimableRewards(address user, address reward) external view returns (uint256); - - /** - * @notice The unclaimed rewards for a user in WAD - * @param user The address of the user - * @param reward The reward to claim - * @return uint256 The unclaimed amount of rewards in WAD - */ - function getUnclaimedRewards(address user, address reward) external view returns (uint256); - - /** - * @notice The underlying asset reward index in RAY - * @param reward The reward to claim - * @return uint256 The underlying asset reward index in RAY - */ - function getCurrentRewardsIndex(address reward) external view returns (uint256); - - /** - * @notice The aToken used inside the 4626 vault. - * @return IERC20 The aToken IERC20. - */ - function aToken() external view returns (IERC20); - - /** - * @notice The IERC20s that are currently rewarded to addresses of the vault via LM on incentivescontroller. - * @return IERC20 The IERC20s of the rewards. - */ - function rewardTokens() external view returns (address[] memory); - - /** - * @notice Fetches all rewardTokens from the incentivecontroller and registers the missing ones. - */ - function refreshRewardTokens() external; - - /** - * @notice Checks if the passed token is a registered reward. - * @return bool signaling if token is a registered reward. - */ - function isRegisteredRewardToken(address reward) external view returns (bool); -} diff --git a/tests/DeploymentsGasLimits.t.sol b/tests/DeploymentsGasLimits.t.sol index 0b67a340..54d32c73 100644 --- a/tests/DeploymentsGasLimits.t.sol +++ b/tests/DeploymentsGasLimits.t.sol @@ -208,7 +208,7 @@ contract DeploymentsGasLimits is BatchTestProcedures { ); } - function testCheckInitCodeSizeBatchs() public view { + function testCheckInitCodeSizeBatchs() public pure { uint16 maxInitCodeSize = 49152; console.log('AaveV3SetupBatch', type(AaveV3SetupBatch).creationCode.length); diff --git a/tests/core/Pool.t.sol b/tests/core/Pool.t.sol index 7ae63020..3f982b2b 100644 --- a/tests/core/Pool.t.sol +++ b/tests/core/Pool.t.sol @@ -667,31 +667,31 @@ contract PoolTests is TestnetProcedures { assertEq(50_000e6, virtualBalance); } - function test_getFlashLoanLogic() public { + function test_getFlashLoanLogic() public view { assertNotEq(pool.getFlashLoanLogic(), address(0)); } - function test_getBorrowLogic() public { + function test_getBorrowLogic() public view { assertNotEq(pool.getBorrowLogic(), address(0)); } - function test_getBridgeLogic() public { + function test_getBridgeLogic() public view { assertNotEq(pool.getBridgeLogic(), address(0)); } - function test_getEModeLogic() public { + function test_getEModeLogic() public view { assertNotEq(pool.getEModeLogic(), address(0)); } - function test_getLiquidationLogic() public { + function test_getLiquidationLogic() public view { assertNotEq(pool.getLiquidationLogic(), address(0)); } - function test_getPoolLogic() public { + function test_getPoolLogic() public view { assertNotEq(pool.getPoolLogic(), address(0)); } - function test_getSupplyLogic() public { + function test_getSupplyLogic() public view { assertNotEq(pool.getSupplyLogic(), address(0)); } diff --git a/tests/core/PoolConfigurator.upgradeabilty.t.sol b/tests/core/PoolConfigurator.upgradeabilty.t.sol index 2c84f556..840a4fff 100644 --- a/tests/core/PoolConfigurator.upgradeabilty.t.sol +++ b/tests/core/PoolConfigurator.upgradeabilty.t.sol @@ -53,7 +53,7 @@ contract PoolConfiguratorUpgradeabilityTests is TestnetProcedures { initTestEnvironment(); } - function test_getConfiguratorLogic() public { + function test_getConfiguratorLogic() public view { assertNotEq(contracts.poolConfiguratorProxy.getConfiguratorLogic(), address(0)); } diff --git a/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol b/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol new file mode 100644 index 00000000..f831be23 --- /dev/null +++ b/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IERC20Errors} from 'openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {TestnetProcedures, TestnetERC20} from '../../utils/TestnetProcedures.sol'; +import {ERC20AaveLMUpgradeable, IERC20AaveLM} from '../../../src/periphery/contracts/static-a-token/ERC20AaveLMUpgradeable.sol'; +import {IRewardsController} from '../../../src/periphery/contracts/rewards/interfaces/IRewardsController.sol'; +import {PullRewardsTransferStrategy, ITransferStrategyBase} from '../../../src/periphery/contracts/rewards/transfer-strategies/PullRewardsTransferStrategy.sol'; +import {RewardsDataTypes} from '../../../src/periphery/contracts/rewards/libraries/RewardsDataTypes.sol'; +import {IEACAggregatorProxy} from '../../../src/periphery/contracts/misc/interfaces/IEACAggregatorProxy.sol'; +import {DataTypes} from '../../../src/core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; + +// Minimal mock as contract is abstract +contract MockERC20AaveLMUpgradeable is ERC20AaveLMUpgradeable { + constructor(IRewardsController rewardsController) ERC20AaveLMUpgradeable(rewardsController) {} + + function mockInit(address asset) external initializer { + __ERC20AaveLM_init(asset); + } + + function mint(address user, uint256 amount) external { + _mint(user, amount); + } +} + +contract MockScaledTestnetERC20 is TestnetERC20 { + constructor( + string memory name, + string memory symbol, + uint8 decimals, + address owner + ) TestnetERC20(name, symbol, decimals, owner) {} + + function scaledTotalSupply() external view returns (uint256) { + return totalSupply(); + } + + function scaledBalanceOf(address user) external view returns (uint256) { + return balanceOf(user); + } + + function getScaledUserBalanceAndSupply(address user) external view returns (uint256, uint256) { + return (balanceOf(user), totalSupply()); + } + + function mint(address user, uint256 amount) public override returns (bool) { + _mint(user, amount); + return true; + } +} + +contract ERC20AaveLMUpgradableTest is TestnetProcedures { + MockERC20AaveLMUpgradeable internal lmUpgradeable; + MockScaledTestnetERC20 internal underlying; + + address public user; + uint256 internal userPrivateKey; + + address internal rewardToken; + address internal emissionAdmin; + PullRewardsTransferStrategy strategy; + + function setUp() public virtual { + initTestEnvironment(false); + + emissionAdmin = vm.addr(1024); + + userPrivateKey = 0xA11CE; + user = address(vm.addr(userPrivateKey)); + + underlying = new MockScaledTestnetERC20('Mock underlying', 'UND', 18, poolAdmin); + + lmUpgradeable = new MockERC20AaveLMUpgradeable(contracts.rewardsControllerProxy); + lmUpgradeable.mockInit(address(underlying)); + + rewardToken = address(new TestnetERC20('LM Reward ERC20', 'RWD', 18, poolAdmin)); + strategy = new PullRewardsTransferStrategy( + report.rewardsControllerProxy, + emissionAdmin, + emissionAdmin + ); + + vm.prank(poolAdmin); + contracts.emissionManager.setEmissionAdmin(rewardToken, emissionAdmin); + } + + function test_7201() external pure { + assertEq( + keccak256(abi.encode(uint256(keccak256('aave-dao.storage.ERC20AaveLM')) - 1)) & + ~bytes32(uint256(0xff)), + 0x4fad66563f105be0bff96185c9058c4934b504d3ba15ca31e86294f0b01fd200 + ); + } + + function test_noRewardsInitialized() external { + vm.expectRevert( + abi.encodeWithSelector(IERC20AaveLM.RewardNotInitialized.selector, rewardToken) + ); + lmUpgradeable.getClaimableRewards(user, rewardToken); + } + + function test_noopWhenNotInitialized() external { + assertEq(IERC20(rewardToken).balanceOf(address(lmUpgradeable)), 0); + assertEq(lmUpgradeable.getTotalClaimableRewards(rewardToken), 0); + assertEq(lmUpgradeable.collectAndUpdateRewards(rewardToken), 0); + assertEq(IERC20(rewardToken).balanceOf(address(lmUpgradeable)), 0); + } + + function test_claimableRewards( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) public { + TestEnv memory env = _setupTestEnvironment( + depositAmount, + emissionEnd, + emissionPerSecond, + waitDuration + ); + + uint256 claimable = lmUpgradeable.getClaimableRewards(user, rewardToken); + assertLe(claimable, env.emissionDuration * env.emissionPerSecond); + } + + function test_collectAndUpdateRewards( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) public { + _setupTestEnvironment(depositAmount, emissionEnd, emissionPerSecond, waitDuration); + + assertEq(IERC20(rewardToken).balanceOf(address(lmUpgradeable)), 0); + uint256 claimable = lmUpgradeable.getTotalClaimableRewards(rewardToken); + lmUpgradeable.collectAndUpdateRewards(rewardToken); + assertEq(IERC20(rewardToken).balanceOf(address(lmUpgradeable)), claimable); + } + + function test_claimRewards( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) public { + _setupTestEnvironment(depositAmount, emissionEnd, emissionPerSecond, waitDuration); + + uint256 claimable = lmUpgradeable.getClaimableRewards(user, rewardToken); + vm.prank(user); + lmUpgradeable.claimRewards(address(this), _getRewardTokens()); + assertEq(IERC20(rewardToken).balanceOf(address(this)), claimable); + assertEq(lmUpgradeable.getClaimableRewards(user, rewardToken), 0); + } + + function test_claimRewardsToSelf( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) public { + _setupTestEnvironment(depositAmount, emissionEnd, emissionPerSecond, waitDuration); + + uint256 claimable = lmUpgradeable.getClaimableRewards(user, rewardToken); + vm.prank(user); + lmUpgradeable.claimRewardsToSelf(_getRewardTokens()); + assertEq(IERC20(rewardToken).balanceOf(user), claimable); + assertEq(lmUpgradeable.getClaimableRewards(user, rewardToken), 0); + } + + function test_claimRewardsOnBehalfOf_shouldRevertForInvalidClaimer( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) external { + _setupTestEnvironment(depositAmount, emissionEnd, emissionPerSecond, waitDuration); + + vm.expectRevert(abi.encodeWithSelector(IERC20AaveLM.InvalidClaimer.selector, address(this))); + lmUpgradeable.claimRewardsOnBehalf(user, address(this), _getRewardTokens()); + } + + function test_claimRewardsOnBehalfOf_self( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) external { + _setupTestEnvironment(depositAmount, emissionEnd, emissionPerSecond, waitDuration); + + uint256 claimable = lmUpgradeable.getClaimableRewards(user, rewardToken); + vm.prank(user); + lmUpgradeable.claimRewardsOnBehalf(user, address(this), _getRewardTokens()); + assertEq(IERC20(rewardToken).balanceOf(address(this)), claimable); + assertEq(lmUpgradeable.getClaimableRewards(user, rewardToken), 0); + } + + function test_claimRewardsOnBehalfOf_validClaimer( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) external { + _setupTestEnvironment(depositAmount, emissionEnd, emissionPerSecond, waitDuration); + + vm.prank(poolAdmin); + contracts.emissionManager.setClaimer(user, address(this)); + + uint256 claimable = lmUpgradeable.getClaimableRewards(user, rewardToken); + lmUpgradeable.claimRewardsOnBehalf(user, address(this), _getRewardTokens()); + assertEq(IERC20(rewardToken).balanceOf(address(this)), claimable); + assertEq(lmUpgradeable.getClaimableRewards(user, rewardToken), 0); + } + + function test_transfer_toSelf( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) external { + TestEnv memory env = _setupTestEnvironment( + depositAmount, + emissionEnd, + emissionPerSecond, + waitDuration + ); + + uint256 claimableBefore = lmUpgradeable.getClaimableRewards(user, rewardToken); + assertEq(lmUpgradeable.getUnclaimedRewards(user, rewardToken), 0); + vm.prank(user); + lmUpgradeable.transfer(user, env.depositAmount); + uint256 claimableAfter = lmUpgradeable.getClaimableRewards(user, rewardToken); + assertEq(lmUpgradeable.getUnclaimedRewards(user, rewardToken), claimableAfter); + assertEq(claimableBefore, claimableAfter); + } + + function test_transfer( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration, + address receiver, + uint256 sendAmount + ) external { + vm.assume(user != receiver && receiver != address(0)); + TestEnv memory env = _setupTestEnvironment( + depositAmount, + emissionEnd, + emissionPerSecond, + waitDuration + ); + + if (sendAmount > env.depositAmount) { + vm.expectRevert( + abi.encodeWithSelector( + IERC20Errors.ERC20InsufficientBalance.selector, + user, + env.depositAmount, + sendAmount + ) + ); + vm.prank(user); + lmUpgradeable.transfer(receiver, sendAmount); + } else { + _fund(env.depositAmount, receiver); + assertEq(lmUpgradeable.getUnclaimedRewards(user, rewardToken), 0); + assertEq(lmUpgradeable.getUnclaimedRewards(receiver, rewardToken), 0); + + uint256 senderClaimableBefore = lmUpgradeable.getClaimableRewards(user, rewardToken); + uint256 receiverClaimableBefore = lmUpgradeable.getClaimableRewards(receiver, rewardToken); + + vm.prank(user); + lmUpgradeable.transfer(receiver, sendAmount); + // rewards should remain the same, but move to unclaimed + assertEq(lmUpgradeable.getUnclaimedRewards(user, rewardToken), senderClaimableBefore); + assertEq(lmUpgradeable.getClaimableRewards(user, rewardToken), senderClaimableBefore); + assertEq(lmUpgradeable.getUnclaimedRewards(receiver, rewardToken), receiverClaimableBefore); + assertEq(lmUpgradeable.getClaimableRewards(receiver, rewardToken), receiverClaimableBefore); + } + } + + function test_isRegisteredRewardToken() external { + assertEq(lmUpgradeable.isRegisteredRewardToken(rewardToken), false); + _setupEmission(uint32(block.timestamp), 0); + assertEq(lmUpgradeable.isRegisteredRewardToken(rewardToken), false); + lmUpgradeable.refreshRewardTokens(); + assertEq(lmUpgradeable.isRegisteredRewardToken(rewardToken), true); + } + + function test_getReferenceAsset() external view { + address ref = lmUpgradeable.getReferenceAsset(); + assertEq(ref, address(underlying)); + } + + function test_rewardTokens() external { + _setupEmission(uint32(block.timestamp), 0); + lmUpgradeable.refreshRewardTokens(); + address[] memory assets = lmUpgradeable.rewardTokens(); + assertEq(assets.length, 1); + assertEq(assets[0], rewardToken); + } + + function test_correctAccountingForDelayedRegistration() external { + address earlyDepositor = address(0xB0B); + _fund(1 ether, earlyDepositor); + _setupEmission(uint32(block.timestamp + 2 days), 1 ether); + + vm.warp(block.timestamp + 1 days); + _fund(1 ether, user); + lmUpgradeable.refreshRewardTokens(); + // as the rewards were not tracked before they should be zero + assertEq(lmUpgradeable.getClaimableRewards(earlyDepositor, rewardToken), 0); + assertEq(lmUpgradeable.getClaimableRewards(user, rewardToken), 0); + + vm.warp(block.timestamp + 3 days); + uint256 claimableBob = lmUpgradeable.getClaimableRewards(earlyDepositor, rewardToken); + uint256 claimableUser = lmUpgradeable.getClaimableRewards(user, rewardToken); + assertEq(claimableBob, claimableUser); + assertEq(claimableBob + claimableUser, 1 days * 1 ether); + } + + // ### INTERNAL HELPER FUNCTIONS ### + struct TestEnv { + // @notice the amount deposited + uint256 depositAmount; + // @notice the timestamp at which emission stops + uint32 emissionEnd; + // @notice emission per second + uint88 emissionPerSecond; + // @notice the duration of emissions in the test environment (time passed) + uint32 emissionDuration; + } + + function _setupTestEnvironment( + uint256 depositAmount, + uint32 emissionEnd, + uint88 emissionPerSecond, + uint32 waitDuration + ) internal returns (TestEnv memory) { + TestEnv memory env; + env.depositAmount = bound(depositAmount, 1 ether, type(uint96).max); + env.emissionEnd = uint32(bound(emissionEnd, block.timestamp, 365 days * 100)); + uint32 endTimestamp = uint32(bound(waitDuration, block.timestamp, 365 days * 100)); + env.emissionDuration = env.emissionEnd > endTimestamp + ? endTimestamp - uint32(block.timestamp) + : env.emissionEnd - uint32(block.timestamp); + env.emissionPerSecond = uint88( + bound( + emissionPerSecond, + 0, + env.emissionDuration > 0 ? type(uint88).max / env.emissionDuration : type(uint88).max + ) + ); + _setupEmission(env.emissionEnd, env.emissionPerSecond); + lmUpgradeable.refreshRewardTokens(); + _fund(env.depositAmount, user); + + vm.warp(endTimestamp); + + return env; + } + + function _getRewardTokens() internal view returns (address[] memory) { + address[] memory rewardTokens = new address[](1); + rewardTokens[0] = rewardToken; + return rewardTokens; + } + + function _setupEmission(uint32 emissionEnd, uint88 emissionPerSecond) internal { + RewardsDataTypes.RewardsConfigInput[] memory config = new RewardsDataTypes.RewardsConfigInput[]( + 1 + ); + config[0] = RewardsDataTypes.RewardsConfigInput( + emissionPerSecond, + 0, // totalSupply is overwritten internally + emissionEnd, + address(underlying), + rewardToken, + ITransferStrategyBase(strategy), + IEACAggregatorProxy(address(2)) + ); + + // configure asset + vm.prank(emissionAdmin); + contracts.emissionManager.configureAssets(config); + + // fund admin & approve transfers to allow claiming + uint256 fundsToEmit = (emissionEnd - block.timestamp) * emissionPerSecond; + deal(rewardToken, emissionAdmin, fundsToEmit, true); + vm.prank(emissionAdmin); + IERC20(rewardToken).approve(address(strategy), fundsToEmit); + } + + /** + * @dev funds the given user with the lm token and updates total supply. + * Maintains consistency by also funding the underlying to the lmUpgradeable + */ + function _fund(uint256 amount, address receiver) internal { + underlying.mint(receiver, amount); + lmUpgradeable.mint(receiver, amount); + vm.prank(receiver); + underlying.transfer(address(lmUpgradeable), amount); + } +} diff --git a/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol b/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol new file mode 100644 index 00000000..714d1407 --- /dev/null +++ b/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol @@ -0,0 +1,478 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IERC20Errors} from 'openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {IERC20Permit} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol'; +import {IPool} from '../../../src/core/contracts/interfaces/IPool.sol'; +import {TestnetProcedures, TestnetERC20} from '../../utils/TestnetProcedures.sol'; +import {ERC4626Upgradeable, ERC4626StataTokenUpgradeable, IERC4626StataToken} from '../../../src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol'; +import {DataTypes} from '../../../src/core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; +import {SigUtils} from '../../utils/SigUtils.sol'; + +// Minimal mock as contract is abstract +contract MockERC4626StataTokenUpgradeable is ERC4626StataTokenUpgradeable { + constructor(IPool pool) ERC4626StataTokenUpgradeable(pool) {} + + function mockInit(address aToken) external initializer { + __ERC4626StataToken_init(aToken); + } +} + +contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { + MockERC4626StataTokenUpgradeable internal erc4626Upgradeable; + address internal underlying; + address internal aToken; + + address public user; + uint256 internal userPrivateKey; + + function setUp() public virtual { + initTestEnvironment(false); + + userPrivateKey = 0xA11CE; + user = address(vm.addr(userPrivateKey)); + + DataTypes.ReserveDataLegacy memory reserveData = contracts.poolProxy.getReserveData( + tokenList.usdx + ); + underlying = address(tokenList.usdx); + aToken = reserveData.aTokenAddress; + erc4626Upgradeable = new MockERC4626StataTokenUpgradeable(contracts.poolProxy); + erc4626Upgradeable.mockInit(address(reserveData.aTokenAddress)); + } + + function test_7201() external pure { + assertEq( + keccak256(abi.encode(uint256(keccak256('aave-dao.storage.ERC4626StataToken')) - 1)) & + ~bytes32(uint256(0xff)), + 0x55029d3f54709e547ed74b2fc842d93107ab1490ab7555dd9dd0bf6451101900 + ); + } + + // ### GETTERS TESTS ### + function test_convertersAndPreviews(uint128 assets) public view { + uint256 shares = erc4626Upgradeable.convertToShares(assets); + assertEq(shares, erc4626Upgradeable.previewDeposit(assets)); + assertEq(shares, erc4626Upgradeable.previewWithdraw(assets)); + assertEq(erc4626Upgradeable.convertToAssets(shares), assets); + assertEq(erc4626Upgradeable.previewMint(shares), assets); + assertEq(erc4626Upgradeable.previewRedeem(shares), assets); + } + + // ### DEPOSIT TESTS ### + function test_depositATokens(uint128 assets, address receiver) public { + _validateReceiver(receiver); + TestEnv memory env = _setupTestEnv(assets); + _fundAToken(env.amount, user); + + vm.startPrank(user); + IERC20(aToken).approve(address(erc4626Upgradeable), env.amount); + uint256 shares = erc4626Upgradeable.depositATokens(env.amount, receiver); + vm.stopPrank(); + + assertEq(erc4626Upgradeable.balanceOf(receiver), shares); + assertEq(IERC20(aToken).balanceOf(address(erc4626Upgradeable)), env.amount); + assertEq(IERC20(aToken).balanceOf(user), 0); + assertEq(erc4626Upgradeable.totalAssets(), env.amount); + } + + function test_depositATokens_self() external { + test_depositATokens(1 ether, user); + } + + function test_deposit_shouldRevert_insufficientAllowance(uint128 assets) external { + TestEnv memory env = _setupTestEnv(assets); + _fundAToken(env.amount, user); + + vm.expectRevert(); // underflows + vm.prank(user); + erc4626Upgradeable.depositATokens(env.amount, user); + } + + function test_depositWithPermit_shouldRevert_emptyPermit_noPreApproval(uint128 assets) external { + TestEnv memory env = _setupTestEnv(assets); + _fundAToken(env.amount, user); + IERC4626StataToken.SignatureParams memory sig; + vm.expectRevert(); // will underflow + vm.prank(user); + erc4626Upgradeable.depositWithPermit(env.amount, user, block.timestamp + 1000, sig, false); + } + + function test_depositWithPermit_emptyPermit_underlying_preApproval( + uint128 assets, + address receiver + ) external { + _validateReceiver(receiver); + TestEnv memory env = _setupTestEnv(assets); + _fundUnderlying(env.amount, user); + IERC4626StataToken.SignatureParams memory sig; + vm.prank(user); + IERC20(underlying).approve(address(erc4626Upgradeable), env.amount); + vm.prank(user); + uint256 shares = erc4626Upgradeable.depositWithPermit( + env.amount, + receiver, + block.timestamp + 1000, + sig, + true + ); + + assertEq(erc4626Upgradeable.balanceOf(receiver), shares); + assertEq(IERC20(aToken).balanceOf(address(erc4626Upgradeable)), env.amount); + assertEq(IERC20(aToken).balanceOf(user), 0); + } + + function test_depositWithPermit_emptyPermit_aToken_preApproval( + uint128 assets, + address receiver + ) external { + _validateReceiver(receiver); + TestEnv memory env = _setupTestEnv(assets); + _fundAToken(env.amount, user); + IERC4626StataToken.SignatureParams memory sig; + vm.prank(user); + IERC20(aToken).approve(address(erc4626Upgradeable), env.amount); + vm.prank(user); + uint256 shares = erc4626Upgradeable.depositWithPermit( + env.amount, + receiver, + block.timestamp + 1000, + sig, + false + ); + + assertEq(erc4626Upgradeable.balanceOf(receiver), shares); + assertEq(IERC20(aToken).balanceOf(address(erc4626Upgradeable)), env.amount); + assertEq(IERC20(aToken).balanceOf(user), 0); + } + + function test_depositWithPermit_underlying(uint128 assets, address receiver) external { + _validateReceiver(receiver); + TestEnv memory env = _setupTestEnv(assets); + _fundUnderlying(env.amount, user); + + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: user, + spender: address(erc4626Upgradeable), + value: env.amount, + nonce: IERC20Permit(underlying).nonces(user), + deadline: block.timestamp + 100 + }); + + bytes32 permitDigest = SigUtils.getTypedDataHash( + permit, + SigUtils.PERMIT_TYPEHASH, + IERC20Permit(underlying).DOMAIN_SEPARATOR() + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); + IERC4626StataToken.SignatureParams memory sig = IERC4626StataToken.SignatureParams(v, r, s); + vm.prank(user); + uint256 shares = erc4626Upgradeable.depositWithPermit( + env.amount, + receiver, + permit.deadline, + sig, + true + ); + + assertEq(erc4626Upgradeable.balanceOf(receiver), shares); + assertEq(IERC20(aToken).balanceOf(address(erc4626Upgradeable)), env.amount); + assertEq(IERC20(underlying).balanceOf(user), 0); + } + + function test_depositWithPermit_aToken(uint128 assets, address receiver) external { + _validateReceiver(receiver); + TestEnv memory env = _setupTestEnv(assets); + _fundAToken(env.amount, user); + + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: user, + spender: address(erc4626Upgradeable), + value: env.amount, + nonce: IERC20Permit(aToken).nonces(user), + deadline: block.timestamp + 100 + }); + + bytes32 permitDigest = SigUtils.getTypedDataHash( + permit, + SigUtils.PERMIT_TYPEHASH, + IERC20Permit(aToken).DOMAIN_SEPARATOR() + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); + IERC4626StataToken.SignatureParams memory sig = IERC4626StataToken.SignatureParams(v, r, s); + vm.prank(user); + uint256 shares = erc4626Upgradeable.depositWithPermit( + env.amount, + receiver, + permit.deadline, + sig, + false + ); + + assertEq(erc4626Upgradeable.balanceOf(receiver), shares); + assertEq(IERC20(aToken).balanceOf(address(erc4626Upgradeable)), env.amount); + assertEq(IERC20(aToken).balanceOf(user), 0); + } + + // ### REDEEM TESTS ### + function test_redeemATokens(uint256 assets, address receiver) public { + _validateReceiver(receiver); + TestEnv memory env = _setupTestEnv(assets); + uint256 shares = _fund4626(env.amount, user); + + vm.prank(user); + uint256 redeemedAssets = erc4626Upgradeable.redeemATokens(shares, receiver, user); + + assertEq(erc4626Upgradeable.balanceOf(user), 0); + assertEq(IERC20(aToken).balanceOf(receiver), redeemedAssets); + } + + function test_redeemATokens_onBehalf_shouldRevert_insufficientAllowance( + uint256 assets, + uint256 allowance + ) external { + TestEnv memory env = _setupTestEnv(assets); + uint256 shares = _fund4626(env.amount, user); + + allowance = bound(allowance, 0, shares - 1); + vm.prank(user); + erc4626Upgradeable.approve(address(this), allowance); + + vm.expectRevert( + abi.encodeWithSelector( + IERC20Errors.ERC20InsufficientAllowance.selector, + address(this), + allowance, + env.amount + ) + ); + erc4626Upgradeable.redeemATokens(env.amount, address(this), user); + } + + function test_redeemATokens_onBehalf(uint256 assets) external { + TestEnv memory env = _setupTestEnv(assets); + uint256 shares = _fund4626(env.amount, user); + + vm.prank(user); + erc4626Upgradeable.approve(address(this), shares); + uint256 redeemedAssets = erc4626Upgradeable.redeemATokens(shares, address(this), user); + + assertEq(erc4626Upgradeable.balanceOf(user), 0); + assertEq(IERC20(aToken).balanceOf(address(this)), redeemedAssets); + } + + function test_redeem(uint256 assets, address receiver) external { + _validateReceiver(receiver); + TestEnv memory env = _setupTestEnv(assets); + uint256 shares = _fund4626(env.amount, user); + + vm.prank(user); + uint256 redeemedAssets = erc4626Upgradeable.redeem(shares, receiver, user); + assertEq(erc4626Upgradeable.balanceOf(user), 0); + assertLe(IERC20(underlying).balanceOf(receiver), redeemedAssets); + } + + // ### withdraw TESTS ### + function test_withdraw(uint256 assets, address receiver) public { + _validateReceiver(receiver); + TestEnv memory env = _setupTestEnv(assets); + uint256 shares = _fund4626(env.amount, user); + + vm.prank(user); + uint256 withdrawnShares = erc4626Upgradeable.withdraw(env.amount, receiver, user); + assertEq(withdrawnShares, shares); + assertEq(erc4626Upgradeable.balanceOf(user), 0); + assertLe(IERC20(underlying).balanceOf(receiver), env.amount); + assertApproxEqAbs(IERC20(underlying).balanceOf(receiver), env.amount, 1); + } + + function test_withdraw_shouldRevert_moreThenAvailable(uint256 assets, address receiver) public { + _validateReceiver(receiver); + TestEnv memory env = _setupTestEnv(assets); + _fund4626(env.amount, user); + + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector( + ERC4626Upgradeable.ERC4626ExceededMaxWithdraw.selector, + address(user), + env.amount + 1, + env.amount + ) + ); + erc4626Upgradeable.withdraw(env.amount + 1, receiver, user); + } + + // ### mint TESTS ### + function test_mint(uint256 assets, address receiver) public { + _validateReceiver(receiver); + TestEnv memory env = _setupTestEnv(assets); + _fundUnderlying(env.amount, user); + + vm.startPrank(user); + IERC20(underlying).approve(address(erc4626Upgradeable), env.amount); + uint256 shares = erc4626Upgradeable.previewDeposit(env.amount); + uint256 assetsUsedForMinting = erc4626Upgradeable.mint(shares, receiver); + assertEq(assetsUsedForMinting, env.amount); + assertEq(erc4626Upgradeable.balanceOf(receiver), shares); + } + + function test_mint_shouldRevert_mintMoreThenBalance(uint256 assets, address receiver) public { + _validateReceiver(receiver); + TestEnv memory env = _setupTestEnv(assets); + _fundUnderlying(env.amount, user); + + vm.startPrank(user); + IERC20(underlying).approve(address(erc4626Upgradeable), type(uint256).max); + uint256 shares = erc4626Upgradeable.previewDeposit(env.amount); + + vm.expectRevert(); + erc4626Upgradeable.mint(shares + 1, receiver); + } + + // ### maxDeposit TESTS ### + function test_maxDeposit_freeze() public { + vm.prank(roleList.marketOwner); + contracts.poolConfiguratorProxy.setReserveFreeze(underlying, true); + + uint256 max = erc4626Upgradeable.maxDeposit(address(0)); + + assertEq(max, 0); + } + + function test_maxDeposit_paused() public { + vm.prank(address(roleList.marketOwner)); + contracts.poolConfiguratorProxy.setReservePause(underlying, true); + + uint256 max = erc4626Upgradeable.maxDeposit(address(0)); + + assertEq(max, 0); + } + + function test_maxDeposit_noCap() public { + vm.prank(address(roleList.marketOwner)); + contracts.poolConfiguratorProxy.setSupplyCap(underlying, 0); + + uint256 maxDeposit = erc4626Upgradeable.maxDeposit(address(0)); + uint256 maxMint = erc4626Upgradeable.maxMint(address(0)); + + assertEq(maxDeposit, type(uint256).max); + assertEq(maxMint, type(uint256).max); + } + + function test_maxDeposit_cap(uint256 cap) public { + cap = bound(cap, 1, type(uint32).max); + vm.prank(address(roleList.marketOwner)); + contracts.poolConfiguratorProxy.setSupplyCap(underlying, cap); + + uint256 max = erc4626Upgradeable.maxDeposit(address(0)); + assertEq(max, cap * 10 ** erc4626Upgradeable.decimals()); + } + + // TODO: perhaps makes sense to add maxDeposit test with accruedToTreasury etc + + // ### maxRedeem TESTS ### + function test_maxRedeem_paused(uint128 assets) public { + TestEnv memory env = _setupTestEnv(assets); + _fund4626(env.amount, user); + + vm.prank(address(roleList.marketOwner)); + contracts.poolConfiguratorProxy.setReservePause(underlying, true); + + uint256 max = erc4626Upgradeable.maxRedeem(address(user)); + + assertEq(max, 0); + } + + function test_maxRedeem_sufficientAvailableLiquidity(uint128 assets) public { + TestEnv memory env = _setupTestEnv(assets); + uint256 shares = _fund4626(env.amount, user); + + uint256 max = erc4626Upgradeable.maxRedeem(address(user)); + + assertEq(max, shares); + } + + function test_maxRedeem_inSufficientAvailableLiquidity(uint256 amountToBorrow) public { + uint128 assets = 1e8; + amountToBorrow = bound(amountToBorrow, 1, assets); + _fund4626(assets, user); + + // borrow out some assets + address borrowUser = address(99); + vm.startPrank(borrowUser); + deal(address(weth), borrowUser, 2_000 ether); + weth.approve(address(contracts.poolProxy), 2_000 ether); + contracts.poolProxy.deposit(address(weth), 2_000 ether, borrowUser, 0); + contracts.poolProxy.borrow(underlying, amountToBorrow, 2, 0, borrowUser); + + uint256 max = erc4626Upgradeable.maxRedeem(address(user)); + + assertEq(max, erc4626Upgradeable.previewRedeem(assets - amountToBorrow)); + } + + // ### lastestAnswer TESTS ### + function test_latestAnswer_priceShouldBeEqualOnDefaultIndex() public { + vm.mockCall( + address(contracts.poolProxy), + abi.encodeWithSelector(IPool.getReserveNormalizedIncome.selector), + abi.encode(1e27) + ); + uint256 stataPrice = uint256(erc4626Upgradeable.latestAnswer()); + uint256 underlyingPrice = contracts.aaveOracle.getAssetPrice(underlying); + assertEq(stataPrice, underlyingPrice); + } + + function test_latestAnswer_priceShouldReflectIndexAccrual(uint256 liquidityIndex) public { + liquidityIndex = bound(liquidityIndex, 1e27, 1e29); + vm.mockCall( + address(contracts.poolProxy), + abi.encodeWithSelector(IPool.getReserveNormalizedIncome.selector), + abi.encode(liquidityIndex) + ); + uint256 stataPrice = uint256(erc4626Upgradeable.latestAnswer()); + uint256 underlyingPrice = contracts.aaveOracle.getAssetPrice(underlying); + uint256 expectedStataPrice = (underlyingPrice * liquidityIndex) / 1e27; + assertEq(stataPrice, expectedStataPrice); + + // reverse the math to ensure precision loss is within bounds + uint256 reversedUnderlying = (stataPrice * 1e27) / liquidityIndex; + assertApproxEqAbs(underlyingPrice, reversedUnderlying, 1); + } + + struct TestEnv { + uint256 amount; + } + + function _validateReceiver(address receiver) internal view { + vm.assume(receiver != address(0) && receiver != address(aToken)); + } + + function _setupTestEnv(uint256 amount) internal pure returns (TestEnv memory) { + TestEnv memory env; + env.amount = bound(amount, 1, type(uint96).max); + return env; + } + + function _fundUnderlying(uint256 assets, address receiver) internal { + deal(underlying, receiver, assets); + } + + function _fundAToken(uint256 assets, address receiver) internal { + _fundUnderlying(assets, receiver); + vm.startPrank(receiver); + IERC20(underlying).approve(address(contracts.poolProxy), assets); + contracts.poolProxy.deposit(underlying, assets, receiver, 0); + vm.stopPrank(); + } + + function _fund4626(uint256 assets, address receiver) internal returns (uint256) { + _fundAToken(assets, receiver); + vm.startPrank(receiver); + IERC20(aToken).approve(address(erc4626Upgradeable), assets); + uint256 shares = erc4626Upgradeable.depositATokens(assets, receiver); + vm.stopPrank(); + return shares; + } +} diff --git a/tests/periphery/static-a-token/StataOracle.t.sol b/tests/periphery/static-a-token/StataOracle.t.sol deleted file mode 100644 index 3d6b6622..00000000 --- a/tests/periphery/static-a-token/StataOracle.t.sol +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -import {StataOracle} from '../../../src/periphery/contracts/static-a-token/StataOracle.sol'; -import {StaticATokenLM} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; -import {BaseTest} from './TestBase.sol'; - -contract StataOracleTest is BaseTest { - StataOracle public oracle; - - function setUp() public override { - super.setUp(); - oracle = new StataOracle(contracts.poolAddressesProvider); - - vm.prank(address(roleList.marketOwner)); - contracts.poolConfiguratorProxy.setSupplyCap(UNDERLYING, 1_000_000); - } - - function test_assetPrice() public view { - uint256 stataPrice = oracle.getAssetPrice(address(staticATokenLM)); - uint256 underlyingPrice = contracts.aaveOracle.getAssetPrice(UNDERLYING); - assertGe(stataPrice, underlyingPrice); - assertEq(stataPrice, (underlyingPrice * staticATokenLM.convertToAssets(1e18)) / 1e18); - } - - function test_assetsPrices() public view { - address[] memory staticATokens = factory.getStaticATokens(); - uint256[] memory stataPrices = oracle.getAssetsPrices(staticATokens); - - for (uint256 i = 0; i < staticATokens.length; i++) { - address staticAToken = staticATokens[i]; - uint256 stataPrice = stataPrices[i]; - - address underlying = StaticATokenLM(staticAToken).asset(); - uint256 underlyingPrice = contracts.aaveOracle.getAssetPrice(underlying); - - assertGe(stataPrice, underlyingPrice); - assertEq( - stataPrice, - (underlyingPrice * StaticATokenLM(staticAToken).convertToAssets(1e18)) / 1e18 - ); - } - } - - function test_error(uint256 shares) public view { - vm.assume(shares <= staticATokenLM.maxMint(address(0))); - uint256 pricePerShare = oracle.getAssetPrice(address(staticATokenLM)); - uint256 pricePerAsset = contracts.aaveOracle.getAssetPrice(UNDERLYING); - uint256 assets = staticATokenLM.convertToAssets(shares); - - assertApproxEqAbs( - (pricePerShare * shares) / 1e18, - (pricePerAsset * assets) / 1e18, - (assets / 1e18) + 1 // there can be imprecision of 1 wei, which will accumulate for each asset - ); - } -} diff --git a/tests/periphery/static-a-token/StataTokenV2Getters.sol b/tests/periphery/static-a-token/StataTokenV2Getters.sol new file mode 100644 index 00000000..c9a8aa1c --- /dev/null +++ b/tests/periphery/static-a-token/StataTokenV2Getters.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {Initializable} from 'openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol'; +import {IERC20Metadata, IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {AToken} from '../../../src/core/contracts/protocol/tokenization/AToken.sol'; +import {StataTokenV2} from '../../../src/periphery/contracts/static-a-token/StataTokenV2.sol'; // TODO: change import to isolate to 4626 +import {BaseTest} from './TestBase.sol'; + +contract StataTokenV2GettersTest is BaseTest { + function test_initializeShouldRevert() public { + address impl = factory.STATA_TOKEN_IMPL(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + StataTokenV2(impl).initialize(aToken, 'hey', 'ho'); + } + + function test_getters() public view { + assertEq(stataTokenV2.name(), 'Static Aave Local WETH v2'); + assertEq(stataTokenV2.symbol(), 'stataLocWETHv2'); + + address referenceAsset = stataTokenV2.getReferenceAsset(); + assertEq(referenceAsset, aToken); + + address underlyingAddress = address(stataTokenV2.asset()); + assertEq(underlyingAddress, underlying); + + IERC20Metadata underlying = IERC20Metadata(underlyingAddress); + assertEq(stataTokenV2.decimals(), underlying.decimals()); + + assertEq( + address(stataTokenV2.INCENTIVES_CONTROLLER()), + address(AToken(aToken).getIncentivesController()) + ); + } +} diff --git a/tests/periphery/static-a-token/StataTokenV2Pausable.t.sol b/tests/periphery/static-a-token/StataTokenV2Pausable.t.sol new file mode 100644 index 00000000..1eccc9df --- /dev/null +++ b/tests/periphery/static-a-token/StataTokenV2Pausable.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol'; +import {IERC20Metadata, IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {IERC4626StataToken} from '../../../src/periphery/contracts/static-a-token/interfaces/IERC4626StataToken.sol'; +import {BaseTest} from './TestBase.sol'; + +contract StataTokenV2PausableTest is BaseTest { + function test_canPause() external view { + assertEq(stataTokenV2.canPause(poolAdmin), true); + } + + function test_canPause_shouldReturnFalse(address actor) external view { + vm.assume(actor != poolAdmin); + assertEq(stataTokenV2.canPause(actor), false); + } + + function test_setPaused_shouldRevertForInvalidCaller(address actor) external { + vm.assume(actor != poolAdmin && actor != proxyAdmin); + vm.expectRevert(abi.encodeWithSelector(IERC4626StataToken.OnlyPauseGuardian.selector, actor)); + _setPaused(actor, true); + } + + function test_setPaused_shouldSucceedForOwner() external { + assertEq(PausableUpgradeable(address(stataTokenV2)).paused(), false); + _setPaused(poolAdmin, true); + assertEq(PausableUpgradeable(address(stataTokenV2)).paused(), true); + } + + function test_deposit_shouldRevert() external { + uint128 amountToDeposit = 5 ether; + _fundUnderlying(amountToDeposit, user); + vm.prank(user); + IERC20(underlying).approve(address(stataTokenV2), amountToDeposit); + + _setPausedAsAclAdmin(true); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + stataTokenV2.deposit(amountToDeposit, user); + } + + function test_mint_shouldRevert() external { + uint128 amountToDeposit = 5 ether; + _fundUnderlying(amountToDeposit, user); + vm.prank(user); + IERC20(underlying).approve(address(stataTokenV2), amountToDeposit); + + uint256 sharesToMint = stataTokenV2.previewDeposit(amountToDeposit); + _setPausedAsAclAdmin(true); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + stataTokenV2.mint(sharesToMint, user); + } + + function test_redeem_shouldRevert() external { + uint128 amountToDeposit = 5 ether; + _fund4626(amountToDeposit, user); + + assertEq(stataTokenV2.maxRedeem(user), stataTokenV2.balanceOf(user)); + + _setPausedAsAclAdmin(true); + uint256 maxRedeem = stataTokenV2.maxRedeem(user); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + stataTokenV2.redeem(maxRedeem, user, user); + } + + function test_withdraw_shouldRevert() external { + uint128 amountToDeposit = 5 ether; + _fund4626(amountToDeposit, user); + + uint256 maxWithdraw = stataTokenV2.maxWithdraw(user); + _setPausedAsAclAdmin(true); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + stataTokenV2.withdraw(maxWithdraw, user, user); + } + + function test_transfer_shouldRevert() external { + uint128 amountToDeposit = 10 ether; + _fund4626(amountToDeposit, user); + + _setPausedAsAclAdmin(true); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + stataTokenV2.transfer(user1, amountToDeposit); + } + + function test_claimingRewards_shouldRevert() external { + uint128 amountToDeposit = 10 ether; + _fund4626(amountToDeposit, user); + + _setPausedAsAclAdmin(true); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + stataTokenV2.claimRewardsToSelf(rewardTokens); + } + + function _setPausedAsAclAdmin(bool paused) internal { + _setPaused(poolAdmin, paused); + } + + function _setPaused(address actor, bool paused) internal { + vm.prank(actor); + stataTokenV2.setPaused(paused); + } +} diff --git a/tests/periphery/static-a-token/StataTokenV2Permit.sol b/tests/periphery/static-a-token/StataTokenV2Permit.sol new file mode 100644 index 00000000..d24b1ab7 --- /dev/null +++ b/tests/periphery/static-a-token/StataTokenV2Permit.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {ERC20PermitUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol'; +import {SigUtils} from '../../utils/SigUtils.sol'; +import {BaseTest} from './TestBase.sol'; + +contract StataTokenV2PermitTest is BaseTest { + function test_permit() public { + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: user, + spender: spender, + value: 1 ether, + nonce: stataTokenV2.nonces(user), + deadline: block.timestamp + 1 days + }); + + bytes32 permitDigest = SigUtils.getTypedDataHash( + permit, + SigUtils.PERMIT_TYPEHASH, + stataTokenV2.DOMAIN_SEPARATOR() + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); + + stataTokenV2.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); + + assertEq(stataTokenV2.allowance(permit.owner, spender), permit.value); + } + + function test_permit_expired() public { + // as the default timestamp is 0, we move ahead in time a bit + vm.warp(10 days); + + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: user, + spender: spender, + value: 1 ether, + nonce: stataTokenV2.nonces(user), + deadline: block.timestamp - 1 days + }); + + bytes32 permitDigest = SigUtils.getTypedDataHash( + permit, + SigUtils.PERMIT_TYPEHASH, + stataTokenV2.DOMAIN_SEPARATOR() + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); + + vm.expectRevert( + abi.encodeWithSelector( + ERC20PermitUpgradeable.ERC2612ExpiredSignature.selector, + permit.deadline + ) + ); + stataTokenV2.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); + } + + function test_permit_invalidSigner() public { + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: address(424242), + spender: spender, + value: 1 ether, + nonce: stataTokenV2.nonces(user), + deadline: block.timestamp + 1 days + }); + + bytes32 permitDigest = SigUtils.getTypedDataHash( + permit, + SigUtils.PERMIT_TYPEHASH, + stataTokenV2.DOMAIN_SEPARATOR() + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); + + vm.expectRevert( + abi.encodeWithSelector( + ERC20PermitUpgradeable.ERC2612InvalidSigner.selector, + user, + permit.owner + ) + ); + stataTokenV2.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); + } +} diff --git a/tests/periphery/static-a-token/StataTokenV2Rescuable.sol b/tests/periphery/static-a-token/StataTokenV2Rescuable.sol new file mode 100644 index 00000000..e43b14d8 --- /dev/null +++ b/tests/periphery/static-a-token/StataTokenV2Rescuable.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IRescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; +import {BaseTest} from './TestBase.sol'; + +contract StataTokenV2RescuableTest is BaseTest { + function test_whoCanRescue() external view { + assertEq(IRescuable(address(stataTokenV2)).whoCanRescue(), poolAdmin); + } + + function test_rescuable_shouldRevertForInvalidCaller() external { + deal(tokenList.usdx, address(stataTokenV2), 1 ether); + vm.expectRevert('ONLY_RESCUE_GUARDIAN'); + IRescuable(address(stataTokenV2)).emergencyTokenTransfer( + tokenList.usdx, + address(this), + 1 ether + ); + } + + function test_rescuable_shouldSuceedForOwner() external { + deal(tokenList.usdx, address(stataTokenV2), 1 ether); + vm.startPrank(poolAdmin); + IRescuable(address(stataTokenV2)).emergencyTokenTransfer( + tokenList.usdx, + address(this), + 1 ether + ); + } +} diff --git a/tests/periphery/static-a-token/StaticATokenLM.t.sol b/tests/periphery/static-a-token/StaticATokenLM.t.sol deleted file mode 100644 index 543a0c21..00000000 --- a/tests/periphery/static-a-token/StaticATokenLM.t.sol +++ /dev/null @@ -1,609 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -import {AToken} from '../../../src/core/contracts/protocol/tokenization/AToken.sol'; -import {DataTypes} from '../../../src/core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; -import {IERC20, IERC20Metadata} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; -import {RayMathExplicitRounding} from '../../../src/periphery/contracts/libraries/RayMathExplicitRounding.sol'; -import {PullRewardsTransferStrategy} from '../../../src/periphery/contracts/rewards/transfer-strategies/PullRewardsTransferStrategy.sol'; -import {RewardsDataTypes} from '../../../src/periphery/contracts/rewards/libraries/RewardsDataTypes.sol'; -import {ITransferStrategyBase} from '../../../src/periphery/contracts/rewards/interfaces/ITransferStrategyBase.sol'; -import {IEACAggregatorProxy} from '../../../src/periphery/contracts/misc/interfaces/IEACAggregatorProxy.sol'; -import {IStaticATokenLM} from '../../../src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol'; -import {SigUtils} from '../../utils/SigUtils.sol'; -import {BaseTest, TestnetERC20} from './TestBase.sol'; - -contract StaticATokenLMTest is BaseTest { - using RayMathExplicitRounding for uint256; - - address public constant EMISSION_ADMIN = address(25); - - function setUp() public override { - super.setUp(); - - _configureLM(); - _openSupplyAndBorrowPositions(); - - vm.startPrank(user); - } - - function test_initializeShouldRevert() public { - address impl = factory.STATIC_A_TOKEN_IMPL(); - vm.expectRevert(); - IStaticATokenLM(impl).initialize(0xe50fA9b3c56FfB159cB0FCA61F5c9D750e8128c8, 'hey', 'ho'); - } - - function test_getters() public view { - assertEq(staticATokenLM.name(), 'Static Aave Local WETH'); - assertEq(staticATokenLM.symbol(), 'stataLocWETH'); - - IERC20 aToken = staticATokenLM.aToken(); - assertEq(address(aToken), A_TOKEN); - - address underlyingAddress = address(staticATokenLM.asset()); - assertEq(underlyingAddress, UNDERLYING); - - IERC20Metadata underlying = IERC20Metadata(underlyingAddress); - assertEq(staticATokenLM.decimals(), underlying.decimals()); - - assertEq( - address(staticATokenLM.INCENTIVES_CONTROLLER()), - address(AToken(A_TOKEN).getIncentivesController()) - ); - } - - function test_convertersAndPreviews() public view { - uint128 amount = 5 ether; - uint256 shares = staticATokenLM.convertToShares(amount); - assertLe(shares, amount, 'SHARES LOWER'); - assertEq(shares, staticATokenLM.previewDeposit(amount), 'PREVIEW_DEPOSIT'); - assertLe(shares, staticATokenLM.previewWithdraw(amount), 'PREVIEW_WITHDRAW'); - uint256 assets = staticATokenLM.convertToAssets(amount); - assertGe(assets, shares, 'ASSETS GREATER'); - assertLe(assets, staticATokenLM.previewMint(amount), 'PREVIEW_MINT'); - assertEq(assets, staticATokenLM.previewRedeem(amount), 'PREVIEW_REDEEM'); - } - - // Redeem tests - function test_redeem() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - assertEq(staticATokenLM.maxRedeem(user), staticATokenLM.balanceOf(user)); - staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user); - assertEq(staticATokenLM.balanceOf(user), 0); - assertLe(IERC20(UNDERLYING).balanceOf(user), amountToDeposit); - assertApproxEqAbs(IERC20(UNDERLYING).balanceOf(user), amountToDeposit, 1); - } - - function test_redeemAToken() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - assertEq(staticATokenLM.maxRedeem(user), staticATokenLM.balanceOf(user)); - staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user, false); - assertEq(staticATokenLM.balanceOf(user), 0); - assertLe(IERC20(A_TOKEN).balanceOf(user), amountToDeposit); - assertApproxEqAbs(IERC20(A_TOKEN).balanceOf(user), amountToDeposit, 1); - } - - function test_redeemAllowance() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - staticATokenLM.approve(user1, staticATokenLM.maxRedeem(user)); - vm.stopPrank(); - vm.startPrank(user1); - staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user1, user); - assertEq(staticATokenLM.balanceOf(user), 0); - assertLe(IERC20(UNDERLYING).balanceOf(user1), amountToDeposit); - assertApproxEqAbs(IERC20(UNDERLYING).balanceOf(user1), amountToDeposit, 1); - } - - function testFail_redeemOverflowAllowance() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - staticATokenLM.approve(user1, staticATokenLM.maxRedeem(user) / 2); - vm.stopPrank(); - vm.startPrank(user1); - staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user1, user); - assertEq(staticATokenLM.balanceOf(user), 0); - assertEq(IERC20(A_TOKEN).balanceOf(user1), amountToDeposit); - } - - function testFail_redeemAboveBalance() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - staticATokenLM.redeem(staticATokenLM.maxRedeem(user) + 1, user, user); - } - - // Withdraw tests - function test_withdraw() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - assertLe(staticATokenLM.maxWithdraw(user), amountToDeposit); - staticATokenLM.withdraw(staticATokenLM.maxWithdraw(user), user, user); - assertEq(staticATokenLM.balanceOf(user), 0); - assertLe(IERC20(UNDERLYING).balanceOf(user), amountToDeposit); - assertApproxEqAbs(IERC20(UNDERLYING).balanceOf(user), amountToDeposit, 1); - } - - function testFail_withdrawAboveBalance() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - _fundUser(amountToDeposit, user1); - - _depositAToken(amountToDeposit, user); - _depositAToken(amountToDeposit, user1); - - assertEq(staticATokenLM.maxWithdraw(user), amountToDeposit); - staticATokenLM.withdraw(staticATokenLM.maxWithdraw(user) + 1, user, user); - } - - // mint - function test_mint() public { - vm.stopPrank(); - - // set supply cap to non-zero - vm.startPrank(poolAdmin); - contracts.poolConfiguratorProxy.setSupplyCap(UNDERLYING, 15_000); - vm.stopPrank(); - - vm.startPrank(user); - - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - IERC20(UNDERLYING).approve(address(staticATokenLM), amountToDeposit); - uint256 shares = 1 ether; - staticATokenLM.mint(shares, user); - assertEq(shares, staticATokenLM.balanceOf(user)); - } - - function testFail_mintAboveBalance() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _underlyingToAToken(amountToDeposit, user); - IERC20(A_TOKEN).approve(address(staticATokenLM), amountToDeposit); - staticATokenLM.mint(amountToDeposit, user); - } - - // test rewards - function test_collectAndUpdateRewards() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - _skipBlocks(60); - assertEq(IERC20(REWARD_TOKEN).balanceOf(address(staticATokenLM)), 0); - uint256 claimable = staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN); - staticATokenLM.collectAndUpdateRewards(REWARD_TOKEN); - assertEq(IERC20(REWARD_TOKEN).balanceOf(address(staticATokenLM)), claimable); - } - - function test_claimRewardsToSelf() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - _skipBlocks(60); - - uint256 claimable = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - staticATokenLM.claimRewardsToSelf(rewardTokens); - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimable); - assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN), 0); - } - - function test_claimRewards() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - _skipBlocks(60); - - uint256 claimable = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - staticATokenLM.claimRewards(user, rewardTokens); - assertEq(claimable, IERC20(REWARD_TOKEN).balanceOf(user)); - assertEq(IERC20(REWARD_TOKEN).balanceOf(address(staticATokenLM)), 0); - assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN), 0); - } - - // should fail as user1 is not a valid claimer - function testFail_claimRewardsOnBehalfOf() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - _skipBlocks(60); - - vm.stopPrank(); - vm.startPrank(user1); - - staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - staticATokenLM.claimRewardsOnBehalf(user, user1, rewardTokens); - } - - function test_depositATokenClaimWithdrawClaim() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - // deposit aweth - _depositAToken(amountToDeposit, user); - - // forward time - _skipBlocks(60); - - // claim - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), 0); - uint256 claimable0 = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), claimable0); - assertGt(claimable0, 0); - staticATokenLM.claimRewardsToSelf(rewardTokens); - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimable0); - - // forward time - _skipBlocks(60); - - // redeem - staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user); - uint256 claimable1 = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), claimable1); - assertGt(claimable1, 0); - - // claim on behalf of other user - staticATokenLM.claimRewardsToSelf(rewardTokens); - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimable1 + claimable0); - assertEq(staticATokenLM.balanceOf(user), 0); - assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN), 0); - assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), 0); - assertGt(AToken(UNDERLYING).balanceOf(user), 5 ether); - } - - function test_depositWETHClaimWithdrawClaim() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - // forward time - _skipBlocks(60); - - // claim - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), 0); - uint256 claimable0 = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), claimable0); - assertGt(claimable0, 0); - staticATokenLM.claimRewardsToSelf(rewardTokens); - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimable0); - - // forward time - _skipBlocks(60); - - // redeem - staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user); - uint256 claimable1 = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), claimable1); - assertGt(claimable1, 0); - - // claim on behalf of other user - staticATokenLM.claimRewardsToSelf(rewardTokens); - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimable1 + claimable0); - assertEq(staticATokenLM.balanceOf(user), 0); - assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN), 0); - assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), 0); - assertGt(AToken(UNDERLYING).balanceOf(user), 5 ether); - } - - function test_transfer() public { - uint128 amountToDeposit = 10 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - // transfer to 2nd user - staticATokenLM.transfer(user1, amountToDeposit / 2); - assertEq(staticATokenLM.getClaimableRewards(user1, REWARD_TOKEN), 0); - - // forward time - _skipBlocks(60); - - // redeem for both - uint256 claimableUser = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user); - staticATokenLM.claimRewardsToSelf(rewardTokens); - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimableUser); - vm.stopPrank(); - vm.startPrank(user1); - uint256 claimableUser1 = staticATokenLM.getClaimableRewards(user1, REWARD_TOKEN); - staticATokenLM.redeem(staticATokenLM.maxRedeem(user1), user1, user1); - staticATokenLM.claimRewardsToSelf(rewardTokens); - assertEq(IERC20(REWARD_TOKEN).balanceOf(user1), claimableUser1); - assertGt(claimableUser1, 0); - - assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), 0); - assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN), 0); - assertEq(staticATokenLM.getClaimableRewards(user1, REWARD_TOKEN), 0); - } - - // getUnclaimedRewards - function test_getUnclaimedRewards() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - uint256 shares = _depositAToken(amountToDeposit, user); - assertEq(staticATokenLM.getUnclaimedRewards(user, REWARD_TOKEN), 0); - _skipBlocks(1000); - staticATokenLM.redeem(shares, user, user); - assertGt(staticATokenLM.getUnclaimedRewards(user, REWARD_TOKEN), 0); - } - - /** - * maxDeposit test - */ - function test_maxDeposit_freeze() public { - vm.stopPrank(); - vm.startPrank(roleList.marketOwner); - contracts.poolConfiguratorProxy.setReserveFreeze(UNDERLYING, true); - - uint256 max = staticATokenLM.maxDeposit(address(0)); - - assertEq(max, 0); - } - - function test_maxDeposit_paused() public { - vm.stopPrank(); - vm.startPrank(address(roleList.marketOwner)); - contracts.poolConfiguratorProxy.setReservePause(UNDERLYING, true); - - uint256 max = staticATokenLM.maxDeposit(address(0)); - - assertEq(max, 0); - } - - function test_maxDeposit_noCap() public { - vm.stopPrank(); - vm.startPrank(address(roleList.marketOwner)); - contracts.poolConfiguratorProxy.setSupplyCap(UNDERLYING, 0); - - uint256 maxDeposit = staticATokenLM.maxDeposit(address(0)); - uint256 maxMint = staticATokenLM.maxMint(address(0)); - - assertEq(maxDeposit, type(uint256).max); - assertEq(maxMint, type(uint256).max); - } - - // should be 0 as supply is ~5k - function test_maxDeposit_5kCap() public { - vm.stopPrank(); - vm.startPrank(address(roleList.marketOwner)); - contracts.poolConfiguratorProxy.setSupplyCap(UNDERLYING, 5_000); - - uint256 max = staticATokenLM.maxDeposit(address(0)); - assertEq(max, 0); - } - - function test_maxDeposit_50kCap() public { - vm.stopPrank(); - vm.startPrank(address(roleList.marketOwner)); - contracts.poolConfiguratorProxy.setSupplyCap(UNDERLYING, 50_000); - - uint256 max = staticATokenLM.maxDeposit(address(0)); - DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(UNDERLYING); - assertEq( - max, - 50_000 * - (10 ** IERC20Metadata(UNDERLYING).decimals()) - - (IERC20Metadata(A_TOKEN).totalSupply() + - uint256(reserveData.accruedToTreasury).rayMulRoundUp(staticATokenLM.rate())) - ); - } - - /** - * maxRedeem test - */ - function test_maxRedeem_paused() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - vm.stopPrank(); - vm.startPrank(address(roleList.marketOwner)); - contracts.poolConfiguratorProxy.setReservePause(UNDERLYING, true); - - uint256 max = staticATokenLM.maxRedeem(address(user)); - - assertEq(max, 0); - } - - function test_maxRedeem_allAvailable() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - uint256 max = staticATokenLM.maxRedeem(address(user)); - - assertEq(max, staticATokenLM.balanceOf(user)); - } - - function test_maxRedeem_partAvailable() public { - uint128 amountToDeposit = 50 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - vm.stopPrank(); - - uint256 maxRedeemBefore = staticATokenLM.previewRedeem(staticATokenLM.maxRedeem(address(user))); - uint256 underlyingBalanceBefore = IERC20Metadata(UNDERLYING).balanceOf(A_TOKEN); - - // create rich user - address borrowUser = address(99); - vm.startPrank(borrowUser); - deal(address(wbtc), borrowUser, 2_000e8); - wbtc.approve(address(POOL), 2_000e8); - POOL.deposit(address(wbtc), 2_000e8, borrowUser, 0); - - // borrow all available - POOL.borrow(UNDERLYING, underlyingBalanceBefore - (maxRedeemBefore / 2), 2, 0, borrowUser); - - uint256 maxRedeemAfter = staticATokenLM.previewRedeem(staticATokenLM.maxRedeem(address(user))); - assertApproxEqAbs(maxRedeemAfter, (maxRedeemBefore / 2), 1); - } - - function test_maxRedeem_nonAvailable() public { - uint128 amountToDeposit = 50 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - vm.stopPrank(); - - uint256 underlyingBalanceBefore = IERC20Metadata(UNDERLYING).balanceOf(A_TOKEN); - // create rich user - address borrowUser = address(99); - vm.startPrank(borrowUser); - deal(address(wbtc), borrowUser, 2_000e8); - wbtc.approve(address(POOL), 2_000e8); - POOL.deposit(address(wbtc), 2_000e8, borrowUser, 0); - - // borrow all available - contracts.poolProxy.borrow(UNDERLYING, underlyingBalanceBefore, 2, 0, borrowUser); - - uint256 maxRedeemAfter = staticATokenLM.maxRedeem(address(user)); - assertEq(maxRedeemAfter, 0); - } - - function test_permit() public { - SigUtils.Permit memory permit = SigUtils.Permit({ - owner: user, - spender: spender, - value: 1 ether, - nonce: staticATokenLM.nonces(user), - deadline: block.timestamp + 1 days - }); - - bytes32 permitDigest = SigUtils.getTypedDataHash( - permit, - staticATokenLM.PERMIT_TYPEHASH(), - staticATokenLM.DOMAIN_SEPARATOR() - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); - - staticATokenLM.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); - - assertEq(staticATokenLM.allowance(permit.owner, spender), permit.value); - } - - function test_permit_expired() public { - // as the default timestamp is 0, we move ahead in time a bit - vm.warp(10 days); - - SigUtils.Permit memory permit = SigUtils.Permit({ - owner: user, - spender: spender, - value: 1 ether, - nonce: staticATokenLM.nonces(user), - deadline: block.timestamp - 1 days - }); - - bytes32 permitDigest = SigUtils.getTypedDataHash( - permit, - staticATokenLM.PERMIT_TYPEHASH(), - staticATokenLM.DOMAIN_SEPARATOR() - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); - - vm.expectRevert('PERMIT_DEADLINE_EXPIRED'); - staticATokenLM.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); - } - - function test_permit_invalidSigner() public { - SigUtils.Permit memory permit = SigUtils.Permit({ - owner: address(424242), - spender: spender, - value: 1 ether, - nonce: staticATokenLM.nonces(user), - deadline: block.timestamp + 1 days - }); - - bytes32 permitDigest = SigUtils.getTypedDataHash( - permit, - staticATokenLM.PERMIT_TYPEHASH(), - staticATokenLM.DOMAIN_SEPARATOR() - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); - - vm.expectRevert('INVALID_SIGNER'); - staticATokenLM.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); - } - - function _configureLM() internal { - PullRewardsTransferStrategy strat = new PullRewardsTransferStrategy( - report.rewardsControllerProxy, - EMISSION_ADMIN, - EMISSION_ADMIN - ); - - vm.startPrank(poolAdmin); - contracts.emissionManager.setEmissionAdmin(REWARD_TOKEN, EMISSION_ADMIN); - vm.stopPrank(); - - vm.startPrank(EMISSION_ADMIN); - IERC20(REWARD_TOKEN).approve(address(strat), 10_000 ether); - vm.stopPrank(); - - vm.startPrank(OWNER); - TestnetERC20(REWARD_TOKEN).mint(EMISSION_ADMIN, 10_000 ether); - vm.stopPrank(); - - RewardsDataTypes.RewardsConfigInput[] memory config = new RewardsDataTypes.RewardsConfigInput[]( - 1 - ); - config[0] = RewardsDataTypes.RewardsConfigInput( - 0.00385 ether, - 10_000 ether, - uint32(block.timestamp + 30 days), - A_TOKEN, - REWARD_TOKEN, - ITransferStrategyBase(strat), - IEACAggregatorProxy(address(2)) - ); - - vm.prank(EMISSION_ADMIN); - contracts.emissionManager.configureAssets(config); - - staticATokenLM.refreshRewardTokens(); - } - - function _openSupplyAndBorrowPositions() internal { - // this is to open borrow positions so that the aToken balance increases - address whale = address(79); - vm.startPrank(whale); - _fundUser(5_000 ether, whale); - - weth.approve(address(POOL), 5_000 ether); - POOL.deposit(address(weth), 5_000 ether, whale, 0); - - POOL.borrow(address(weth), 1_000 ether, 2, 0, whale); - vm.stopPrank(); - } -} diff --git a/tests/periphery/static-a-token/StaticATokenMetaTransactions.t.sol b/tests/periphery/static-a-token/StaticATokenMetaTransactions.t.sol deleted file mode 100644 index 62dd690a..00000000 --- a/tests/periphery/static-a-token/StaticATokenMetaTransactions.t.sol +++ /dev/null @@ -1,252 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -import {IERC20WithPermit} from 'solidity-utils/contracts/oz-common/interfaces/IERC20WithPermit.sol'; -import {StaticATokenLM, IStaticATokenLM, IERC20} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; -import {SigUtils} from '../../utils/SigUtils.sol'; -import {BaseTest, IAToken, IRewardsController, DataTypes} from './TestBase.sol'; - -contract StaticATokenMetaTransactions is BaseTest { - function setUp() public override { - super.setUp(); - - // Testing meta transactions with USDX as WETH does not support permit - DataTypes.ReserveDataLegacy memory reserveDataUSDX = contracts.poolProxy.getReserveData( - address(usdx) - ); - UNDERLYING = address(usdx); - A_TOKEN = reserveDataUSDX.aTokenAddress; - - staticATokenLM = StaticATokenLM(factory.getStaticAToken(UNDERLYING)); - - vm.startPrank(user); - } - - function test_validateDomainSeparator() public view { - address[] memory staticATokens = factory.getStaticATokens(); - - for (uint256 i = 0; i < staticATokens.length; i++) { - bytes32 separator1 = StaticATokenLM(staticATokens[i]).DOMAIN_SEPARATOR(); - for (uint256 j = 0; j < staticATokens.length; j++) { - if (i != j) { - bytes32 separator2 = StaticATokenLM(staticATokens[j]).DOMAIN_SEPARATOR(); - assertNotEq(separator1, separator2, 'DOMAIN_SEPARATOR_MUST_BE_UNIQUE'); - } - } - } - } - - function test_metaDepositATokenUnderlyingNoPermit() public { - uint128 amountToDeposit = 5e6; - deal(UNDERLYING, user, amountToDeposit); - IERC20(UNDERLYING).approve(address(staticATokenLM), 1e6); - IStaticATokenLM.PermitParams memory permitParams; - - // generate combined permit - SigUtils.DepositPermit memory depositPermit = SigUtils.DepositPermit({ - owner: user, - spender: spender, - value: 1e6, - referralCode: 0, - fromUnderlying: true, - nonce: staticATokenLM.nonces(user), - deadline: block.timestamp + 1 days, - permit: permitParams - }); - bytes32 digest = SigUtils.getTypedDepositHash( - depositPermit, - staticATokenLM.METADEPOSIT_TYPEHASH(), - staticATokenLM.DOMAIN_SEPARATOR() - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); - - IStaticATokenLM.SignatureParams memory sigParams = IStaticATokenLM.SignatureParams(v, r, s); - - uint256 previewDeposit = staticATokenLM.previewDeposit(depositPermit.value); - staticATokenLM.metaDeposit( - depositPermit.owner, - depositPermit.spender, - depositPermit.value, - depositPermit.referralCode, - depositPermit.fromUnderlying, - depositPermit.deadline, - permitParams, - sigParams - ); - - assertEq(staticATokenLM.balanceOf(depositPermit.spender), previewDeposit); - } - - function test_metaDepositATokenUnderlying() public { - uint128 amountToDeposit = 5e6; - deal(UNDERLYING, user, amountToDeposit); - - // permit for aToken deposit - SigUtils.Permit memory permit = SigUtils.Permit({ - owner: user, - spender: address(staticATokenLM), - value: 1e6, - nonce: IERC20WithPermit(UNDERLYING).nonces(user), - deadline: block.timestamp + 1 days - }); - - bytes32 permitDigest = SigUtils.getTypedDataHash( - permit, - staticATokenLM.PERMIT_TYPEHASH(), - IERC20WithPermit(UNDERLYING).DOMAIN_SEPARATOR() - ); - - (uint8 pV, bytes32 pR, bytes32 pS) = vm.sign(userPrivateKey, permitDigest); - - IStaticATokenLM.PermitParams memory permitParams = IStaticATokenLM.PermitParams( - permit.owner, - permit.spender, - permit.value, - permit.deadline, - pV, - pR, - pS - ); - - // generate combined permit - SigUtils.DepositPermit memory depositPermit = SigUtils.DepositPermit({ - owner: user, - spender: spender, - value: permit.value, - referralCode: 0, - fromUnderlying: true, - nonce: staticATokenLM.nonces(user), - deadline: permit.deadline, - permit: permitParams - }); - (uint8 v, bytes32 r, bytes32 s) = vm.sign( - userPrivateKey, - SigUtils.getTypedDepositHash( - depositPermit, - staticATokenLM.METADEPOSIT_TYPEHASH(), - staticATokenLM.DOMAIN_SEPARATOR() - ) - ); - - IStaticATokenLM.SignatureParams memory sigParams = IStaticATokenLM.SignatureParams(v, r, s); - - uint256 previewDeposit = staticATokenLM.previewDeposit(depositPermit.value); - uint256 shares = staticATokenLM.metaDeposit( - depositPermit.owner, - depositPermit.spender, - depositPermit.value, - depositPermit.referralCode, - depositPermit.fromUnderlying, - depositPermit.deadline, - permitParams, - sigParams - ); - assertEq(shares, previewDeposit); - assertEq(staticATokenLM.balanceOf(depositPermit.spender), previewDeposit); - } - - function test_metaDepositAToken() public { - uint128 amountToDeposit = 5e6; - _fundUser(amountToDeposit, user); - _underlyingToAToken(amountToDeposit, user); - - // permit for aToken deposit - SigUtils.Permit memory permit = SigUtils.Permit({ - owner: user, - spender: address(staticATokenLM), - value: 1e6, - nonce: IERC20WithPermit(A_TOKEN).nonces(user), - deadline: block.timestamp + 1 days - }); - - bytes32 permitDigest = SigUtils.getTypedDataHash( - permit, - staticATokenLM.PERMIT_TYPEHASH(), - IERC20WithPermit(A_TOKEN).DOMAIN_SEPARATOR() - ); - - (uint8 pV, bytes32 pR, bytes32 pS) = vm.sign(userPrivateKey, permitDigest); - - IStaticATokenLM.PermitParams memory permitParams = IStaticATokenLM.PermitParams( - permit.owner, - permit.spender, - permit.value, - permit.deadline, - pV, - pR, - pS - ); - - // generate combined permit - SigUtils.DepositPermit memory depositPermit = SigUtils.DepositPermit({ - owner: user, - spender: spender, - value: permit.value, - referralCode: 0, - fromUnderlying: false, - nonce: staticATokenLM.nonces(user), - deadline: permit.deadline, - permit: permitParams - }); - bytes32 digest = SigUtils.getTypedDepositHash( - depositPermit, - staticATokenLM.METADEPOSIT_TYPEHASH(), - staticATokenLM.DOMAIN_SEPARATOR() - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); - - IStaticATokenLM.SignatureParams memory sigParams = IStaticATokenLM.SignatureParams(v, r, s); - - uint256 previewDeposit = staticATokenLM.previewDeposit(depositPermit.value); - - staticATokenLM.metaDeposit( - depositPermit.owner, - depositPermit.spender, - depositPermit.value, - depositPermit.referralCode, - depositPermit.fromUnderlying, - depositPermit.deadline, - permitParams, - sigParams - ); - - assertEq(staticATokenLM.balanceOf(depositPermit.spender), previewDeposit); - } - - function test_metaWithdraw() public { - uint128 amountToDeposit = 5e6; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - SigUtils.WithdrawPermit memory permit = SigUtils.WithdrawPermit({ - owner: user, - spender: spender, - staticAmount: 0, - dynamicAmount: 1e6, - toUnderlying: false, - nonce: staticATokenLM.nonces(user), - deadline: block.timestamp + 1 days - }); - bytes32 digest = SigUtils.getTypedWithdrawHash( - permit, - staticATokenLM.METAWITHDRAWAL_TYPEHASH(), - staticATokenLM.DOMAIN_SEPARATOR() - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); - - IStaticATokenLM.SignatureParams memory sigParams = IStaticATokenLM.SignatureParams(v, r, s); - - staticATokenLM.metaWithdraw( - permit.owner, - permit.spender, - permit.staticAmount, - permit.dynamicAmount, - permit.toUnderlying, - permit.deadline, - sigParams - ); - - assertEq(IERC20(A_TOKEN).balanceOf(permit.spender), permit.dynamicAmount); - } -} diff --git a/tests/periphery/static-a-token/StaticATokenNoLM.t.sol b/tests/periphery/static-a-token/StaticATokenNoLM.t.sol deleted file mode 100644 index 84ddbd33..00000000 --- a/tests/periphery/static-a-token/StaticATokenNoLM.t.sol +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -import {BaseTest, IERC20} from './TestBase.sol'; - -/** - * Testing the static token wrapper on a pool that never had LM enabled - * This is a slightly different assumption than a pool that doesn't have LM enabled any more as incentivesController.rewardTokens() will have length=0 - */ -contract StaticATokenNoLMTest is BaseTest { - function setUp() public override { - super.setUp(); - - vm.startPrank(user); - } - - // test rewards - function test_collectAndUpdateRewardsWithLMDisabled() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - _skipBlocks(60); - assertEq(IERC20(REWARD_TOKEN).balanceOf(address(staticATokenLM)), 0); - assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), 0); - assertEq(staticATokenLM.collectAndUpdateRewards(REWARD_TOKEN), 0); - assertEq(IERC20(REWARD_TOKEN).balanceOf(address(staticATokenLM)), 0); - } - - function test_claimRewardsToSelfWithLMDisabled() public { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - _skipBlocks(60); - - try staticATokenLM.getClaimableRewards(user, REWARD_TOKEN) {} catch Error( - string memory reason - ) { - require(keccak256(bytes(reason)) == keccak256(bytes('9'))); - } - - try staticATokenLM.claimRewardsToSelf(rewardTokens) {} catch Error(string memory reason) { - require(keccak256(bytes(reason)) == keccak256(bytes('9'))); - } - assertEq(IERC20(REWARD_TOKEN).balanceOf(user), 0); - } -} diff --git a/tests/periphery/static-a-token/TestBase.sol b/tests/periphery/static-a-token/TestBase.sol index 058ad713..0365fb21 100644 --- a/tests/periphery/static-a-token/TestBase.sol +++ b/tests/periphery/static-a-token/TestBase.sol @@ -1,18 +1,18 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.10; -import {IRewardsController} from '../../../src/periphery/contracts/rewards/interfaces/IRewardsController.sol'; +import {IERC20Metadata, IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; import {ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/TransparentProxyFactory.sol'; -import {IPool} from '../../../src/core/contracts/interfaces/IPool.sol'; -import {StaticATokenFactory} from '../../../src/periphery/contracts/static-a-token/StaticATokenFactory.sol'; -import {StaticATokenLM, IStaticATokenLM, IERC20, IERC20Metadata, ERC20} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; -import {IAToken} from '../../../src/core/contracts/interfaces/IAToken.sol'; +import {StataTokenFactory} from '../../../src/periphery/contracts/static-a-token/StataTokenFactory.sol'; +import {StataTokenV2} from '../../../src/periphery/contracts/static-a-token/StataTokenV2.sol'; +import {IERC20AaveLM} from '../../../src/periphery/contracts/static-a-token/interfaces/IERC20AaveLM.sol'; import {TestnetProcedures, TestnetERC20} from '../../utils/TestnetProcedures.sol'; import {DataTypes} from '../../../src/core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; abstract contract BaseTest is TestnetProcedures { address constant OWNER = address(1234); + address public constant EMISSION_ADMIN = address(25); address public user; address public user1; @@ -21,17 +21,16 @@ abstract contract BaseTest is TestnetProcedures { uint256 internal userPrivateKey; uint256 internal spenderPrivateKey; - StaticATokenLM public staticATokenLM; + StataTokenV2 public stataTokenV2; address public proxyAdmin; ITransparentProxyFactory public proxyFactory; - StaticATokenFactory public factory; + StataTokenFactory public factory; address[] rewardTokens; - address public UNDERLYING; - address public A_TOKEN; - address public REWARD_TOKEN; - IPool public POOL; + address public underlying; + address public aToken; + address public rewardToken; function setUp() public virtual { userPrivateKey = 0xA11CE; @@ -40,29 +39,24 @@ abstract contract BaseTest is TestnetProcedures { user1 = address(vm.addr(2)); spender = vm.addr(spenderPrivateKey); - initTestEnvironment(); + initTestEnvironment(false); DataTypes.ReserveDataLegacy memory reserveDataWETH = contracts.poolProxy.getReserveData( tokenList.weth ); - UNDERLYING = address(weth); - REWARD_TOKEN = address(new TestnetERC20('LM Reward ERC20', 'RWD', 18, OWNER)); - A_TOKEN = reserveDataWETH.aTokenAddress; - POOL = contracts.poolProxy; + underlying = address(weth); + rewardToken = address(new TestnetERC20('LM Reward ERC20', 'RWD', 18, OWNER)); + aToken = reserveDataWETH.aTokenAddress; - rewardTokens.push(REWARD_TOKEN); + rewardTokens.push(rewardToken); proxyFactory = ITransparentProxyFactory(report.transparentProxyFactory); proxyAdmin = report.proxyAdmin; - factory = StaticATokenFactory(report.staticATokenFactoryProxy); - factory.createStaticATokens(POOL.getReservesList()); + factory = StataTokenFactory(report.staticATokenFactoryProxy); + factory.createStataTokens(contracts.poolProxy.getReservesList()); - staticATokenLM = StaticATokenLM(factory.getStaticAToken(UNDERLYING)); - } - - function _fundUser(uint128 amountToDeposit, address targetUser) internal { - deal(UNDERLYING, targetUser, amountToDeposit); + stataTokenV2 = StataTokenV2(factory.getStataToken(underlying)); } function _skipBlocks(uint128 blocks) internal { @@ -70,22 +64,32 @@ abstract contract BaseTest is TestnetProcedures { vm.warp(block.timestamp + blocks * 12); // assuming a block is around 12seconds } - function _underlyingToAToken(uint256 amountToDeposit, address targetUser) internal { - IERC20(UNDERLYING).approve(address(POOL), amountToDeposit); - POOL.deposit(UNDERLYING, amountToDeposit, targetUser, 0); + function testAdmin() public { + vm.stopPrank(); + vm.startPrank(proxyAdmin); + assertEq(TransparentUpgradeableProxy(payable(address(stataTokenV2))).admin(), proxyAdmin); + assertEq(TransparentUpgradeableProxy(payable(address(factory))).admin(), proxyAdmin); + vm.stopPrank(); } - function _depositAToken(uint256 amountToDeposit, address targetUser) internal returns (uint256) { - _underlyingToAToken(amountToDeposit, targetUser); - IERC20(A_TOKEN).approve(address(staticATokenLM), amountToDeposit); - return staticATokenLM.deposit(amountToDeposit, targetUser, 10, false); + function _fundUnderlying(uint256 assets, address receiver) internal { + deal(underlying, receiver, assets); } - function testAdmin() public { + function _fundAToken(uint256 assets, address receiver) internal { + _fundUnderlying(assets, receiver); + vm.startPrank(receiver); + IERC20(underlying).approve(address(contracts.poolProxy), assets); + contracts.poolProxy.deposit(underlying, assets, receiver, 0); vm.stopPrank(); - vm.startPrank(proxyAdmin); - assertEq(TransparentUpgradeableProxy(payable(address(staticATokenLM))).admin(), proxyAdmin); - assertEq(TransparentUpgradeableProxy(payable(address(factory))).admin(), proxyAdmin); + } + + function _fund4626(uint256 assets, address receiver) internal returns (uint256) { + _fundAToken(assets, receiver); + vm.startPrank(receiver); + IERC20(aToken).approve(address(stataTokenV2), assets); + uint256 shares = stataTokenV2.depositATokens(assets, receiver); vm.stopPrank(); + return shares; } } diff --git a/tests/utils/DiffUtils.sol b/tests/utils/DiffUtils.sol index a5e7577d..23008fb5 100644 --- a/tests/utils/DiffUtils.sol +++ b/tests/utils/DiffUtils.sol @@ -22,7 +22,7 @@ contract DiffUtils is Test { string[] memory inputs = new string[](7); inputs[0] = 'npx'; - inputs[1] = '@bgd-labs/aave-cli@^0.16.2'; + inputs[1] = '@bgd-labs/aave-cli@^0.16.4'; inputs[2] = 'diff-snapshots'; inputs[3] = beforePath; inputs[4] = afterPath; diff --git a/tests/utils/SigUtils.sol b/tests/utils/SigUtils.sol index 8c64e400..a41339fd 100644 --- a/tests/utils/SigUtils.sol +++ b/tests/utils/SigUtils.sol @@ -1,36 +1,18 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.10; -import {IStaticATokenLM} from '../../src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol'; +import {IERC20AaveLM} from '../../src/periphery/contracts/static-a-token/interfaces/IERC20AaveLM.sol'; library SigUtils { - struct Permit { - address owner; - address spender; - uint256 value; - uint256 nonce; - uint256 deadline; - } + bytes32 internal constant PERMIT_TYPEHASH = + keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)'); - struct WithdrawPermit { - address owner; - address spender; - uint256 staticAmount; - uint256 dynamicAmount; - bool toUnderlying; - uint256 nonce; - uint256 deadline; - } - - struct DepositPermit { + struct Permit { address owner; address spender; uint256 value; - uint16 referralCode; - bool fromUnderlying; uint256 nonce; uint256 deadline; - IStaticATokenLM.PermitParams permit; } // computes the hash of a permit @@ -48,45 +30,6 @@ library SigUtils { ); } - function getWithdrawHash( - WithdrawPermit memory permit, - bytes32 typehash - ) internal pure returns (bytes32) { - return - keccak256( - abi.encode( - typehash, - permit.owner, - permit.spender, - permit.staticAmount, - permit.dynamicAmount, - permit.toUnderlying, - permit.nonce, - permit.deadline - ) - ); - } - - function getDepositHash( - DepositPermit memory permit, - bytes32 typehash - ) internal pure returns (bytes32) { - return - keccak256( - abi.encode( - typehash, - permit.owner, - permit.spender, - permit.value, - permit.referralCode, - permit.fromUnderlying, - permit.nonce, - permit.deadline, - permit.permit - ) - ); - } - // computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer function getTypedDataHash( Permit memory permit, @@ -96,22 +39,4 @@ library SigUtils { return keccak256(abi.encodePacked('\x19\x01', domainSeparator, getStructHash(permit, typehash))); } - - function getTypedWithdrawHash( - WithdrawPermit memory permit, - bytes32 typehash, - bytes32 domainSeparator - ) public pure returns (bytes32) { - return - keccak256(abi.encodePacked('\x19\x01', domainSeparator, getWithdrawHash(permit, typehash))); - } - - function getTypedDepositHash( - DepositPermit memory permit, - bytes32 typehash, - bytes32 domainSeparator - ) public pure returns (bytes32) { - return - keccak256(abi.encodePacked('\x19\x01', domainSeparator, getDepositHash(permit, typehash))); - } }