From d092425296d95e77157c4aca7e845f3d5f814660 Mon Sep 17 00:00:00 2001 From: sendra Date: Mon, 15 Jul 2024 11:19:08 +0200 Subject: [PATCH 01/22] feat: Add rescuable to static a token (#29) * feat: Add rescuable to static a token * Update src/periphery/contracts/static-a-token/StaticATokenLM.sol --------- Co-authored-by: Lukas --- .../contracts/static-a-token/StaticATokenLM.sol | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/periphery/contracts/static-a-token/StaticATokenLM.sol b/src/periphery/contracts/static-a-token/StaticATokenLM.sol index 9a55fe50..37121d11 100644 --- a/src/periphery/contracts/static-a-token/StaticATokenLM.sol +++ b/src/periphery/contracts/static-a-token/StaticATokenLM.sol @@ -12,6 +12,7 @@ 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 {Rescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; import {IStaticATokenLM} from './interfaces/IStaticATokenLM.sol'; import {IAToken} from './interfaces/IAToken.sol'; @@ -32,7 +33,8 @@ contract StaticATokenLM is Initializable, ERC20('STATIC__aToken_IMPL', 'STATIC__aToken_IMPL', 18), IStaticATokenLM, - IERC4626 + IERC4626, + Rescuable { using SafeERC20 for IERC20; using SafeCast for uint256; @@ -48,7 +50,7 @@ contract StaticATokenLM is 'Withdraw(address owner,address receiver,uint256 shares,uint256 assets,bool withdrawFromAave,uint256 nonce,uint256 deadline)' ); - uint256 public constant STATIC__ATOKEN_LM_REVISION = 2; + uint256 public constant STATIC__ATOKEN_LM_REVISION = 3; IPool public immutable POOL; IRewardsController public immutable INCENTIVES_CONTROLLER; @@ -87,6 +89,11 @@ contract StaticATokenLM is emit Initialized(newAToken, staticATokenName, staticATokenSymbol); } + /// @inheritdoc IRescuable + function whoCanRescue() public view override returns (address) { + return POOL.ADDRESSES_PROVIDER().getACLAdmin(); + } + ///@inheritdoc IStaticATokenLM function refreshRewardTokens() public override { address[] memory rewards = INCENTIVES_CONTROLLER.getRewardsByAsset(address(_aToken)); From 9daf54f494f4d0ee50870a2f4368ec403cdc8c6e Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 23 Jul 2024 05:35:43 +0900 Subject: [PATCH 02/22] fix: combine interface (#33) --- src/periphery/contracts/static-a-token/StaticATokenLM.sol | 3 +-- .../contracts/static-a-token/interfaces/IStaticATokenLM.sol | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/periphery/contracts/static-a-token/StaticATokenLM.sol b/src/periphery/contracts/static-a-token/StaticATokenLM.sol index 37121d11..a1e5b7f0 100644 --- a/src/periphery/contracts/static-a-token/StaticATokenLM.sol +++ b/src/periphery/contracts/static-a-token/StaticATokenLM.sol @@ -12,7 +12,7 @@ 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 {Rescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; +import {IRescuable, Rescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; import {IStaticATokenLM} from './interfaces/IStaticATokenLM.sol'; import {IAToken} from './interfaces/IAToken.sol'; @@ -33,7 +33,6 @@ contract StaticATokenLM is Initializable, ERC20('STATIC__aToken_IMPL', 'STATIC__aToken_IMPL', 18), IStaticATokenLM, - IERC4626, Rescuable { using SafeERC20 for IERC20; diff --git a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol index eed469f3..b36c328b 100644 --- a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol +++ b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol @@ -2,9 +2,10 @@ pragma solidity ^0.8.10; import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {IERC4626} from './IERC4626.sol'; import {IInitializableStaticATokenLM} from './IInitializableStaticATokenLM.sol'; -interface IStaticATokenLM is IInitializableStaticATokenLM { +interface IStaticATokenLM is IInitializableStaticATokenLM, IERC4626 { struct SignatureParams { uint8 v; bytes32 r; From a8e061a1663f38e322335e08e71c1f20f8be42f5 Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 8 Aug 2024 00:25:48 +0300 Subject: [PATCH 03/22] Update METADEPOSIT_TYPEHASH (#44) * Update METADEPOSIT_TYPEHASH * cleanup of MetaDepositParams --- .../static-a-token/StaticATokenLM.sol | 5 +- .../interfaces/IStaticATokenLM.sol | 2 - .../StaticATokenMetaTransactions.t.sol | 93 +++++++++---------- tests/utils/SigUtils.sol | 38 ++++---- 4 files changed, 63 insertions(+), 75 deletions(-) diff --git a/src/periphery/contracts/static-a-token/StaticATokenLM.sol b/src/periphery/contracts/static-a-token/StaticATokenLM.sol index a1e5b7f0..508d5a2e 100644 --- a/src/periphery/contracts/static-a-token/StaticATokenLM.sol +++ b/src/periphery/contracts/static-a-token/StaticATokenLM.sol @@ -42,7 +42,7 @@ contract StaticATokenLM is bytes32 public constant METADEPOSIT_TYPEHASH = keccak256( - 'Deposit(address depositor,address receiver,uint256 assets,uint16 referralCode,bool depositToAave,uint256 nonce,uint256 deadline,PermitParams permit)' + 'Deposit(address depositor,address receiver,uint256 assets,uint16 referralCode,bool depositToAave,uint256 nonce,uint256 deadline)' ); bytes32 public constant METAWITHDRAWAL_TYPEHASH = keccak256( @@ -149,8 +149,7 @@ contract StaticATokenLM is referralCode, depositToAave, nonce, - deadline, - permit + deadline ) ) ) diff --git a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol index b36c328b..a5afeace 100644 --- a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol +++ b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol @@ -13,8 +13,6 @@ interface IStaticATokenLM is IInitializableStaticATokenLM, IERC4626 { } struct PermitParams { - address owner; - address spender; uint256 value; uint256 deadline; uint8 v; diff --git a/tests/periphery/static-a-token/StaticATokenMetaTransactions.t.sol b/tests/periphery/static-a-token/StaticATokenMetaTransactions.t.sol index 62dd690a..dc8c68d2 100644 --- a/tests/periphery/static-a-token/StaticATokenMetaTransactions.t.sol +++ b/tests/periphery/static-a-token/StaticATokenMetaTransactions.t.sol @@ -43,18 +43,17 @@ contract StaticATokenMetaTransactions is BaseTest { IStaticATokenLM.PermitParams memory permitParams; // generate combined permit - SigUtils.DepositPermit memory depositPermit = SigUtils.DepositPermit({ - owner: user, - spender: spender, - value: 1e6, + SigUtils.MetaDepositParams memory metaDepositParams = SigUtils.MetaDepositParams({ + depositor: user, + receiver: spender, + assets: 1e6, referralCode: 0, fromUnderlying: true, nonce: staticATokenLM.nonces(user), - deadline: block.timestamp + 1 days, - permit: permitParams + deadline: block.timestamp + 1 days }); bytes32 digest = SigUtils.getTypedDepositHash( - depositPermit, + metaDepositParams, staticATokenLM.METADEPOSIT_TYPEHASH(), staticATokenLM.DOMAIN_SEPARATOR() ); @@ -62,19 +61,19 @@ contract StaticATokenMetaTransactions is BaseTest { IStaticATokenLM.SignatureParams memory sigParams = IStaticATokenLM.SignatureParams(v, r, s); - uint256 previewDeposit = staticATokenLM.previewDeposit(depositPermit.value); + uint256 previewDeposit = staticATokenLM.previewDeposit(metaDepositParams.assets); staticATokenLM.metaDeposit( - depositPermit.owner, - depositPermit.spender, - depositPermit.value, - depositPermit.referralCode, - depositPermit.fromUnderlying, - depositPermit.deadline, + metaDepositParams.depositor, + metaDepositParams.receiver, + metaDepositParams.assets, + metaDepositParams.referralCode, + metaDepositParams.fromUnderlying, + metaDepositParams.deadline, permitParams, sigParams ); - assertEq(staticATokenLM.balanceOf(depositPermit.spender), previewDeposit); + assertEq(staticATokenLM.balanceOf(metaDepositParams.receiver), previewDeposit); } function test_metaDepositATokenUnderlying() public { @@ -99,8 +98,6 @@ contract StaticATokenMetaTransactions is BaseTest { (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, @@ -109,20 +106,19 @@ contract StaticATokenMetaTransactions is BaseTest { ); // generate combined permit - SigUtils.DepositPermit memory depositPermit = SigUtils.DepositPermit({ - owner: user, - spender: spender, - value: permit.value, + SigUtils.MetaDepositParams memory metaDepositParams = SigUtils.MetaDepositParams({ + depositor: user, + receiver: spender, + assets: permit.value, referralCode: 0, fromUnderlying: true, nonce: staticATokenLM.nonces(user), - deadline: permit.deadline, - permit: permitParams + deadline: permit.deadline }); (uint8 v, bytes32 r, bytes32 s) = vm.sign( userPrivateKey, SigUtils.getTypedDepositHash( - depositPermit, + metaDepositParams, staticATokenLM.METADEPOSIT_TYPEHASH(), staticATokenLM.DOMAIN_SEPARATOR() ) @@ -130,19 +126,19 @@ contract StaticATokenMetaTransactions is BaseTest { IStaticATokenLM.SignatureParams memory sigParams = IStaticATokenLM.SignatureParams(v, r, s); - uint256 previewDeposit = staticATokenLM.previewDeposit(depositPermit.value); + uint256 previewDeposit = staticATokenLM.previewDeposit(metaDepositParams.assets); uint256 shares = staticATokenLM.metaDeposit( - depositPermit.owner, - depositPermit.spender, - depositPermit.value, - depositPermit.referralCode, - depositPermit.fromUnderlying, - depositPermit.deadline, + metaDepositParams.depositor, + metaDepositParams.receiver, + metaDepositParams.assets, + metaDepositParams.referralCode, + metaDepositParams.fromUnderlying, + metaDepositParams.deadline, permitParams, sigParams ); assertEq(shares, previewDeposit); - assertEq(staticATokenLM.balanceOf(depositPermit.spender), previewDeposit); + assertEq(staticATokenLM.balanceOf(metaDepositParams.receiver), previewDeposit); } function test_metaDepositAToken() public { @@ -168,8 +164,6 @@ contract StaticATokenMetaTransactions is BaseTest { (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, @@ -178,18 +172,17 @@ contract StaticATokenMetaTransactions is BaseTest { ); // generate combined permit - SigUtils.DepositPermit memory depositPermit = SigUtils.DepositPermit({ - owner: user, - spender: spender, - value: permit.value, + SigUtils.MetaDepositParams memory metaDepositParams = SigUtils.MetaDepositParams({ + depositor: user, + receiver: spender, + assets: permit.value, referralCode: 0, fromUnderlying: false, nonce: staticATokenLM.nonces(user), - deadline: permit.deadline, - permit: permitParams + deadline: permit.deadline }); bytes32 digest = SigUtils.getTypedDepositHash( - depositPermit, + metaDepositParams, staticATokenLM.METADEPOSIT_TYPEHASH(), staticATokenLM.DOMAIN_SEPARATOR() ); @@ -197,20 +190,20 @@ contract StaticATokenMetaTransactions is BaseTest { IStaticATokenLM.SignatureParams memory sigParams = IStaticATokenLM.SignatureParams(v, r, s); - uint256 previewDeposit = staticATokenLM.previewDeposit(depositPermit.value); + uint256 previewDeposit = staticATokenLM.previewDeposit(metaDepositParams.assets); staticATokenLM.metaDeposit( - depositPermit.owner, - depositPermit.spender, - depositPermit.value, - depositPermit.referralCode, - depositPermit.fromUnderlying, - depositPermit.deadline, + metaDepositParams.depositor, + metaDepositParams.receiver, + metaDepositParams.assets, + metaDepositParams.referralCode, + metaDepositParams.fromUnderlying, + metaDepositParams.deadline, permitParams, sigParams ); - assertEq(staticATokenLM.balanceOf(depositPermit.spender), previewDeposit); + assertEq(staticATokenLM.balanceOf(metaDepositParams.receiver), previewDeposit); } function test_metaWithdraw() public { @@ -219,7 +212,7 @@ contract StaticATokenMetaTransactions is BaseTest { _depositAToken(amountToDeposit, user); - SigUtils.WithdrawPermit memory permit = SigUtils.WithdrawPermit({ + SigUtils.MetaWithdrawParams memory permit = SigUtils.MetaWithdrawParams({ owner: user, spender: spender, staticAmount: 0, diff --git a/tests/utils/SigUtils.sol b/tests/utils/SigUtils.sol index 8c64e400..311a256d 100644 --- a/tests/utils/SigUtils.sol +++ b/tests/utils/SigUtils.sol @@ -12,7 +12,7 @@ library SigUtils { uint256 deadline; } - struct WithdrawPermit { + struct MetaWithdrawParams { address owner; address spender; uint256 staticAmount; @@ -22,15 +22,14 @@ library SigUtils { uint256 deadline; } - struct DepositPermit { - address owner; - address spender; - uint256 value; + struct MetaDepositParams { + address depositor; + address receiver; + uint256 assets; uint16 referralCode; bool fromUnderlying; uint256 nonce; uint256 deadline; - IStaticATokenLM.PermitParams permit; } // computes the hash of a permit @@ -49,7 +48,7 @@ library SigUtils { } function getWithdrawHash( - WithdrawPermit memory permit, + MetaWithdrawParams memory permit, bytes32 typehash ) internal pure returns (bytes32) { return @@ -68,21 +67,20 @@ library SigUtils { } function getDepositHash( - DepositPermit memory permit, + MetaDepositParams memory params, 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 + params.depositor, + params.receiver, + params.assets, + params.referralCode, + params.fromUnderlying, + params.nonce, + params.deadline ) ); } @@ -98,20 +96,20 @@ library SigUtils { } function getTypedWithdrawHash( - WithdrawPermit memory permit, + MetaWithdrawParams memory params, bytes32 typehash, bytes32 domainSeparator ) public pure returns (bytes32) { return - keccak256(abi.encodePacked('\x19\x01', domainSeparator, getWithdrawHash(permit, typehash))); + keccak256(abi.encodePacked('\x19\x01', domainSeparator, getWithdrawHash(params, typehash))); } function getTypedDepositHash( - DepositPermit memory permit, + MetaDepositParams memory params, bytes32 typehash, bytes32 domainSeparator ) public pure returns (bytes32) { return - keccak256(abi.encodePacked('\x19\x01', domainSeparator, getDepositHash(permit, typehash))); + keccak256(abi.encodePacked('\x19\x01', domainSeparator, getDepositHash(params, typehash))); } } From 4ad98c06d161b0da33936b87cfa75e2b2b591212 Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 8 Aug 2024 15:21:22 +0200 Subject: [PATCH 04/22] feat: pausability (#45) --- foundry.toml | 41 +++--- .../static-a-token/DeprecationGap.sol | 12 ++ .../static-a-token/StaticATokenFactory.sol | 7 +- .../static-a-token/StaticATokenLM.sol | 31 ++++- .../interfaces/IStaticATokenLM.sol | 9 ++ tests/periphery/static-a-token/Pausable.t.sol | 125 ++++++++++++++++++ .../static-a-token/StaticATokenLM.t.sol | 58 +++----- tests/periphery/static-a-token/TestBase.sol | 43 ++++++ 8 files changed, 258 insertions(+), 68 deletions(-) create mode 100644 src/periphery/contracts/static-a-token/DeprecationGap.sol create mode 100644 tests/periphery/static-a-token/Pausable.t.sol diff --git a/foundry.toml b/foundry.toml index 61253e0b..0d5dd4c3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,14 +4,17 @@ test = 'tests' script = 'scripts' optimizer = true optimizer_runs = 200 -solc='0.8.19' +solc = '0.8.20' evm_version = 'paris' bytecode_hash = 'none' 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] @@ -25,7 +28,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}" @@ -38,19 +41,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/src/periphery/contracts/static-a-token/DeprecationGap.sol b/src/periphery/contracts/static-a-token/DeprecationGap.sol new file mode 100644 index 00000000..c3b4b47b --- /dev/null +++ b/src/periphery/contracts/static-a-token/DeprecationGap.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +/** + * This contract adds a single slot gap + * The slot is required to account for the now deprecated Initializable. + * The new version of Initializable uses erc7201, so it no longer occupies the first slot. + * https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/proxy/utils/Initializable.sol#L60 + */ +contract DeprecationGap { + uint256 internal __deprecated_initializable_gap; +} diff --git a/src/periphery/contracts/static-a-token/StaticATokenFactory.sol b/src/periphery/contracts/static-a-token/StaticATokenFactory.sol index c48e339a..4e0f8bd0 100644 --- a/src/periphery/contracts/static-a-token/StaticATokenFactory.sol +++ b/src/periphery/contracts/static-a-token/StaticATokenFactory.sol @@ -17,7 +17,7 @@ import {IStaticATokenFactory} from './interfaces/IStaticATokenFactory.sol'; */ contract StaticATokenFactory is Initializable, IStaticATokenFactory { IPool public immutable POOL; - address public immutable ADMIN; + address public immutable PROXY_ADMIN; ITransparentProxyFactory public immutable TRANSPARENT_PROXY_FACTORY; address public immutable STATIC_A_TOKEN_IMPL; @@ -33,7 +33,7 @@ contract StaticATokenFactory is Initializable, IStaticATokenFactory { address staticATokenImpl ) { POOL = pool; - ADMIN = proxyAdmin; + PROXY_ADMIN = proxyAdmin; TRANSPARENT_PROXY_FACTORY = transparentProxyFactory; STATIC_A_TOKEN_IMPL = staticATokenImpl; } @@ -54,7 +54,7 @@ contract StaticATokenFactory is Initializable, IStaticATokenFactory { ); address staticAToken = TRANSPARENT_PROXY_FACTORY.createDeterministic( STATIC_A_TOKEN_IMPL, - ADMIN, + PROXY_ADMIN, abi.encodeWithSelector( StaticATokenLM.initialize.selector, reserveData.aTokenAddress, @@ -63,6 +63,7 @@ contract StaticATokenFactory is Initializable, IStaticATokenFactory { ), bytes32(uint256(uint160(underlyings[i]))) ); + _underlyingToStaticAToken[underlyings[i]] = staticAToken; staticATokens[i] = staticAToken; _staticATokens.push(staticAToken); diff --git a/src/periphery/contracts/static-a-token/StaticATokenLM.sol b/src/periphery/contracts/static-a-token/StaticATokenLM.sol index 508d5a2e..5ca0fb29 100644 --- a/src/periphery/contracts/static-a-token/StaticATokenLM.sol +++ b/src/periphery/contracts/static-a-token/StaticATokenLM.sol @@ -3,11 +3,11 @@ 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 {IACLManager} from '../../../core/contracts/interfaces/IACLManager.sol'; +import {IRewardsController} from '../rewards/interfaces/IRewardsController.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'; @@ -21,6 +21,8 @@ import {IInitializableStaticATokenLM} from './interfaces/IInitializableStaticATo import {StaticATokenErrors} from './StaticATokenErrors.sol'; import {RayMathExplicitRounding, Rounding} from '../libraries/RayMathExplicitRounding.sol'; import {IERC4626} from './interfaces/IERC4626.sol'; +import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol'; +import {DeprecationGap} from './DeprecationGap.sol'; /** * @title StaticATokenLM @@ -30,10 +32,11 @@ import {IERC4626} from './interfaces/IERC4626.sol'; * @author BGD labs */ contract StaticATokenLM is - Initializable, + DeprecationGap, ERC20('STATIC__aToken_IMPL', 'STATIC__aToken_IMPL', 18), IStaticATokenLM, - Rescuable + Rescuable, + PausableUpgradeable { using SafeERC20 for IERC20; using SafeCast for uint256; @@ -61,10 +64,20 @@ contract StaticATokenLM is mapping(address => mapping(address => UserRewardsData)) internal _userRewardsData; constructor(IPool pool, IRewardsController rewardsController) { + _disableInitializers(); POOL = pool; INCENTIVES_CONTROLLER = rewardsController; } + modifier onlyPauseGuardian() { + if (!canPause(msg.sender)) revert OnlyPauseGuardian(msg.sender); + _; + } + + function canPause(address actor) public view returns (bool) { + return IACLManager(POOL.ADDRESSES_PROVIDER().getACLManager()).isEmergencyAdmin(actor); + } + ///@inheritdoc IInitializableStaticATokenLM function initialize( address newAToken, @@ -93,6 +106,12 @@ contract StaticATokenLM is return POOL.ADDRESSES_PROVIDER().getACLAdmin(); } + ///@inheritdoc IStaticATokenLM + function setPaused(bool paused) external onlyPauseGuardian { + if (paused) _pause(); + else _unpause(); + } + ///@inheritdoc IStaticATokenLM function refreshRewardTokens() public override { address[] memory rewards = INCENTIVES_CONTROLLER.getRewardsByAsset(address(_aToken)); @@ -540,7 +559,7 @@ contract StaticATokenLM is * @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 { + function _beforeTokenTransfer(address from, address to, uint256) internal override whenNotPaused { for (uint256 i = 0; i < _rewardTokens.length; i++) { address rewardToken = address(_rewardTokens[i]); uint256 rewardsIndex = getCurrentRewardsIndex(rewardToken); @@ -633,7 +652,7 @@ contract StaticATokenLM is address onBehalfOf, address receiver, address[] memory rewards - ) internal { + ) internal whenNotPaused { for (uint256 i = 0; i < rewards.length; i++) { if (address(rewards[i]) == address(0)) { continue; diff --git a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol index a5afeace..18edaeb7 100644 --- a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol +++ b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol @@ -30,6 +30,8 @@ interface IStaticATokenLM is IInitializableStaticATokenLM, IERC4626 { uint248 lastUpdatedIndex; } + error OnlyPauseGuardian(address caller); + event RewardTokenRegistered(address indexed reward, uint256 startIndex); /** @@ -207,7 +209,14 @@ interface IStaticATokenLM is IInitializableStaticATokenLM, IERC4626 { /** * @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); + + /** + * @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/tests/periphery/static-a-token/Pausable.t.sol b/tests/periphery/static-a-token/Pausable.t.sol new file mode 100644 index 00000000..966a33ac --- /dev/null +++ b/tests/periphery/static-a-token/Pausable.t.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {UpgradableOwnableWithGuardian} from 'solidity-utils/contracts/access-control/UpgradableOwnableWithGuardian.sol'; +import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol'; +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 Pausable is BaseTest { + using RayMathExplicitRounding for uint256; + + function test_setPaused_shouldRevertForInvalidCaller(address actor) external { + vm.assume(actor != poolAdmin && actor != proxyAdmin); + vm.expectRevert(abi.encodeWithSelector(IStaticATokenLM.OnlyPauseGuardian.selector, actor)); + _setPaused(actor, true); + } + + function test_setPaused_shouldSuceedForOwner() external { + assertEq(PausableUpgradeable(address(staticATokenLM)).paused(), false); + _setPaused(poolAdmin, true); + assertEq(PausableUpgradeable(address(staticATokenLM)).paused(), true); + } + + function test_deposit_shouldRevert() external { + vm.startPrank(user); + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + IERC20(UNDERLYING).approve(address(staticATokenLM), amountToDeposit); + vm.stopPrank(); + + _setPausedAsAclAdmin(true); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + staticATokenLM.deposit(amountToDeposit, user, 0, true); + } + + function test_mint_shouldRevert() external { + vm.startPrank(user); + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + IERC20(UNDERLYING).approve(address(staticATokenLM), amountToDeposit); + vm.stopPrank(); + + uint256 sharesToMint = staticATokenLM.previewDeposit(amountToDeposit); + _setPausedAsAclAdmin(true); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + staticATokenLM.mint(sharesToMint, user); + } + + function test_redeem_shouldRevert() external { + uint128 amountToDeposit = 5 ether; + vm.startPrank(user); + _fundUser(amountToDeposit, user); + _depositAToken(amountToDeposit, user); + vm.stopPrank(); + + assertEq(staticATokenLM.maxRedeem(user), staticATokenLM.balanceOf(user)); + + _setPausedAsAclAdmin(true); + uint256 maxRedeem = staticATokenLM.maxRedeem(user); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + staticATokenLM.redeem(maxRedeem, user, user); + } + + function test_withdraw_shouldRevert() external { + uint128 amountToDeposit = 5 ether; + vm.startPrank(user); + _fundUser(amountToDeposit, user); + _depositAToken(amountToDeposit, user); + vm.stopPrank(); + + uint256 maxWithdraw = staticATokenLM.maxWithdraw(user); + _setPausedAsAclAdmin(true); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + staticATokenLM.withdraw(maxWithdraw, user, user); + } + + function test_transfer_shouldRevert() external { + uint128 amountToDeposit = 10 ether; + vm.startPrank(user); + _fundUser(amountToDeposit, user); + _depositAToken(amountToDeposit, user); + vm.stopPrank(); + + _setPausedAsAclAdmin(true); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + staticATokenLM.transfer(user1, amountToDeposit); + } + + function test_claimingRewards_shouldRevert() external { + _configureLM(); + uint128 amountToDeposit = 10 ether; + vm.startPrank(user); + _fundUser(amountToDeposit, user); + _depositAToken(amountToDeposit, user); + vm.stopPrank(); + + _setPausedAsAclAdmin(true); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + vm.prank(user); + staticATokenLM.claimRewardsToSelf(rewardTokens); + } + + function _setPausedAsAclAdmin(bool paused) internal { + _setPaused(poolAdmin, paused); + } + + function _setPaused(address actor, bool paused) internal { + vm.prank(actor); + staticATokenLM.setPaused(paused); + } +} diff --git a/tests/periphery/static-a-token/StaticATokenLM.t.sol b/tests/periphery/static-a-token/StaticATokenLM.t.sol index 543a0c21..6f2ddaf4 100644 --- a/tests/periphery/static-a-token/StaticATokenLM.t.sol +++ b/tests/periphery/static-a-token/StaticATokenLM.t.sol @@ -1,14 +1,12 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.10; +import {IRescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; +import {Initializable} from 'openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol'; 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'; @@ -16,8 +14,6 @@ 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(); @@ -29,8 +25,8 @@ contract StaticATokenLMTest is BaseTest { function test_initializeShouldRevert() public { address impl = factory.STATIC_A_TOKEN_IMPL(); - vm.expectRevert(); - IStaticATokenLM(impl).initialize(0xe50fA9b3c56FfB159cB0FCA61F5c9D750e8128c8, 'hey', 'ho'); + vm.expectRevert(Initializable.InvalidInitialization.selector); + IStaticATokenLM(impl).initialize(A_TOKEN, 'hey', 'ho'); } function test_getters() public view { @@ -556,42 +552,24 @@ contract StaticATokenLMTest is BaseTest { 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 + function test_rescuable_shouldRevertForInvalidCaller() external { + deal(tokenList.usdx, address(staticATokenLM), 1 ether); + vm.expectRevert('ONLY_RESCUE_GUARDIAN'); + IRescuable(address(staticATokenLM)).emergencyTokenTransfer( + tokenList.usdx, + address(this), + 1 ether ); + } + function test_rescuable_shouldSuceedForOwner() external { + deal(tokenList.usdx, address(staticATokenLM), 1 ether); 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)) + IRescuable(address(staticATokenLM)).emergencyTokenTransfer( + tokenList.usdx, + address(this), + 1 ether ); - - vm.prank(EMISSION_ADMIN); - contracts.emissionManager.configureAssets(config); - - staticATokenLM.refreshRewardTokens(); } function _openSupplyAndBorrowPositions() internal { diff --git a/tests/periphery/static-a-token/TestBase.sol b/tests/periphery/static-a-token/TestBase.sol index 058ad713..b20aab9c 100644 --- a/tests/periphery/static-a-token/TestBase.sol +++ b/tests/periphery/static-a-token/TestBase.sol @@ -2,6 +2,10 @@ pragma solidity ^0.8.10; import {IRewardsController} from '../../../src/periphery/contracts/rewards/interfaces/IRewardsController.sol'; +import {RewardsDataTypes} from '../../../src/periphery/contracts/rewards/libraries/RewardsDataTypes.sol'; +import {PullRewardsTransferStrategy} from '../../../src/periphery/contracts/rewards/transfer-strategies/PullRewardsTransferStrategy.sol'; +import {ITransferStrategyBase} from '../../../src/periphery/contracts/rewards/interfaces/ITransferStrategyBase.sol'; +import {IEACAggregatorProxy} from '../../../src/periphery/contracts/misc/interfaces/IEACAggregatorProxy.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'; @@ -13,6 +17,7 @@ import {DataTypes} from '../../../src/core/contracts/protocol/libraries/configur abstract contract BaseTest is TestnetProcedures { address constant OWNER = address(1234); + address public constant EMISSION_ADMIN = address(25); address public user; address public user1; @@ -61,6 +66,44 @@ abstract contract BaseTest is TestnetProcedures { staticATokenLM = StaticATokenLM(factory.getStaticAToken(UNDERLYING)); } + 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 _fundUser(uint128 amountToDeposit, address targetUser) internal { deal(UNDERLYING, targetUser, amountToDeposit); } From ca640a8812a2f0a4216f80f87bb415806e0c9b52 Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 8 Aug 2024 16:36:30 +0200 Subject: [PATCH 05/22] feat: expose latest answer on static a token (#3) --- Makefile | 1 + .../contracts/static-a-token/README.md | 17 +++++++++++ .../static-a-token/StaticATokenLM.sol | 17 +++++++++-- .../interfaces/IStaticATokenLM.sol | 11 +++++++ .../static-a-token/StaticATokenLM.t.sol | 29 +++++++++++++++++++ 5 files changed, 73 insertions(+), 2 deletions(-) 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/src/periphery/contracts/static-a-token/README.md b/src/periphery/contracts/static-a-token/README.md index 9ced57a6..f5dddf20 100644 --- a/src/periphery/contracts/static-a-token/README.md +++ b/src/periphery/contracts/static-a-token/README.md @@ -36,3 +36,20 @@ 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 Umbrella + +- Interface inheritance has been changed so that `IStaticATokenLM` implements `IERC4626`, making it easier for integrators to work with the interface. +- The static A tokens are given a `rescuable`, which can be used by the ACL admin to rescue tokens locked to the contract. +- Permit params have been excluded from the METADEPOSIT_TYPEHASH as they are not necessary. Even if someone were to frontrun the permit via mempool observation the permit is wrapped in a `try..catch` to prevent griefing attacks. +- The static a token not implements pausability, which allows the ACL admin to pause all transfers. + +The storage layout diff was generated via: + +``` +git checkout main +forge inspect src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM storage-layout --pretty > reports/StaticATokenStorageBefore.md +git checkout project-a +forge inspect src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM storage-layout --pretty > reports/StaticATokenStorageAfter.md +make git-diff before=reports/StaticATokenStorageBefore.md after=reports/StaticATokenStorageAfter.md out=StaticATokenStorageDiff +``` diff --git a/src/periphery/contracts/static-a-token/StaticATokenLM.sol b/src/periphery/contracts/static-a-token/StaticATokenLM.sol index 5ca0fb29..80f40ef4 100644 --- a/src/periphery/contracts/static-a-token/StaticATokenLM.sol +++ b/src/periphery/contracts/static-a-token/StaticATokenLM.sol @@ -2,6 +2,8 @@ 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 {DataTypes, ReserveConfiguration} from '../../../core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; import {WadRayMath} from '../../../core/contracts/protocol/libraries/math/WadRayMath.sol'; import {MathUtils} from '../../../core/contracts/protocol/libraries/math/MathUtils.sol'; @@ -55,6 +57,7 @@ contract StaticATokenLM is uint256 public constant STATIC__ATOKEN_LM_REVISION = 3; IPool public immutable POOL; + IPoolAddressesProvider immutable POOL_ADDRESSES_PROVIDER; IRewardsController public immutable INCENTIVES_CONTROLLER; IERC20 internal _aToken; @@ -67,6 +70,7 @@ contract StaticATokenLM is _disableInitializers(); POOL = pool; INCENTIVES_CONTROLLER = rewardsController; + POOL_ADDRESSES_PROVIDER = pool.ADDRESSES_PROVIDER(); } modifier onlyPauseGuardian() { @@ -75,7 +79,7 @@ contract StaticATokenLM is } function canPause(address actor) public view returns (bool) { - return IACLManager(POOL.ADDRESSES_PROVIDER().getACLManager()).isEmergencyAdmin(actor); + return IACLManager(POOL_ADDRESSES_PROVIDER.getACLManager()).isEmergencyAdmin(actor); } ///@inheritdoc IInitializableStaticATokenLM @@ -103,7 +107,7 @@ contract StaticATokenLM is /// @inheritdoc IRescuable function whoCanRescue() public view override returns (address) { - return POOL.ADDRESSES_PROVIDER().getACLAdmin(); + return POOL_ADDRESSES_PROVIDER.getACLAdmin(); } ///@inheritdoc IStaticATokenLM @@ -468,6 +472,15 @@ contract StaticATokenLM is return _withdraw(owner, receiver, shares, 0, withdrawFromAave); } + ///@inheritdoc IStaticATokenLM + function latestAnswer() external view returns (int256) { + return + int256( + (IAaveOracle(POOL_ADDRESSES_PROVIDER.getPriceOracle()).getAssetPrice(_aTokenUnderlying) * + POOL.getReserveNormalizedIncome(_aTokenUnderlying)) / 1e27 + ); + } + function _deposit( address depositor, address receiver, diff --git a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol index 18edaeb7..2fbdd9cf 100644 --- a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol +++ b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol @@ -219,4 +219,15 @@ interface IStaticATokenLM is IInitializableStaticATokenLM, IERC4626 { * @param paused boolean determining if the token should be paused or unpaused */ function setPaused(bool paused) external; + + /** + * @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/tests/periphery/static-a-token/StaticATokenLM.t.sol b/tests/periphery/static-a-token/StaticATokenLM.t.sol index 6f2ddaf4..a8a23c32 100644 --- a/tests/periphery/static-a-token/StaticATokenLM.t.sol +++ b/tests/periphery/static-a-token/StaticATokenLM.t.sol @@ -10,6 +10,7 @@ import {RayMathExplicitRounding} from '../../../src/periphery/contracts/librarie import {IStaticATokenLM} from '../../../src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol'; import {SigUtils} from '../../utils/SigUtils.sol'; import {BaseTest, TestnetERC20} from './TestBase.sol'; +import {IPool} from '../../../src/core/contracts/interfaces/IPool.sol'; contract StaticATokenLMTest is BaseTest { using RayMathExplicitRounding for uint256; @@ -48,6 +49,34 @@ contract StaticATokenLMTest is BaseTest { ); } + function test_latestAnswer_priceShouldBeEqualOnDefaultIndex() public { + vm.mockCall( + address(POOL), + abi.encodeWithSelector(IPool.getReserveNormalizedIncome.selector), + abi.encode(1e27) + ); + uint256 stataPrice = uint256(staticATokenLM.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(POOL), + abi.encodeWithSelector(IPool.getReserveNormalizedIncome.selector), + abi.encode(liquidityIndex) + ); + uint256 stataPrice = uint256(staticATokenLM.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); + } + function test_convertersAndPreviews() public view { uint128 amount = 5 ether; uint256 shares = staticATokenLM.convertToShares(amount); From 03ce79f6d890101b8bf3e7dcde4da3fb0d2b62b8 Mon Sep 17 00:00:00 2001 From: sakulstra Date: Thu, 8 Aug 2024 17:21:04 +0200 Subject: [PATCH 06/22] fix: add readme --- .../static-a-token/DeprecationGap.sol | 2 +- .../contracts/static-a-token/README.md | 53 +++++++++++++++++-- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/periphery/contracts/static-a-token/DeprecationGap.sol b/src/periphery/contracts/static-a-token/DeprecationGap.sol index c3b4b47b..cdc3652c 100644 --- a/src/periphery/contracts/static-a-token/DeprecationGap.sol +++ b/src/periphery/contracts/static-a-token/DeprecationGap.sol @@ -8,5 +8,5 @@ pragma solidity ^0.8.10; * https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/proxy/utils/Initializable.sol#L60 */ contract DeprecationGap { - uint256 internal __deprecated_initializable_gap; + uint256 internal __deprecated; } diff --git a/src/periphery/contracts/static-a-token/README.md b/src/periphery/contracts/static-a-token/README.md index f5dddf20..0ac5bd59 100644 --- a/src/periphery/contracts/static-a-token/README.md +++ b/src/periphery/contracts/static-a-token/README.md @@ -39,12 +39,39 @@ For this project, the security procedures applied/being finished are: ## Upgrade Notes Umbrella -- Interface inheritance has been changed so that `IStaticATokenLM` implements `IERC4626`, making it easier for integrators to work with the interface. -- The static A tokens are given a `rescuable`, which can be used by the ACL admin to rescue tokens locked to the contract. -- Permit params have been excluded from the METADEPOSIT_TYPEHASH as they are not necessary. Even if someone were to frontrun the permit via mempool observation the permit is wrapped in a `try..catch` to prevent griefing attacks. -- The static a token not implements pausability, which allows the ACL admin to pause all transfers. +### Inheritance -The storage layout diff was generated via: +Interface inheritance has been changed so that `IStaticATokenLM` implements `IERC4626`, making it easier for integrators to work with the interface. +The current `Initializable` has been removed in favor of the new [Initializable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/9a47a37c4b8ce2ac465e8656f31d32ac6fe26eaa/contracts/proxy/utils/Initializable.sol) following the [`ERC-7201`](https://eips.ethereum.org/EIPS/eip-7201) standard. +To account for the shift in storage, a new `DeprecationGap` has been introduced to maintain the remaining storage at the current position. + +### Misc + +Permit params have been excluded from the METADEPOSIT_TYPEHASH as they are not necessary. +Potential frontrunning of the permit via mempool observation is unavoidable, but due to wrapping the permit execution in a `try..catch` griefing is impossible. + +### Features + +#### Rescuable + +[Rescuable](https://github.com/bgd-labs/solidity-utils/blob/main/src/contracts/utils/Rescuable.sol) has been applied to +the `StaticATokenLM` which will allow the ACL_ADMIN of the corresponding `POOL` to rescue any tokens on the contract. + +#### Pausability + +The `StaticATokenLM` 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 `StaticATokenLM` implemented by 3th parties for improved UX/DX the `StaticATokenLM` now exposes `latestAnswer`. +`latestAnswer` returns the asset price priced as `underlying_price * excahngeRate`. +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 + +### Storage diff ``` git checkout main @@ -53,3 +80,19 @@ git checkout project-a forge inspect src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM storage-layout --pretty > reports/StaticATokenStorageAfter.md make git-diff before=reports/StaticATokenStorageBefore.md after=reports/StaticATokenStorageAfter.md out=StaticATokenStorageDiff ``` + +```diff +diff --git a/reports/StaticATokenStorageBefore.md b/reports/StaticATokenStorageAfter.md +index a7e3105..89e0967 100644 +--- a/reports/StaticATokenStorageBefore.md ++++ b/reports/StaticATokenStorageAfter.md +@@ -1,7 +1,6 @@ + | Name | Type | Slot | Offset | Bytes | Contract | + | ------------------ | ------------------------------------------------------------------------------ | ---- | ------ | ----- | ------------------------------------------------------------------------ | +-| \_initialized | uint8 | 0 | 0 | 1 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | +-| \_initializing | bool | 0 | 1 | 1 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | ++| \_\_deprecated | uint256 | 0 | 0 | 32 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | + | name | string | 1 | 0 | 32 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | + | symbol | string | 2 | 0 | 32 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | + | decimals | uint8 | 3 | 0 | 1 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | +``` From 1214bccced62cfcc69df2249d0b984462269a22b Mon Sep 17 00:00:00 2001 From: sakulstra Date: Thu, 8 Aug 2024 17:27:14 +0200 Subject: [PATCH 07/22] docs: note on upgrade --- src/periphery/contracts/static-a-token/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/periphery/contracts/static-a-token/README.md b/src/periphery/contracts/static-a-token/README.md index 0ac5bd59..bb633af9 100644 --- a/src/periphery/contracts/static-a-token/README.md +++ b/src/periphery/contracts/static-a-token/README.md @@ -60,7 +60,7 @@ the `StaticATokenLM` which will allow the ACL_ADMIN of the corresponding `POOL` #### Pausability The `StaticATokenLM` 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.. +As long as the vault is paused, minting, burning, transfers and claiming of rewards is impossible. #### LatestAnswer @@ -96,3 +96,10 @@ index a7e3105..89e0967 100644 | symbol | string | 2 | 0 | 32 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | | decimals | uint8 | 3 | 0 | 1 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | ``` + +### Umbrella upgrade plan + +The upgrade can be performed independent(before) from any umbrella changes as it has no dependencies. +The upgrade will need to: +- upgrade the `StaticATokenFactory` with a new version, replacing the `STATIC_A_TOKEN_IMPL`. +- upgrade existing stata tokens via `upgradeToAndCall` to the new implementation. While the tokens are already initialized, due to changing the `Initializable` the corresponding storage is lost. From 93a819df30028378805af8bee0cbfa8596607e3b Mon Sep 17 00:00:00 2001 From: sakulstra Date: Fri, 9 Aug 2024 14:52:02 +0200 Subject: [PATCH 08/22] fix: move around tests --- .../contracts/static-a-token/README.md | 2 + tests/periphery/static-a-token/Rewards.t.sol | 156 ++++++++++++++++++ .../static-a-token/StaticATokenLM.t.sol | 130 --------------- 3 files changed, 158 insertions(+), 130 deletions(-) create mode 100644 tests/periphery/static-a-token/Rewards.t.sol diff --git a/src/periphery/contracts/static-a-token/README.md b/src/periphery/contracts/static-a-token/README.md index bb633af9..1b5ca9f7 100644 --- a/src/periphery/contracts/static-a-token/README.md +++ b/src/periphery/contracts/static-a-token/README.md @@ -67,6 +67,7 @@ As long as the vault is paused, minting, burning, transfers and claiming of rewa While there are already mechanisms to price the `StaticATokenLM` implemented by 3th parties for improved UX/DX the `StaticATokenLM` now exposes `latestAnswer`. `latestAnswer` returns the asset price priced as `underlying_price * excahngeRate`. 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 @@ -101,5 +102,6 @@ index a7e3105..89e0967 100644 The upgrade can be performed independent(before) from any umbrella changes as it has no dependencies. The upgrade will need to: + - upgrade the `StaticATokenFactory` with a new version, replacing the `STATIC_A_TOKEN_IMPL`. - upgrade existing stata tokens via `upgradeToAndCall` to the new implementation. While the tokens are already initialized, due to changing the `Initializable` the corresponding storage is lost. diff --git a/tests/periphery/static-a-token/Rewards.t.sol b/tests/periphery/static-a-token/Rewards.t.sol new file mode 100644 index 00000000..e21c00b2 --- /dev/null +++ b/tests/periphery/static-a-token/Rewards.t.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {AToken} from '../../../src/core/contracts/protocol/tokenization/AToken.sol'; +import {IERC20} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; +import {BaseTest} from './TestBase.sol'; + +contract StataTokenRewardsTest is BaseTest { + function setUp() public override { + super.setUp(); + + _configureLM(); + + vm.startPrank(user); + } + + function test_claimableRewards() external { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + _depositAToken(amountToDeposit, user); + + vm.warp(block.timestamp + 200); + uint256 claimable = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); + assertEq(claimable, 200 * 0.00385 ether); + } + + // 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); + assertGe(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); + assertGe(AToken(UNDERLYING).balanceOf(user), 5 ether); + } +} diff --git a/tests/periphery/static-a-token/StaticATokenLM.t.sol b/tests/periphery/static-a-token/StaticATokenLM.t.sol index a8a23c32..ccc4f74b 100644 --- a/tests/periphery/static-a-token/StaticATokenLM.t.sol +++ b/tests/periphery/static-a-token/StaticATokenLM.t.sol @@ -208,136 +208,6 @@ contract StaticATokenLMTest is BaseTest { 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); From 1c7a6ffd39988c27abf62894ef850e1df6526c26 Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 12 Aug 2024 10:54:09 +0200 Subject: [PATCH 09/22] feat: use openzeppelin & remove meta txns (for now) (#5) * fix: remove deprecation gap * fix: migrate to oz * oz: use erc20pausableupgradable * fix: move 4626 to oz as well * fix: cleanup * feat: add failing rewards test * fix: use oz * fix: cleanup tests * fix: remove deprecated interfaces * fix: lint * fix: remove unused revision * fix: address comments * fix: alter function ordering a bit --- .../dependencies/openzeppelin/ECDSA.sol | 180 -------- .../contracts/dependencies/solmate/ERC20.sol | 207 ---------- .../static-a-token/DeprecationGap.sol | 12 - .../contracts/static-a-token/StataOracle.sol | 2 +- .../static-a-token/StaticATokenLM.sol | 387 ++++++------------ .../static-a-token/interfaces/IERC4626.sol | 241 ----------- .../interfaces/IStaticATokenLM.sol | 63 +-- tests/periphery/static-a-token/Pausable.t.sol | 2 +- tests/periphery/static-a-token/Rewards.t.sol | 45 +- .../static-a-token/StataOracle.t.sol | 31 ++ .../static-a-token/StaticATokenLM.t.sol | 93 +---- .../StaticATokenMetaTransactions.t.sol | 245 ----------- tests/periphery/static-a-token/TestBase.sol | 5 +- 13 files changed, 237 insertions(+), 1276 deletions(-) delete mode 100644 src/periphery/contracts/dependencies/openzeppelin/ECDSA.sol delete mode 100644 src/periphery/contracts/dependencies/solmate/ERC20.sol delete mode 100644 src/periphery/contracts/static-a-token/DeprecationGap.sol delete mode 100644 src/periphery/contracts/static-a-token/interfaces/IERC4626.sol delete mode 100644 tests/periphery/static-a-token/StaticATokenMetaTransactions.t.sol 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/static-a-token/DeprecationGap.sol b/src/periphery/contracts/static-a-token/DeprecationGap.sol deleted file mode 100644 index cdc3652c..00000000 --- a/src/periphery/contracts/static-a-token/DeprecationGap.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -/** - * This contract adds a single slot gap - * The slot is required to account for the now deprecated Initializable. - * The new version of Initializable uses erc7201, so it no longer occupies the first slot. - * https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/proxy/utils/Initializable.sol#L60 - */ -contract DeprecationGap { - uint256 internal __deprecated; -} diff --git a/src/periphery/contracts/static-a-token/StataOracle.sol b/src/periphery/contracts/static-a-token/StataOracle.sol index d1d7e7ca..1a715b07 100644 --- a/src/periphery/contracts/static-a-token/StataOracle.sol +++ b/src/periphery/contracts/static-a-token/StataOracle.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.10; +import {IERC4626} from '@openzeppelin/contracts/interfaces/IERC4626.sol'; 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 diff --git a/src/periphery/contracts/static-a-token/StaticATokenLM.sol b/src/periphery/contracts/static-a-token/StaticATokenLM.sol index 80f40ef4..d546fbb5 100644 --- a/src/periphery/contracts/static-a-token/StaticATokenLM.sol +++ b/src/periphery/contracts/static-a-token/StaticATokenLM.sol @@ -1,5 +1,15 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; +pragma solidity ^0.8.17; + +import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol'; +import {ERC20Upgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol'; +import {ERC20PermitUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol'; +import {ERC20PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PausableUpgradeable.sol'; +import {ERC4626Upgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC4626Upgradeable.sol'; +import {IERC4626} from 'openzeppelin-contracts/contracts/interfaces/IERC4626.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/interfaces/IERC20.sol'; +import {IERC20Metadata} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; import {IPool} from '../../../core/contracts/interfaces/IPool.sol'; import {IPoolAddressesProvider} from '../../../core/contracts/interfaces/IPoolAddressesProvider.sol'; @@ -10,21 +20,13 @@ import {MathUtils} from '../../../core/contracts/protocol/libraries/math/MathUti import {IACLManager} from '../../../core/contracts/interfaces/IACLManager.sol'; import {IRewardsController} from '../rewards/interfaces/IRewardsController.sol'; import {SafeCast} from 'solidity-utils/contracts/oz-common/SafeCast.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 {IRescuable, Rescuable} from 'solidity-utils/contracts/utils/Rescuable.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'; -import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol'; -import {DeprecationGap} from './DeprecationGap.sol'; /** * @title StaticATokenLM @@ -34,37 +36,29 @@ import {DeprecationGap} from './DeprecationGap.sol'; * @author BGD labs */ contract StaticATokenLM is - DeprecationGap, - ERC20('STATIC__aToken_IMPL', 'STATIC__aToken_IMPL', 18), + ERC20Upgradeable, + ERC20PermitUpgradeable, + ERC20PausableUpgradeable, + ERC4626Upgradeable, IStaticATokenLM, - Rescuable, - PausableUpgradeable + Rescuable { 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)' - ); - 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 = 3; - IPool public immutable POOL; IPoolAddressesProvider immutable POOL_ADDRESSES_PROVIDER; IRewardsController public immutable INCENTIVES_CONTROLLER; IERC20 internal _aToken; address internal _aTokenUnderlying; + uint8 internal _decimals; address[] internal _rewardTokens; - mapping(address => RewardIndexCache) internal _startIndex; - mapping(address => mapping(address => UserRewardsData)) internal _userRewardsData; + mapping(address user => RewardIndexCache cache) internal _startIndex; + mapping(address user => mapping(address reward => UserRewardsData cache)) + internal _userRewardsData; constructor(IPool pool, IRewardsController rewardsController) { _disableInitializers(); @@ -73,15 +67,6 @@ contract StaticATokenLM is POOL_ADDRESSES_PROVIDER = pool.ADDRESSES_PROVIDER(); } - modifier onlyPauseGuardian() { - if (!canPause(msg.sender)) revert OnlyPauseGuardian(msg.sender); - _; - } - - function canPause(address actor) public view returns (bool) { - return IACLManager(POOL_ADDRESSES_PROVIDER.getACLManager()).isEmergencyAdmin(actor); - } - ///@inheritdoc IInitializableStaticATokenLM function initialize( address newAToken, @@ -89,11 +74,10 @@ contract StaticATokenLM is string calldata staticATokenSymbol ) external initializer { require(IAToken(newAToken).POOL() == address(POOL)); + __ERC20_init(staticATokenName, staticATokenSymbol); + __ERC20Permit_init(staticATokenName); _aToken = IERC20(newAToken); - - name = staticATokenName; - symbol = staticATokenSymbol; - decimals = IERC20Metadata(newAToken).decimals(); + _decimals = IERC20Metadata(address(_aToken)).decimals(); _aTokenUnderlying = IAToken(newAToken).UNDERLYING_ASSET_ADDRESS(); IERC20(_aTokenUnderlying).forceApprove(address(POOL), type(uint256).max); @@ -105,28 +89,25 @@ contract StaticATokenLM is emit Initialized(newAToken, staticATokenName, staticATokenSymbol); } - /// @inheritdoc IRescuable - function whoCanRescue() public view override returns (address) { - return POOL_ADDRESSES_PROVIDER.getACLAdmin(); + modifier onlyPauseGuardian() { + if (!canPause(msg.sender)) revert OnlyPauseGuardian(msg.sender); + _; } ///@inheritdoc IStaticATokenLM - function setPaused(bool paused) external onlyPauseGuardian { - if (paused) _pause(); - else _unpause(); + function canPause(address actor) public view returns (bool) { + return IACLManager(POOL_ADDRESSES_PROVIDER.getACLManager()).isEmergencyAdmin(actor); } - ///@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 IRescuable + function whoCanRescue() public view override returns (address) { + return POOL_ADDRESSES_PROVIDER.getACLAdmin(); } - ///@inheritdoc IStaticATokenLM - function isRegisteredRewardToken(address reward) public view override returns (bool) { - return _startIndex[reward].isRegistered; + ///@inheritdoc IERC4626 + function deposit(uint256 assets, address receiver) public override returns (uint256) { + (uint256 shares, ) = _deposit(msg.sender, receiver, 0, assets, 0, true); + return shares; } ///@inheritdoc IStaticATokenLM @@ -140,146 +121,43 @@ contract StaticATokenLM is 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 - ) - ) - ) - ); - 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); - } + function mint(uint256 shares, address receiver) public override returns (uint256) { + (, uint256 assets) = _deposit(msg.sender, receiver, shares, 0, 0, true); - ///@inheritdoc IERC4626 - function previewMint(uint256 shares) public view virtual returns (uint256) { - return _convertToAssets(shares, Rounding.UP); + return assets; } ///@inheritdoc IERC4626 - function previewWithdraw(uint256 assets) public view virtual returns (uint256) { - return _convertToShares(assets, Rounding.UP); + function withdraw( + uint256 assets, + address receiver, + address owner + ) public override returns (uint256) { + (uint256 shares, ) = _withdraw(owner, receiver, 0, assets, true); + + return shares; } ///@inheritdoc IERC4626 - function previewDeposit(uint256 assets) public view virtual returns (uint256) { - return _convertToShares(assets, Rounding.DOWN); - } + function redeem( + uint256 shares, + address receiver, + address owner + ) public override returns (uint256) { + (, uint256 assets) = _withdraw(owner, receiver, shares, 0, true); - ///@inheritdoc IStaticATokenLM - function rate() public view returns (uint256) { - return POOL.getReserveNormalizedIncome(_aTokenUnderlying); + return assets; } ///@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); + function redeem( + uint256 shares, + address receiver, + address owner, + bool withdrawFromAave + ) external returns (uint256, uint256) { + return _withdraw(owner, receiver, shares, 0, withdrawFromAave); } ///@inheritdoc IStaticATokenLM @@ -305,6 +183,42 @@ contract StaticATokenLM is _claimRewardsOnBehalf(msg.sender, msg.sender, rewards); } + /// @inheritdoc IERC20Metadata + function decimals() public view override(ERC20Upgradeable, ERC4626Upgradeable) returns (uint8) { + return _decimals; + } + + ///@inheritdoc IStaticATokenLM + function setPaused(bool paused) external onlyPauseGuardian { + if (paused) _pause(); + else _unpause(); + } + + ///@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 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 isRegisteredRewardToken(address reward) public view override returns (bool) { + return _startIndex[reward].isRegistered; + } + ///@inheritdoc IStaticATokenLM function getCurrentRewardsIndex(address reward) public view returns (uint256) { if (address(reward) == address(0)) { @@ -328,7 +242,7 @@ contract StaticATokenLM is ///@inheritdoc IStaticATokenLM function getClaimableRewards(address user, address reward) external view returns (uint256) { - return _getClaimableRewards(user, reward, balanceOf[user], getCurrentRewardsIndex(reward)); + return _getClaimableRewards(user, reward, balanceOf(user), getCurrentRewardsIndex(reward)); } ///@inheritdoc IStaticATokenLM @@ -336,11 +250,21 @@ contract StaticATokenLM is return _userRewardsData[user][reward].unclaimedRewards; } + ///@inheritdoc IStaticATokenLM + function rate() public view returns (uint256) { + return POOL.getReserveNormalizedIncome(_aTokenUnderlying); + } + ///@inheritdoc IERC4626 - function asset() external view returns (address) { + function asset() public view override returns (address) { return address(_aTokenUnderlying); } + ///@inheritdoc IERC4626 + function totalAssets() public view override returns (uint256) { + return _aToken.balanceOf(address(this)); + } + ///@inheritdoc IStaticATokenLM function aToken() external view returns (IERC20) { return _aToken; @@ -352,37 +276,32 @@ contract StaticATokenLM is } ///@inheritdoc IERC4626 - function totalAssets() external view returns (uint256) { - return _aToken.balanceOf(address(this)); - } - - ///@inheritdoc IERC4626 - function convertToShares(uint256 assets) external view returns (uint256) { + function convertToShares(uint256 assets) public view override returns (uint256) { return _convertToShares(assets, Rounding.DOWN); } ///@inheritdoc IERC4626 - function convertToAssets(uint256 shares) external view returns (uint256) { + function convertToAssets(uint256 shares) public view override returns (uint256) { return _convertToAssets(shares, Rounding.DOWN); } ///@inheritdoc IERC4626 - function maxMint(address) public view virtual returns (uint256) { + 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, Rounding.DOWN); } ///@inheritdoc IERC4626 - function maxWithdraw(address owner) public view virtual returns (uint256) { + function maxWithdraw(address owner) public view override returns (uint256) { uint256 shares = maxRedeem(owner); return _convertToAssets(shares, Rounding.DOWN); } ///@inheritdoc IERC4626 - function maxRedeem(address owner) public view virtual returns (uint256) { + function maxRedeem(address owner) public view override returns (uint256) { address cachedATokenUnderlying = _aTokenUnderlying; - DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(cachedATokenUnderlying); + DataTypes.ReserveData memory reserveData = POOL.getReserveDataExtended(cachedATokenUnderlying); // if paused or inactive users cannot withdraw underlying if ( @@ -394,10 +313,10 @@ contract StaticATokenLM is // otherwise users can withdraw up to the available amount uint256 underlyingTokenBalanceInShares = _convertToShares( - IERC20(cachedATokenUnderlying).balanceOf(reserveData.aTokenAddress), + reserveData.virtualUnderlyingBalance, Rounding.DOWN ); - uint256 cachedUserBalance = balanceOf[owner]; + uint256 cachedUserBalance = balanceOf(owner); return underlyingTokenBalanceInShares >= cachedUserBalance ? cachedUserBalance @@ -405,7 +324,7 @@ contract StaticATokenLM is } ///@inheritdoc IERC4626 - function maxDeposit(address) public view virtual returns (uint256) { + function maxDeposit(address) public view override returns (uint256) { DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(_aTokenUnderlying); // if inactive, paused or frozen users cannot deposit underlying @@ -427,51 +346,6 @@ contract StaticATokenLM is 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); - } - ///@inheritdoc IStaticATokenLM function latestAnswer() external view returns (int256) { return @@ -549,9 +423,7 @@ contract StaticATokenLM is } 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; + _spendAllowance(owner, msg.sender, shares); } _burn(owner, shares); @@ -572,7 +444,11 @@ contract StaticATokenLM is * @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 whenNotPaused { + function _update( + address from, + address to, + uint256 amount + ) internal override(ERC20Upgradeable, ERC20PausableUpgradeable) whenNotPaused { for (uint256 i = 0; i < _rewardTokens.length; i++) { address rewardToken = address(_rewardTokens[i]); uint256 rewardsIndex = getCurrentRewardsIndex(rewardToken); @@ -583,6 +459,7 @@ contract StaticATokenLM is _updateUser(to, rewardsIndex, rewardToken); } } + super._update(from, to, amount); } /** @@ -592,7 +469,7 @@ contract StaticATokenLM is * @param rewardToken The address of the reward token */ function _updateUser(address user, uint256 currentRewardsIndex, address rewardToken) internal { - uint256 balance = balanceOf[user]; + uint256 balance = balanceOf(user); if (balance > 0) { _userRewardsData[user][rewardToken].unclaimedRewards = _getClaimableRewards( user, @@ -610,19 +487,17 @@ contract StaticATokenLM is * @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) { + uint256 currentRewardsIndex + ) internal view returns (uint256) { if (balance == 0) { return 0; } - return (balance * (currentRewardsIndex - rewardsIndexOnLastInteraction)) / assetUnit; + return (balance * (currentRewardsIndex - rewardsIndexOnLastInteraction)) / 10 ** decimals(); } /** @@ -642,7 +517,6 @@ contract StaticATokenLM is 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( @@ -650,8 +524,7 @@ contract StaticATokenLM is currentUserRewardsData.rewardsIndexOnLastInteraction == 0 ? rewardsIndexCache.lastUpdatedIndex : currentUserRewardsData.rewardsIndexOnLastInteraction, - currentRewardsIndex, - assetUnit + currentRewardsIndex ); } @@ -671,7 +544,7 @@ contract StaticATokenLM is continue; } uint256 currentRewardsIndex = getCurrentRewardsIndex(rewards[i]); - uint256 balance = balanceOf[onBehalfOf]; + uint256 balance = balanceOf(onBehalfOf); uint256 userReward = _getClaimableRewards( onBehalfOf, rewards[i], 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/IStaticATokenLM.sol b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol index 2fbdd9cf..026cad99 100644 --- a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol +++ b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol @@ -1,11 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; -import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; -import {IERC4626} from './IERC4626.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/interfaces/IERC20.sol'; import {IInitializableStaticATokenLM} from './IInitializableStaticATokenLM.sol'; -interface IStaticATokenLM is IInitializableStaticATokenLM, IERC4626 { +interface IStaticATokenLM is IInitializableStaticATokenLM { struct SignatureParams { uint8 v; bytes32 r; @@ -69,57 +68,6 @@ interface IStaticATokenLM is IInitializableStaticATokenLM, IERC4626 { 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 @@ -214,6 +162,13 @@ interface IStaticATokenLM is IInitializableStaticATokenLM, IERC4626 { */ function isRegisteredRewardToken(address reward) external view returns (bool); + /** + * @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 diff --git a/tests/periphery/static-a-token/Pausable.t.sol b/tests/periphery/static-a-token/Pausable.t.sol index 966a33ac..59a24dec 100644 --- a/tests/periphery/static-a-token/Pausable.t.sol +++ b/tests/periphery/static-a-token/Pausable.t.sol @@ -15,7 +15,7 @@ import {IStaticATokenLM} from '../../../src/periphery/contracts/static-a-token/i import {SigUtils} from '../../utils/SigUtils.sol'; import {BaseTest, TestnetERC20} from './TestBase.sol'; -contract Pausable is BaseTest { +contract StataPausableTest is BaseTest { using RayMathExplicitRounding for uint256; function test_setPaused_shouldRevertForInvalidCaller(address actor) external { diff --git a/tests/periphery/static-a-token/Rewards.t.sol b/tests/periphery/static-a-token/Rewards.t.sol index e21c00b2..36a13dd8 100644 --- a/tests/periphery/static-a-token/Rewards.t.sol +++ b/tests/periphery/static-a-token/Rewards.t.sol @@ -5,7 +5,7 @@ import {AToken} from '../../../src/core/contracts/protocol/tokenization/AToken.s import {IERC20} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; import {BaseTest} from './TestBase.sol'; -contract StataTokenRewardsTest is BaseTest { +contract StataRewardsTest is BaseTest { function setUp() public override { super.setUp(); @@ -153,4 +153,47 @@ contract StataTokenRewardsTest is BaseTest { assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), 0); assertGe(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); + } } diff --git a/tests/periphery/static-a-token/StataOracle.t.sol b/tests/periphery/static-a-token/StataOracle.t.sol index 3d6b6622..df569d9c 100644 --- a/tests/periphery/static-a-token/StataOracle.t.sol +++ b/tests/periphery/static-a-token/StataOracle.t.sol @@ -4,6 +4,7 @@ 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'; +import {IPool} from '../../../src/core/contracts/interfaces/IPool.sol'; contract StataOracleTest is BaseTest { StataOracle public oracle; @@ -16,6 +17,7 @@ contract StataOracleTest is BaseTest { contracts.poolConfiguratorProxy.setSupplyCap(UNDERLYING, 1_000_000); } + // ### tests for the dedicated oracle aggregator function test_assetPrice() public view { uint256 stataPrice = oracle.getAssetPrice(address(staticATokenLM)); uint256 underlyingPrice = contracts.aaveOracle.getAssetPrice(UNDERLYING); @@ -54,4 +56,33 @@ contract StataOracleTest is BaseTest { (assets / 1e18) + 1 // there can be imprecision of 1 wei, which will accumulate for each asset ); } + + // ### tests for the token internal oracle + function test_latestAnswer_priceShouldBeEqualOnDefaultIndex() public { + vm.mockCall( + address(POOL), + abi.encodeWithSelector(IPool.getReserveNormalizedIncome.selector), + abi.encode(1e27) + ); + uint256 stataPrice = uint256(staticATokenLM.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(POOL), + abi.encodeWithSelector(IPool.getReserveNormalizedIncome.selector), + abi.encode(liquidityIndex) + ); + uint256 stataPrice = uint256(staticATokenLM.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); + } } diff --git a/tests/periphery/static-a-token/StaticATokenLM.t.sol b/tests/periphery/static-a-token/StaticATokenLM.t.sol index ccc4f74b..57009857 100644 --- a/tests/periphery/static-a-token/StaticATokenLM.t.sol +++ b/tests/periphery/static-a-token/StaticATokenLM.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.10; import {IRescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; +import {ERC20PermitUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol'; import {Initializable} from 'openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol'; import {AToken} from '../../../src/core/contracts/protocol/tokenization/AToken.sol'; import {DataTypes} from '../../../src/core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; @@ -49,34 +50,6 @@ contract StaticATokenLMTest is BaseTest { ); } - function test_latestAnswer_priceShouldBeEqualOnDefaultIndex() public { - vm.mockCall( - address(POOL), - abi.encodeWithSelector(IPool.getReserveNormalizedIncome.selector), - abi.encode(1e27) - ); - uint256 stataPrice = uint256(staticATokenLM.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(POOL), - abi.encodeWithSelector(IPool.getReserveNormalizedIncome.selector), - abi.encode(liquidityIndex) - ); - uint256 stataPrice = uint256(staticATokenLM.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); - } - function test_convertersAndPreviews() public view { uint128 amount = 5 ether; uint256 shares = staticATokenLM.convertToShares(amount); @@ -208,49 +181,6 @@ contract StaticATokenLMTest is BaseTest { staticATokenLM.mint(amountToDeposit, user); } - 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 */ @@ -398,7 +328,7 @@ contract StaticATokenLMTest is BaseTest { bytes32 permitDigest = SigUtils.getTypedDataHash( permit, - staticATokenLM.PERMIT_TYPEHASH(), + PERMIT_TYPEHASH, staticATokenLM.DOMAIN_SEPARATOR() ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); @@ -422,12 +352,17 @@ contract StaticATokenLMTest is BaseTest { bytes32 permitDigest = SigUtils.getTypedDataHash( permit, - staticATokenLM.PERMIT_TYPEHASH(), + PERMIT_TYPEHASH, staticATokenLM.DOMAIN_SEPARATOR() ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); - vm.expectRevert('PERMIT_DEADLINE_EXPIRED'); + vm.expectRevert( + abi.encodeWithSelector( + ERC20PermitUpgradeable.ERC2612ExpiredSignature.selector, + permit.deadline + ) + ); staticATokenLM.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); } @@ -442,12 +377,18 @@ contract StaticATokenLMTest is BaseTest { bytes32 permitDigest = SigUtils.getTypedDataHash( permit, - staticATokenLM.PERMIT_TYPEHASH(), + PERMIT_TYPEHASH, staticATokenLM.DOMAIN_SEPARATOR() ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); - vm.expectRevert('INVALID_SIGNER'); + vm.expectRevert( + abi.encodeWithSelector( + ERC20PermitUpgradeable.ERC2612InvalidSigner.selector, + user, + permit.owner + ) + ); staticATokenLM.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); } 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 dc8c68d2..00000000 --- a/tests/periphery/static-a-token/StaticATokenMetaTransactions.t.sol +++ /dev/null @@ -1,245 +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.MetaDepositParams memory metaDepositParams = SigUtils.MetaDepositParams({ - depositor: user, - receiver: spender, - assets: 1e6, - referralCode: 0, - fromUnderlying: true, - nonce: staticATokenLM.nonces(user), - deadline: block.timestamp + 1 days - }); - bytes32 digest = SigUtils.getTypedDepositHash( - metaDepositParams, - 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(metaDepositParams.assets); - staticATokenLM.metaDeposit( - metaDepositParams.depositor, - metaDepositParams.receiver, - metaDepositParams.assets, - metaDepositParams.referralCode, - metaDepositParams.fromUnderlying, - metaDepositParams.deadline, - permitParams, - sigParams - ); - - assertEq(staticATokenLM.balanceOf(metaDepositParams.receiver), 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.value, - permit.deadline, - pV, - pR, - pS - ); - - // generate combined permit - SigUtils.MetaDepositParams memory metaDepositParams = SigUtils.MetaDepositParams({ - depositor: user, - receiver: spender, - assets: permit.value, - referralCode: 0, - fromUnderlying: true, - nonce: staticATokenLM.nonces(user), - deadline: permit.deadline - }); - (uint8 v, bytes32 r, bytes32 s) = vm.sign( - userPrivateKey, - SigUtils.getTypedDepositHash( - metaDepositParams, - staticATokenLM.METADEPOSIT_TYPEHASH(), - staticATokenLM.DOMAIN_SEPARATOR() - ) - ); - - IStaticATokenLM.SignatureParams memory sigParams = IStaticATokenLM.SignatureParams(v, r, s); - - uint256 previewDeposit = staticATokenLM.previewDeposit(metaDepositParams.assets); - uint256 shares = staticATokenLM.metaDeposit( - metaDepositParams.depositor, - metaDepositParams.receiver, - metaDepositParams.assets, - metaDepositParams.referralCode, - metaDepositParams.fromUnderlying, - metaDepositParams.deadline, - permitParams, - sigParams - ); - assertEq(shares, previewDeposit); - assertEq(staticATokenLM.balanceOf(metaDepositParams.receiver), 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.value, - permit.deadline, - pV, - pR, - pS - ); - - // generate combined permit - SigUtils.MetaDepositParams memory metaDepositParams = SigUtils.MetaDepositParams({ - depositor: user, - receiver: spender, - assets: permit.value, - referralCode: 0, - fromUnderlying: false, - nonce: staticATokenLM.nonces(user), - deadline: permit.deadline - }); - bytes32 digest = SigUtils.getTypedDepositHash( - metaDepositParams, - 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(metaDepositParams.assets); - - staticATokenLM.metaDeposit( - metaDepositParams.depositor, - metaDepositParams.receiver, - metaDepositParams.assets, - metaDepositParams.referralCode, - metaDepositParams.fromUnderlying, - metaDepositParams.deadline, - permitParams, - sigParams - ); - - assertEq(staticATokenLM.balanceOf(metaDepositParams.receiver), previewDeposit); - } - - function test_metaWithdraw() public { - uint128 amountToDeposit = 5e6; - _fundUser(amountToDeposit, user); - - _depositAToken(amountToDeposit, user); - - SigUtils.MetaWithdrawParams memory permit = SigUtils.MetaWithdrawParams({ - 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/TestBase.sol b/tests/periphery/static-a-token/TestBase.sol index b20aab9c..aa00e0ca 100644 --- a/tests/periphery/static-a-token/TestBase.sol +++ b/tests/periphery/static-a-token/TestBase.sol @@ -10,12 +10,15 @@ import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent- 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 {StaticATokenLM, IStaticATokenLM, IERC20, IERC20Metadata} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; import {IAToken} from '../../../src/core/contracts/interfaces/IAToken.sol'; import {TestnetProcedures, TestnetERC20} from '../../utils/TestnetProcedures.sol'; import {DataTypes} from '../../../src/core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; abstract contract BaseTest is TestnetProcedures { + bytes32 internal constant PERMIT_TYPEHASH = + keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)'); + address constant OWNER = address(1234); address public constant EMISSION_ADMIN = address(25); From 63dc7f26cc612feb2c2addea670a55c74c862202 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 16 Aug 2024 15:58:36 +0300 Subject: [PATCH 10/22] feat: move logic to oz and rework all tests (#7) * fix: remove deprecation gap * fix: migrate to oz * oz: use erc20pausableupgradable * fix: move 4626 to oz as well * fix: cleanup * feat: add failing rewards test * fix: use oz * fix: cleanup tests * fix: remove deprecated interfaces * fix: lint * fix: remove unused revision * fix: address comments * fix: alter function ordering a bit * More OZ logic on stata * add missing virtual * Separate Stata4626 (#8) * Separate Stata4626 * change to erc7201 * regenerated storage location * change latestAnswer calculation logic * DRAFT: Refactoring in extensions style * add initializer * remove unused params at __Stata4626_init * remove RayMathExplicitRounding * regenerated ERC20AaveLMStorageLocation * add RAY constant * remove IInitializableStata4626LM * depositWithPermit * disclamer on _update overload * some descriptions cleanup * change require to revert * add comment to latestAnswer calc * add comment to latestAnswer calc -1 * make ERC20AaveLMUpgradable abstract * update license * rename merger and 4626 contracts * change Upgradable to Upgradeable * move _disableInitializers into StataTokenV2 * rename IStata4626 to IERC4626StataToken * rename init on ERC4626StataToken * Changes on stata initializations, to follow more strict guidelines * Changes to make stata more consistent with using ERC20 extensions * Fix on function called on initialize of stata * feat: improved tests * fix: update test * feat: add erc4626 tests * fix: migrate some more tests * fix: improve tests * refactor: move to dedicated files * feat: improve tests * fix typo * feat: add permit tests * fix: linting * feat: improved docs * fix: typos * fix: use internal function --------- Co-authored-by: eboado Co-authored-by: sakulstra --------- Co-authored-by: sakulstra Co-authored-by: eboado --- remappings.txt | 4 +- .../misc/DeployAaveV3MarketBatchedBase.sol | 2 +- .../procedures/AaveV3HelpersProcedureTwo.sol | 4 +- .../libraries/RayMathExplicitRounding.sol | 42 -- .../static-a-token/ERC20AaveLMUpgradeable.sol | 305 +++++++++ .../ERC4626StataTokenUpgradeable.sol | 282 ++++++++ .../contracts/static-a-token/README.md | 69 +- .../contracts/static-a-token/StataOracle.sol | 41 -- .../contracts/static-a-token/StataTokenV2.sol | 84 +++ .../static-a-token/StaticATokenErrors.sol | 14 - .../static-a-token/StaticATokenFactory.sol | 14 +- .../static-a-token/StaticATokenLM.sol | 622 ------------------ .../interfaces/IERC20AaveLM.sol | 106 +++ .../interfaces/IERC4626StataToken.sol | 71 ++ .../IInitializableStaticATokenLM.sol | 32 - .../interfaces/IStataOracle.sol | 31 - .../interfaces/IStataTokenV2.sol | 20 + .../interfaces/IStaticATokenFactory.sol | 2 + .../interfaces/IStaticATokenLM.sol | 188 ------ tests/DeploymentsGasLimits.t.sol | 2 +- tests/core/Pool.t.sol | 14 +- .../core/PoolConfigurator.upgradeabilty.t.sol | 2 +- .../ERC20AaveLMUpgradable.t.sol | 404 ++++++++++++ .../ERC4626StataTokenUpgradeable.t.sol | 477 ++++++++++++++ tests/periphery/static-a-token/Pausable.t.sol | 125 ---- tests/periphery/static-a-token/Rewards.t.sol | 199 ------ .../static-a-token/StataOracle.t.sol | 88 --- .../static-a-token/StataTokenV2Getters.sol | 35 + .../static-a-token/StataTokenV2Pausable.t.sol | 108 +++ .../static-a-token/StataTokenV2Permit.sol | 83 +++ .../static-a-token/StataTokenV2Rescuable.sol | 31 + .../static-a-token/StaticATokenLM.t.sol | 427 ------------ .../static-a-token/StaticATokenNoLM.t.sol | 50 -- tests/periphery/static-a-token/TestBase.sol | 109 +-- tests/utils/SigUtils.sol | 81 +-- 35 files changed, 2093 insertions(+), 2075 deletions(-) delete mode 100644 src/periphery/contracts/libraries/RayMathExplicitRounding.sol create mode 100644 src/periphery/contracts/static-a-token/ERC20AaveLMUpgradeable.sol create mode 100644 src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol delete mode 100644 src/periphery/contracts/static-a-token/StataOracle.sol create mode 100644 src/periphery/contracts/static-a-token/StataTokenV2.sol delete mode 100644 src/periphery/contracts/static-a-token/StaticATokenErrors.sol delete mode 100644 src/periphery/contracts/static-a-token/StaticATokenLM.sol create mode 100644 src/periphery/contracts/static-a-token/interfaces/IERC20AaveLM.sol create mode 100644 src/periphery/contracts/static-a-token/interfaces/IERC4626StataToken.sol delete mode 100644 src/periphery/contracts/static-a-token/interfaces/IInitializableStaticATokenLM.sol delete mode 100644 src/periphery/contracts/static-a-token/interfaces/IStataOracle.sol create mode 100644 src/periphery/contracts/static-a-token/interfaces/IStataTokenV2.sol delete mode 100644 src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol create mode 100644 tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol create mode 100644 tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol delete mode 100644 tests/periphery/static-a-token/Pausable.t.sol delete mode 100644 tests/periphery/static-a-token/Rewards.t.sol delete mode 100644 tests/periphery/static-a-token/StataOracle.t.sol create mode 100644 tests/periphery/static-a-token/StataTokenV2Getters.sol create mode 100644 tests/periphery/static-a-token/StataTokenV2Pausable.t.sol create mode 100644 tests/periphery/static-a-token/StataTokenV2Permit.sol create mode 100644 tests/periphery/static-a-token/StataTokenV2Rescuable.sol delete mode 100644 tests/periphery/static-a-token/StaticATokenLM.t.sol delete mode 100644 tests/periphery/static-a-token/StaticATokenNoLM.t.sol 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..01b456e1 100644 --- a/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol +++ b/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol @@ -3,7 +3,7 @@ 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 {StataTokenV2} from 'aave-v3-periphery/contracts/static-a-token/StataTokenV2.sol'; import {StaticATokenFactory} from 'aave-v3-periphery/contracts/static-a-token/StaticATokenFactory.sol'; import {IErrors} from '../../interfaces/IErrors.sol'; @@ -17,7 +17,7 @@ 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( 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..097e22c5 --- /dev/null +++ b/src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol @@ -0,0 +1,282 @@ +// 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) public 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 + ) public 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) public returns (uint256) { + uint256 assets = previewRedeem(shares); + _withdraw(_msgSender(), receiver, owner, assets, shares, false); + + return assets; + } + + ///@inheritdoc IERC4626StataToken + function aToken() public 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 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 1b5ca9f7..b6bd003e 100644 --- a/src/periphery/contracts/static-a-token/README.md +++ b/src/periphery/contracts/static-a-token/README.md @@ -17,7 +17,7 @@ The static-a-token contains an [EIP-4626](https://eips.ethereum.org/EIPS/eip-462 - **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. -See [IStaticATokenLM.sol](./interfaces/IStaticATokenLM.sol) for detailed method documentation. +See [IStata4626LM.sol](./interfaces/IERC20AaveLM.sol) for detailed method documentation. ## Deployed Addresses @@ -37,71 +37,46 @@ 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 Umbrella +## Upgrade Notes StataTokenV2 ### Inheritance -Interface inheritance has been changed so that `IStaticATokenLM` implements `IERC4626`, making it easier for integrators to work with the interface. -The current `Initializable` has been removed in favor of the new [Initializable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/9a47a37c4b8ce2ac465e8656f31d32ac6fe26eaa/contracts/proxy/utils/Initializable.sol) following the [`ERC-7201`](https://eips.ethereum.org/EIPS/eip-7201) standard. -To account for the shift in storage, a new `DeprecationGap` has been introduced to maintain the remaining storage at the current position. +The `StaticATokenLM`(v1) was based on solmate. +To allow more flexibility the new `StataTokenV2`(v2) is based on [open-zeppelin-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. -### Misc +The `StataTokenV2` is seperated in 3 different contracts, where `StataTokenV2` inherits `ERC4626StataToken` and `ERC20AaveLM`. -Permit params have been excluded from the METADEPOSIT_TYPEHASH as they are not necessary. -Potential frontrunning of the permit via mempool observation is unavoidable, but due to wrapping the permit execution in a `try..catch` griefing is impossible. +- `ERC20AaveLM` is an abstract contract implementing the forwarding of liquidity mining from an underlying AaveERC20 - an ERC20 implementing `scaled` functions - to a wrapper contract. +- `ERC4626StataToken` is an abstract contract implementing the [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626) methods for an underlying aToken. In addition it adds a `latestAnswer`. +- `StataTokenV2` is the main contract stritching things together, while adding `Pausability`, `Rescuability`, `Permit` and the actual initialization. -### Features +### 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 `StaticATokenLM` which will allow the ACL_ADMIN of the corresponding `POOL` to rescue any tokens on the contract. +the `StataTokenV2` which will allow the ACL_ADMIN of the corresponding `POOL` to rescue any tokens on the contract. #### Pausability -The `StaticATokenLM` 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. +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 `StaticATokenLM` implemented by 3th parties for improved UX/DX the `StaticATokenLM` now exposes `latestAnswer`. -`latestAnswer` returns the asset price priced as `underlying_price * excahngeRate`. +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 - -### Storage diff - -``` -git checkout main -forge inspect src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM storage-layout --pretty > reports/StaticATokenStorageBefore.md -git checkout project-a -forge inspect src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM storage-layout --pretty > reports/StaticATokenStorageAfter.md -make git-diff before=reports/StaticATokenStorageBefore.md after=reports/StaticATokenStorageAfter.md out=StaticATokenStorageDiff -``` - -```diff -diff --git a/reports/StaticATokenStorageBefore.md b/reports/StaticATokenStorageAfter.md -index a7e3105..89e0967 100644 ---- a/reports/StaticATokenStorageBefore.md -+++ b/reports/StaticATokenStorageAfter.md -@@ -1,7 +1,6 @@ - | Name | Type | Slot | Offset | Bytes | Contract | - | ------------------ | ------------------------------------------------------------------------------ | ---- | ------ | ----- | ------------------------------------------------------------------------ | --| \_initialized | uint8 | 0 | 0 | 1 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | --| \_initializing | bool | 0 | 1 | 1 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | -+| \_\_deprecated | uint256 | 0 | 0 | 32 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | - | name | string | 1 | 0 | 32 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | - | symbol | string | 2 | 0 | 32 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | - | decimals | uint8 | 3 | 0 | 1 | src/periphery/contracts/static-a-token/StaticATokenLM.sol:StaticATokenLM | -``` - -### Umbrella upgrade plan - -The upgrade can be performed independent(before) from any umbrella changes as it has no dependencies. -The upgrade will need to: - -- upgrade the `StaticATokenFactory` with a new version, replacing the `STATIC_A_TOKEN_IMPL`. -- upgrade existing stata tokens via `upgradeToAndCall` to the new implementation. While the tokens are already initialized, due to changing the `Initializable` the corresponding storage is lost. 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 1a715b07..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 {IERC4626} from '@openzeppelin/contracts/interfaces/IERC4626.sol'; -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'; - -/** - * @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/StataTokenV2.sol b/src/periphery/contracts/static-a-token/StataTokenV2.sol new file mode 100644 index 00000000..0142fff3 --- /dev/null +++ b/src/periphery/contracts/static-a-token/StataTokenV2.sol @@ -0,0 +1,84 @@ +// 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'; + +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 index 4e0f8bd0..91af7f72 100644 --- a/src/periphery/contracts/static-a-token/StaticATokenFactory.sol +++ b/src/periphery/contracts/static-a-token/StaticATokenFactory.sol @@ -5,7 +5,7 @@ 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 {StataTokenV2} from './StataTokenV2.sol'; import {IStaticATokenFactory} from './interfaces/IStaticATokenFactory.sol'; /** @@ -47,18 +47,22 @@ contract StaticATokenFactory is Initializable, IStaticATokenFactory { 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'); + if (reserveData.aTokenAddress == address(0)) + revert NotListedUnderlying(reserveData.aTokenAddress); bytes memory symbol = abi.encodePacked( 'stat', - IERC20Metadata(reserveData.aTokenAddress).symbol() + IERC20Metadata(reserveData.aTokenAddress).symbol(), + 'v2' ); address staticAToken = TRANSPARENT_PROXY_FACTORY.createDeterministic( STATIC_A_TOKEN_IMPL, PROXY_ADMIN, abi.encodeWithSelector( - StaticATokenLM.initialize.selector, + StataTokenV2.initialize.selector, reserveData.aTokenAddress, - string(abi.encodePacked('Static ', IERC20Metadata(reserveData.aTokenAddress).name())), + string( + abi.encodePacked('Static ', IERC20Metadata(reserveData.aTokenAddress).name(), ' v2') + ), string(symbol) ), bytes32(uint256(uint160(underlyings[i]))) 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 d546fbb5..00000000 --- a/src/periphery/contracts/static-a-token/StaticATokenLM.sol +++ /dev/null @@ -1,622 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.17; - -import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol'; -import {ERC20Upgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol'; -import {ERC20PermitUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol'; -import {ERC20PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PausableUpgradeable.sol'; -import {ERC4626Upgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC4626Upgradeable.sol'; -import {IERC4626} from 'openzeppelin-contracts/contracts/interfaces/IERC4626.sol'; -import {IERC20} from 'openzeppelin-contracts/contracts/interfaces/IERC20.sol'; -import {IERC20Metadata} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; -import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; - -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 {DataTypes, ReserveConfiguration} from '../../../core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; -import {WadRayMath} from '../../../core/contracts/protocol/libraries/math/WadRayMath.sol'; -import {MathUtils} from '../../../core/contracts/protocol/libraries/math/MathUtils.sol'; -import {IACLManager} from '../../../core/contracts/interfaces/IACLManager.sol'; -import {IRewardsController} from '../rewards/interfaces/IRewardsController.sol'; -import {SafeCast} from 'solidity-utils/contracts/oz-common/SafeCast.sol'; -import {IRescuable, Rescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; - -import {IStaticATokenLM} from './interfaces/IStaticATokenLM.sol'; -import {IAToken} from './interfaces/IAToken.sol'; -import {IInitializableStaticATokenLM} from './interfaces/IInitializableStaticATokenLM.sol'; -import {StaticATokenErrors} from './StaticATokenErrors.sol'; -import {RayMathExplicitRounding, Rounding} from '../libraries/RayMathExplicitRounding.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 - ERC20Upgradeable, - ERC20PermitUpgradeable, - ERC20PausableUpgradeable, - ERC4626Upgradeable, - IStaticATokenLM, - Rescuable -{ - using SafeERC20 for IERC20; - using SafeCast for uint256; - using WadRayMath for uint256; - using RayMathExplicitRounding for uint256; - - IPool public immutable POOL; - IPoolAddressesProvider immutable POOL_ADDRESSES_PROVIDER; - IRewardsController public immutable INCENTIVES_CONTROLLER; - - IERC20 internal _aToken; - address internal _aTokenUnderlying; - uint8 internal _decimals; - address[] internal _rewardTokens; - mapping(address user => RewardIndexCache cache) internal _startIndex; - mapping(address user => mapping(address reward => UserRewardsData cache)) - internal _userRewardsData; - - constructor(IPool pool, IRewardsController rewardsController) { - _disableInitializers(); - POOL = pool; - INCENTIVES_CONTROLLER = rewardsController; - POOL_ADDRESSES_PROVIDER = pool.ADDRESSES_PROVIDER(); - } - - ///@inheritdoc IInitializableStaticATokenLM - function initialize( - address newAToken, - string calldata staticATokenName, - string calldata staticATokenSymbol - ) external initializer { - require(IAToken(newAToken).POOL() == address(POOL)); - __ERC20_init(staticATokenName, staticATokenSymbol); - __ERC20Permit_init(staticATokenName); - _aToken = IERC20(newAToken); - _decimals = IERC20Metadata(address(_aToken)).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); - } - - modifier onlyPauseGuardian() { - if (!canPause(msg.sender)) revert OnlyPauseGuardian(msg.sender); - _; - } - - ///@inheritdoc IStaticATokenLM - function canPause(address actor) public view returns (bool) { - return IACLManager(POOL_ADDRESSES_PROVIDER.getACLManager()).isEmergencyAdmin(actor); - } - - /// @inheritdoc IRescuable - function whoCanRescue() public view override returns (address) { - return POOL_ADDRESSES_PROVIDER.getACLAdmin(); - } - - ///@inheritdoc IERC4626 - function deposit(uint256 assets, address receiver) public override returns (uint256) { - (uint256 shares, ) = _deposit(msg.sender, receiver, 0, assets, 0, true); - return shares; - } - - ///@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 IERC4626 - function mint(uint256 shares, address receiver) public override returns (uint256) { - (, uint256 assets) = _deposit(msg.sender, receiver, shares, 0, 0, true); - - return assets; - } - - ///@inheritdoc IERC4626 - function withdraw( - uint256 assets, - address receiver, - address owner - ) public override returns (uint256) { - (uint256 shares, ) = _withdraw(owner, receiver, 0, assets, true); - - return shares; - } - - ///@inheritdoc IERC4626 - function redeem( - uint256 shares, - address receiver, - address owner - ) public override 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 returns (uint256, uint256) { - return _withdraw(owner, receiver, shares, 0, withdrawFromAave); - } - - ///@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 IERC20Metadata - function decimals() public view override(ERC20Upgradeable, ERC4626Upgradeable) returns (uint8) { - return _decimals; - } - - ///@inheritdoc IStaticATokenLM - function setPaused(bool paused) external onlyPauseGuardian { - if (paused) _pause(); - else _unpause(); - } - - ///@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 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 isRegisteredRewardToken(address reward) public view override returns (bool) { - return _startIndex[reward].isRegistered; - } - - ///@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 IStaticATokenLM - function rate() public view returns (uint256) { - return POOL.getReserveNormalizedIncome(_aTokenUnderlying); - } - - ///@inheritdoc IERC4626 - function asset() public view override returns (address) { - return address(_aTokenUnderlying); - } - - ///@inheritdoc IERC4626 - function totalAssets() public view override returns (uint256) { - return _aToken.balanceOf(address(this)); - } - - ///@inheritdoc IStaticATokenLM - function aToken() external view returns (IERC20) { - return _aToken; - } - - ///@inheritdoc IStaticATokenLM - function rewardTokens() external view returns (address[] memory) { - return _rewardTokens; - } - - ///@inheritdoc IERC4626 - function convertToShares(uint256 assets) public view override returns (uint256) { - return _convertToShares(assets, Rounding.DOWN); - } - - ///@inheritdoc IERC4626 - function convertToAssets(uint256 shares) public view override returns (uint256) { - return _convertToAssets(shares, Rounding.DOWN); - } - - ///@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, Rounding.DOWN); - } - - ///@inheritdoc IERC4626 - function maxWithdraw(address owner) public view override returns (uint256) { - uint256 shares = maxRedeem(owner); - return _convertToAssets(shares, Rounding.DOWN); - } - - ///@inheritdoc IERC4626 - function maxRedeem(address owner) public view override returns (uint256) { - address cachedATokenUnderlying = _aTokenUnderlying; - DataTypes.ReserveData memory reserveData = POOL.getReserveDataExtended(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( - reserveData.virtualUnderlyingBalance, - Rounding.DOWN - ); - 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(_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 IStaticATokenLM - function latestAnswer() external view returns (int256) { - return - int256( - (IAaveOracle(POOL_ADDRESSES_PROVIDER.getPriceOracle()).getAssetPrice(_aTokenUnderlying) * - POOL.getReserveNormalizedIncome(_aTokenUnderlying)) / 1e27 - ); - } - - 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) { - _spendAllowance(owner, msg.sender, 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 _update( - address from, - address to, - uint256 amount - ) internal override(ERC20Upgradeable, ERC20PausableUpgradeable) whenNotPaused { - 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 { - 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) { - RewardIndexCache memory rewardsIndexCache = _startIndex[reward]; - require(rewardsIndexCache.isRegistered == true, StaticATokenErrors.REWARD_NOT_INITIALIZED); - 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 whenNotPaused { - 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/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/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/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 index 7532e92c..1aee13d4 100644 --- a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenFactory.sol +++ b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenFactory.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.10; interface IStaticATokenFactory { + error NotListedUnderlying(address underlying); + /** * @notice Creates new staticATokens * @param underlyings the addresses of the underlyings to create. 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 026cad99..00000000 --- a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol +++ /dev/null @@ -1,188 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; - -import {IERC20} from 'openzeppelin-contracts/contracts/interfaces/IERC20.sol'; -import {IInitializableStaticATokenLM} from './IInitializableStaticATokenLM.sol'; - -interface IStaticATokenLM is IInitializableStaticATokenLM { - struct SignatureParams { - uint8 v; - bytes32 r; - bytes32 s; - } - - struct PermitParams { - 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; - } - - error OnlyPauseGuardian(address caller); - - 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 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. - * @param reward The reward to claim - * @return bool signaling if token is a registered reward. - */ - function isRegisteredRewardToken(address reward) external view returns (bool); - - /** - * @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; - - /** - * @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/tests/DeploymentsGasLimits.t.sol b/tests/DeploymentsGasLimits.t.sol index 28d5d8e4..c916c158 100644 --- a/tests/DeploymentsGasLimits.t.sol +++ b/tests/DeploymentsGasLimits.t.sol @@ -193,7 +193,7 @@ contract DeploymentsGasLimits is BatchTestProcedures { ); } - function testCheckInitCodeSizeBatchs() public view { + function testCheckInitCodeSizeBatches() 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..762c3229 --- /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_2701() external view { + 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); + 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 user) internal { + underlying.mint(user, amount); + lmUpgradeable.mint(user, amount); + vm.prank(user); + 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..e3977a2c --- /dev/null +++ b/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol @@ -0,0 +1,477 @@ +// 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 {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'; +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_2701() external view { + 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 { + vm.assume(receiver != address(0)); + 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); + } + + 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 { + vm.assume(receiver != address(0)); + 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 { + vm.assume(receiver != address(0)); + 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 { + vm.assume(receiver != address(0)); + 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 { + vm.assume(receiver != address(0)); + 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 { + vm.assume(receiver != address(0)); + 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 { + vm.assume(receiver != address(0)); + 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 { + vm.assume(receiver != address(0)); + 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 { + vm.assume(receiver != address(0)); + 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 { + vm.assume(receiver != address(0)); + 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 { + vm.assume(receiver != address(0)); + 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(); + uint256 assetsUsedForMinting = 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); + uint256 shares = _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 _setupTestEnv(uint256 amount) internal returns (TestEnv memory) { + TestEnv memory env; + env.amount = bound(amount, 1, type(uint96).max); + return env; + } + + function _fundUnderlying(uint256 assets, address user) internal { + deal(underlying, user, assets); + } + + function _fundAToken(uint256 assets, address user) internal { + _fundUnderlying(assets, user); + vm.startPrank(user); + IERC20(underlying).approve(address(contracts.poolProxy), assets); + contracts.poolProxy.deposit(underlying, assets, user, 0); + vm.stopPrank(); + } + + function _fund4626(uint256 assets, address user) internal returns (uint256) { + _fundAToken(assets, user); + vm.startPrank(user); + IERC20(aToken).approve(address(erc4626Upgradeable), assets); + uint256 shares = erc4626Upgradeable.depositATokens(assets, user); + vm.stopPrank(); + return shares; + } +} diff --git a/tests/periphery/static-a-token/Pausable.t.sol b/tests/periphery/static-a-token/Pausable.t.sol deleted file mode 100644 index 59a24dec..00000000 --- a/tests/periphery/static-a-token/Pausable.t.sol +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -import {UpgradableOwnableWithGuardian} from 'solidity-utils/contracts/access-control/UpgradableOwnableWithGuardian.sol'; -import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol'; -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 StataPausableTest is BaseTest { - using RayMathExplicitRounding for uint256; - - function test_setPaused_shouldRevertForInvalidCaller(address actor) external { - vm.assume(actor != poolAdmin && actor != proxyAdmin); - vm.expectRevert(abi.encodeWithSelector(IStaticATokenLM.OnlyPauseGuardian.selector, actor)); - _setPaused(actor, true); - } - - function test_setPaused_shouldSuceedForOwner() external { - assertEq(PausableUpgradeable(address(staticATokenLM)).paused(), false); - _setPaused(poolAdmin, true); - assertEq(PausableUpgradeable(address(staticATokenLM)).paused(), true); - } - - function test_deposit_shouldRevert() external { - vm.startPrank(user); - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - IERC20(UNDERLYING).approve(address(staticATokenLM), amountToDeposit); - vm.stopPrank(); - - _setPausedAsAclAdmin(true); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - vm.prank(user); - staticATokenLM.deposit(amountToDeposit, user, 0, true); - } - - function test_mint_shouldRevert() external { - vm.startPrank(user); - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - IERC20(UNDERLYING).approve(address(staticATokenLM), amountToDeposit); - vm.stopPrank(); - - uint256 sharesToMint = staticATokenLM.previewDeposit(amountToDeposit); - _setPausedAsAclAdmin(true); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - vm.prank(user); - staticATokenLM.mint(sharesToMint, user); - } - - function test_redeem_shouldRevert() external { - uint128 amountToDeposit = 5 ether; - vm.startPrank(user); - _fundUser(amountToDeposit, user); - _depositAToken(amountToDeposit, user); - vm.stopPrank(); - - assertEq(staticATokenLM.maxRedeem(user), staticATokenLM.balanceOf(user)); - - _setPausedAsAclAdmin(true); - uint256 maxRedeem = staticATokenLM.maxRedeem(user); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - vm.prank(user); - staticATokenLM.redeem(maxRedeem, user, user); - } - - function test_withdraw_shouldRevert() external { - uint128 amountToDeposit = 5 ether; - vm.startPrank(user); - _fundUser(amountToDeposit, user); - _depositAToken(amountToDeposit, user); - vm.stopPrank(); - - uint256 maxWithdraw = staticATokenLM.maxWithdraw(user); - _setPausedAsAclAdmin(true); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - vm.prank(user); - staticATokenLM.withdraw(maxWithdraw, user, user); - } - - function test_transfer_shouldRevert() external { - uint128 amountToDeposit = 10 ether; - vm.startPrank(user); - _fundUser(amountToDeposit, user); - _depositAToken(amountToDeposit, user); - vm.stopPrank(); - - _setPausedAsAclAdmin(true); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - vm.prank(user); - staticATokenLM.transfer(user1, amountToDeposit); - } - - function test_claimingRewards_shouldRevert() external { - _configureLM(); - uint128 amountToDeposit = 10 ether; - vm.startPrank(user); - _fundUser(amountToDeposit, user); - _depositAToken(amountToDeposit, user); - vm.stopPrank(); - - _setPausedAsAclAdmin(true); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - vm.prank(user); - staticATokenLM.claimRewardsToSelf(rewardTokens); - } - - function _setPausedAsAclAdmin(bool paused) internal { - _setPaused(poolAdmin, paused); - } - - function _setPaused(address actor, bool paused) internal { - vm.prank(actor); - staticATokenLM.setPaused(paused); - } -} diff --git a/tests/periphery/static-a-token/Rewards.t.sol b/tests/periphery/static-a-token/Rewards.t.sol deleted file mode 100644 index 36a13dd8..00000000 --- a/tests/periphery/static-a-token/Rewards.t.sol +++ /dev/null @@ -1,199 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -import {AToken} from '../../../src/core/contracts/protocol/tokenization/AToken.sol'; -import {IERC20} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; -import {BaseTest} from './TestBase.sol'; - -contract StataRewardsTest is BaseTest { - function setUp() public override { - super.setUp(); - - _configureLM(); - - vm.startPrank(user); - } - - function test_claimableRewards() external { - uint128 amountToDeposit = 5 ether; - _fundUser(amountToDeposit, user); - _depositAToken(amountToDeposit, user); - - vm.warp(block.timestamp + 200); - uint256 claimable = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); - assertEq(claimable, 200 * 0.00385 ether); - } - - // 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); - assertGe(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); - assertGe(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); - } -} 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 df569d9c..00000000 --- a/tests/periphery/static-a-token/StataOracle.t.sol +++ /dev/null @@ -1,88 +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'; -import {IPool} from '../../../src/core/contracts/interfaces/IPool.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); - } - - // ### tests for the dedicated oracle aggregator - 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 - ); - } - - // ### tests for the token internal oracle - function test_latestAnswer_priceShouldBeEqualOnDefaultIndex() public { - vm.mockCall( - address(POOL), - abi.encodeWithSelector(IPool.getReserveNormalizedIncome.selector), - abi.encode(1e27) - ); - uint256 stataPrice = uint256(staticATokenLM.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(POOL), - abi.encodeWithSelector(IPool.getReserveNormalizedIncome.selector), - abi.encode(liquidityIndex) - ); - uint256 stataPrice = uint256(staticATokenLM.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); - } -} diff --git a/tests/periphery/static-a-token/StataTokenV2Getters.sol b/tests/periphery/static-a-token/StataTokenV2Getters.sol new file mode 100644 index 00000000..425ada34 --- /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.STATIC_A_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..06d90b95 --- /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 { + assertEq(stataTokenV2.canPause(poolAdmin), true); + } + + function test_canPause_shouldReturnFalse(address actor) external { + 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 57009857..00000000 --- a/tests/periphery/static-a-token/StaticATokenLM.t.sol +++ /dev/null @@ -1,427 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; - -import {IRescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; -import {ERC20PermitUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol'; -import {Initializable} from 'openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol'; -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 {IStaticATokenLM} from '../../../src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol'; -import {SigUtils} from '../../utils/SigUtils.sol'; -import {BaseTest, TestnetERC20} from './TestBase.sol'; -import {IPool} from '../../../src/core/contracts/interfaces/IPool.sol'; - -contract StaticATokenLMTest is BaseTest { - using RayMathExplicitRounding for uint256; - - function setUp() public override { - super.setUp(); - - _configureLM(); - _openSupplyAndBorrowPositions(); - - vm.startPrank(user); - } - - function test_initializeShouldRevert() public { - address impl = factory.STATIC_A_TOKEN_IMPL(); - vm.expectRevert(Initializable.InvalidInitialization.selector); - IStaticATokenLM(impl).initialize(A_TOKEN, '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); - } - - /** - * 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, - 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, - PERMIT_TYPEHASH, - staticATokenLM.DOMAIN_SEPARATOR() - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); - - vm.expectRevert( - abi.encodeWithSelector( - ERC20PermitUpgradeable.ERC2612ExpiredSignature.selector, - permit.deadline - ) - ); - 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, - PERMIT_TYPEHASH, - staticATokenLM.DOMAIN_SEPARATOR() - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); - - vm.expectRevert( - abi.encodeWithSelector( - ERC20PermitUpgradeable.ERC2612InvalidSigner.selector, - user, - permit.owner - ) - ); - staticATokenLM.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); - } - - function test_rescuable_shouldRevertForInvalidCaller() external { - deal(tokenList.usdx, address(staticATokenLM), 1 ether); - vm.expectRevert('ONLY_RESCUE_GUARDIAN'); - IRescuable(address(staticATokenLM)).emergencyTokenTransfer( - tokenList.usdx, - address(this), - 1 ether - ); - } - - function test_rescuable_shouldSuceedForOwner() external { - deal(tokenList.usdx, address(staticATokenLM), 1 ether); - vm.startPrank(poolAdmin); - IRescuable(address(staticATokenLM)).emergencyTokenTransfer( - tokenList.usdx, - address(this), - 1 ether - ); - } - - 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/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 aa00e0ca..4a098285 100644 --- a/tests/periphery/static-a-token/TestBase.sol +++ b/tests/periphery/static-a-token/TestBase.sol @@ -1,17 +1,12 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.10; -import {IRewardsController} from '../../../src/periphery/contracts/rewards/interfaces/IRewardsController.sol'; -import {RewardsDataTypes} from '../../../src/periphery/contracts/rewards/libraries/RewardsDataTypes.sol'; -import {PullRewardsTransferStrategy} from '../../../src/periphery/contracts/rewards/transfer-strategies/PullRewardsTransferStrategy.sol'; -import {ITransferStrategyBase} from '../../../src/periphery/contracts/rewards/interfaces/ITransferStrategyBase.sol'; -import {IEACAggregatorProxy} from '../../../src/periphery/contracts/misc/interfaces/IEACAggregatorProxy.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} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; -import {IAToken} from '../../../src/core/contracts/interfaces/IAToken.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'; @@ -29,17 +24,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; 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; @@ -48,67 +42,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.createStaticATokens(contracts.poolProxy.getReservesList()); - staticATokenLM = StaticATokenLM(factory.getStaticAToken(UNDERLYING)); - } - - 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 _fundUser(uint128 amountToDeposit, address targetUser) internal { - deal(UNDERLYING, targetUser, amountToDeposit); + stataTokenV2 = StataTokenV2(factory.getStaticAToken(underlying)); } function _skipBlocks(uint128 blocks) internal { @@ -116,22 +67,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 user) internal { + deal(underlying, user, assets); } - function testAdmin() public { + function _fundAToken(uint256 assets, address user) internal { + _fundUnderlying(assets, user); + vm.startPrank(user); + IERC20(underlying).approve(address(contracts.poolProxy), assets); + contracts.poolProxy.deposit(underlying, assets, user, 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 user) internal returns (uint256) { + _fundAToken(assets, user); + vm.startPrank(user); + IERC20(aToken).approve(address(stataTokenV2), assets); + uint256 shares = stataTokenV2.depositATokens(assets, user); vm.stopPrank(); + return shares; } } diff --git a/tests/utils/SigUtils.sol b/tests/utils/SigUtils.sol index 311a256d..a41339fd 100644 --- a/tests/utils/SigUtils.sol +++ b/tests/utils/SigUtils.sol @@ -1,9 +1,12 @@ // 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 { + bytes32 internal constant PERMIT_TYPEHASH = + keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)'); + struct Permit { address owner; address spender; @@ -12,26 +15,6 @@ library SigUtils { uint256 deadline; } - struct MetaWithdrawParams { - address owner; - address spender; - uint256 staticAmount; - uint256 dynamicAmount; - bool toUnderlying; - uint256 nonce; - uint256 deadline; - } - - struct MetaDepositParams { - address depositor; - address receiver; - uint256 assets; - uint16 referralCode; - bool fromUnderlying; - uint256 nonce; - uint256 deadline; - } - // computes the hash of a permit function getStructHash(Permit memory _permit, bytes32 typehash) internal pure returns (bytes32) { return @@ -47,44 +30,6 @@ library SigUtils { ); } - function getWithdrawHash( - MetaWithdrawParams 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( - MetaDepositParams memory params, - bytes32 typehash - ) internal pure returns (bytes32) { - return - keccak256( - abi.encode( - typehash, - params.depositor, - params.receiver, - params.assets, - params.referralCode, - params.fromUnderlying, - params.nonce, - params.deadline - ) - ); - } - // 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, @@ -94,22 +39,4 @@ library SigUtils { return keccak256(abi.encodePacked('\x19\x01', domainSeparator, getStructHash(permit, typehash))); } - - function getTypedWithdrawHash( - MetaWithdrawParams memory params, - bytes32 typehash, - bytes32 domainSeparator - ) public pure returns (bytes32) { - return - keccak256(abi.encodePacked('\x19\x01', domainSeparator, getWithdrawHash(params, typehash))); - } - - function getTypedDepositHash( - MetaDepositParams memory params, - bytes32 typehash, - bytes32 domainSeparator - ) public pure returns (bytes32) { - return - keccak256(abi.encodePacked('\x19\x01', domainSeparator, getDepositHash(params, typehash))); - } } From 103f389dd8bd4dbc5b5c836476ff1d4e05dd75fb Mon Sep 17 00:00:00 2001 From: sakulstra Date: Fri, 16 Aug 2024 14:57:55 +0200 Subject: [PATCH 11/22] fix: cleanup imports refactor: cleanup some things (cherry picked from commit dd7166d0640e1a5bb9ad7afa03d9a21c6eb938ee) --- bun.lockb | Bin 91091 -> 91091 bytes package.json | 2 +- .../ERC20AaveLMUpgradable.t.sol | 2 +- .../ERC4626StataTokenUpgradeable.t.sol | 54 +++++++++--------- tests/periphery/static-a-token/TestBase.sol | 3 - tests/utils/DiffUtils.sol | 2 +- 6 files changed, 30 insertions(+), 33 deletions(-) diff --git a/bun.lockb b/bun.lockb index 471ed8580a4fd2c7f70ff7b2a474769a9baa45ce..dc28b8a63a9f4935f9a9bae4b3d26e5321092820 100755 GIT binary patch delta 143 zcmV;A0C4})#|6{J1&}Tv&#bB3ts(8f4Tf(bdu+Q9hF()-jk`I;WOCEzzEF*Eu}&Hg zlcW+Tlh6+cvzQQNOF-lap9yl83rk~8eVJyO9jPfQamJ>bDKLcpI{0A)Pm^@b%A(Gz x|G;$pqL%n_)?+QqLcayXDmPqqZrbR;DZsNmT)&+GG_(ErLn8q+w?Q2Nl141lL;L^$ delta 143 zcmV;A0C4})#|6{J1&}Tv6W$`<@azjz`v#X&jL<9fu1{GhU#Ycwpq>q#X)b#+u}&Hg zlOz`?lh6+cvzQQNOF;R4+^k0z)N|c#FTG_!baQ0M(ttgLyB=T_jQ)rX(}VhkIuXp^ xhHsUx+ETP`7px<1j#)4VBW>E7-#WeKVm-4xT)&+GGPC{pLn8q)w?Q2Nl18|?LBIe2 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/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol b/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol index 762c3229..d01a1172 100644 --- a/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol +++ b/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol @@ -85,7 +85,7 @@ contract ERC20AaveLMUpgradableTest is TestnetProcedures { contracts.emissionManager.setEmissionAdmin(rewardToken, emissionAdmin); } - function test_2701() external view { + function test_2701() external pure { assertEq( keccak256(abi.encode(uint256(keccak256('aave-dao.storage.ERC20AaveLM')) - 1)) & ~bytes32(uint256(0xff)), diff --git a/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol b/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol index e3977a2c..9d83e42e 100644 --- a/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol +++ b/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol @@ -7,10 +7,6 @@ import {IERC20Permit} from 'openzeppelin-contracts/contracts/token/ERC20/extensi 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 {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'; import {SigUtils} from '../../utils/SigUtils.sol'; @@ -46,7 +42,7 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { erc4626Upgradeable.mockInit(address(reserveData.aTokenAddress)); } - function test_2701() external view { + function test_2701() external pure { assertEq( keccak256(abi.encode(uint256(keccak256('aave-dao.storage.ERC4626StataToken')) - 1)) & ~bytes32(uint256(0xff)), @@ -66,7 +62,7 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { // ### DEPOSIT TESTS ### function test_depositATokens(uint128 assets, address receiver) public { - vm.assume(receiver != address(0)); + _validateReceiver(receiver); TestEnv memory env = _setupTestEnv(assets); _fundAToken(env.amount, user); @@ -106,7 +102,7 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { uint128 assets, address receiver ) external { - vm.assume(receiver != address(0)); + _validateReceiver(receiver); TestEnv memory env = _setupTestEnv(assets); _fundUnderlying(env.amount, user); IERC4626StataToken.SignatureParams memory sig; @@ -130,7 +126,7 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { uint128 assets, address receiver ) external { - vm.assume(receiver != address(0)); + _validateReceiver(receiver); TestEnv memory env = _setupTestEnv(assets); _fundAToken(env.amount, user); IERC4626StataToken.SignatureParams memory sig; @@ -151,7 +147,7 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { } function test_depositWithPermit_underlying(uint128 assets, address receiver) external { - vm.assume(receiver != address(0)); + _validateReceiver(receiver); TestEnv memory env = _setupTestEnv(assets); _fundUnderlying(env.amount, user); @@ -220,7 +216,7 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { // ### REDEEM TESTS ### function test_redeemATokens(uint256 assets, address receiver) public { - vm.assume(receiver != address(0)); + _validateReceiver(receiver); TestEnv memory env = _setupTestEnv(assets); uint256 shares = _fund4626(env.amount, user); @@ -266,7 +262,7 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { } function test_redeem(uint256 assets, address receiver) external { - vm.assume(receiver != address(0)); + _validateReceiver(receiver); TestEnv memory env = _setupTestEnv(assets); uint256 shares = _fund4626(env.amount, user); @@ -278,7 +274,7 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { // ### withdraw TESTS ### function test_withdraw(uint256 assets, address receiver) public { - vm.assume(receiver != address(0)); + _validateReceiver(receiver); TestEnv memory env = _setupTestEnv(assets); uint256 shares = _fund4626(env.amount, user); @@ -291,7 +287,7 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { } function test_withdraw_shouldRevert_moreThenAvailable(uint256 assets, address receiver) public { - vm.assume(receiver != address(0)); + _validateReceiver(receiver); TestEnv memory env = _setupTestEnv(assets); _fund4626(env.amount, user); @@ -309,7 +305,7 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { // ### mint TESTS ### function test_mint(uint256 assets, address receiver) public { - vm.assume(receiver != address(0)); + _validateReceiver(receiver); TestEnv memory env = _setupTestEnv(assets); _fundUnderlying(env.amount, user); @@ -331,7 +327,7 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { uint256 shares = erc4626Upgradeable.previewDeposit(env.amount); vm.expectRevert(); - uint256 assetsUsedForMinting = erc4626Upgradeable.mint(shares + 1, receiver); + erc4626Upgradeable.mint(shares + 1, receiver); } // ### maxDeposit TESTS ### @@ -400,7 +396,7 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { function test_maxRedeem_inSufficientAvailableLiquidity(uint256 amountToBorrow) public { uint128 assets = 1e8; amountToBorrow = bound(amountToBorrow, 1, assets); - uint256 shares = _fund4626(assets, user); + _fund4626(assets, user); // borrow out some assets address borrowUser = address(99); @@ -448,29 +444,33 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { uint256 amount; } - function _setupTestEnv(uint256 amount) internal returns (TestEnv memory) { + 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 user) internal { - deal(underlying, user, assets); + function _fundUnderlying(uint256 assets, address receiver) internal { + deal(underlying, receiver, assets); } - function _fundAToken(uint256 assets, address user) internal { - _fundUnderlying(assets, user); - vm.startPrank(user); + 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, user, 0); + contracts.poolProxy.deposit(underlying, assets, receiver, 0); vm.stopPrank(); } - function _fund4626(uint256 assets, address user) internal returns (uint256) { - _fundAToken(assets, user); - vm.startPrank(user); + 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, user); + uint256 shares = erc4626Upgradeable.depositATokens(assets, receiver); vm.stopPrank(); return shares; } diff --git a/tests/periphery/static-a-token/TestBase.sol b/tests/periphery/static-a-token/TestBase.sol index 4a098285..51bfce1a 100644 --- a/tests/periphery/static-a-token/TestBase.sol +++ b/tests/periphery/static-a-token/TestBase.sol @@ -11,9 +11,6 @@ import {TestnetProcedures, TestnetERC20} from '../../utils/TestnetProcedures.sol import {DataTypes} from '../../../src/core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; abstract contract BaseTest is TestnetProcedures { - bytes32 internal constant PERMIT_TYPEHASH = - keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)'); - address constant OWNER = address(1234); address public constant EMISSION_ADMIN = address(25); 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; From 2f57bd398e17986d9d941e7d9c107139169fb3ad Mon Sep 17 00:00:00 2001 From: sakulstra Date: Fri, 16 Aug 2024 15:27:46 +0200 Subject: [PATCH 12/22] test: prevent mint to address(0) --- .../ERC20AaveLMUpgradable.t.sol | 10 +++++----- .../ERC4626StataTokenUpgradeable.t.sol | 4 ++-- .../static-a-token/StataTokenV2Pausable.t.sol | 4 ++-- tests/periphery/static-a-token/TestBase.sol | 20 +++++++++---------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol b/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol index d01a1172..c436e937 100644 --- a/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol +++ b/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol @@ -242,7 +242,7 @@ contract ERC20AaveLMUpgradableTest is TestnetProcedures { address receiver, uint256 sendAmount ) external { - vm.assume(user != receiver); + vm.assume(user != receiver && receiver != address(0)); TestEnv memory env = _setupTestEnvironment( depositAmount, emissionEnd, @@ -395,10 +395,10 @@ contract ERC20AaveLMUpgradableTest is TestnetProcedures { * @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 user) internal { - underlying.mint(user, amount); - lmUpgradeable.mint(user, amount); - vm.prank(user); + 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 index 9d83e42e..158d64cc 100644 --- a/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol +++ b/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol @@ -181,7 +181,7 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { } function test_depositWithPermit_aToken(uint128 assets, address receiver) external { - vm.assume(receiver != address(0)); + _validateReceiver(receiver); TestEnv memory env = _setupTestEnv(assets); _fundAToken(env.amount, user); @@ -318,7 +318,7 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { } function test_mint_shouldRevert_mintMoreThenBalance(uint256 assets, address receiver) public { - vm.assume(receiver != address(0)); + _validateReceiver(receiver); TestEnv memory env = _setupTestEnv(assets); _fundUnderlying(env.amount, user); diff --git a/tests/periphery/static-a-token/StataTokenV2Pausable.t.sol b/tests/periphery/static-a-token/StataTokenV2Pausable.t.sol index 06d90b95..1eccc9df 100644 --- a/tests/periphery/static-a-token/StataTokenV2Pausable.t.sol +++ b/tests/periphery/static-a-token/StataTokenV2Pausable.t.sol @@ -7,11 +7,11 @@ import {IERC4626StataToken} from '../../../src/periphery/contracts/static-a-toke import {BaseTest} from './TestBase.sol'; contract StataTokenV2PausableTest is BaseTest { - function test_canPause() external { + function test_canPause() external view { assertEq(stataTokenV2.canPause(poolAdmin), true); } - function test_canPause_shouldReturnFalse(address actor) external { + function test_canPause_shouldReturnFalse(address actor) external view { vm.assume(actor != poolAdmin); assertEq(stataTokenV2.canPause(actor), false); } diff --git a/tests/periphery/static-a-token/TestBase.sol b/tests/periphery/static-a-token/TestBase.sol index 51bfce1a..cc14fc3e 100644 --- a/tests/periphery/static-a-token/TestBase.sol +++ b/tests/periphery/static-a-token/TestBase.sol @@ -72,23 +72,23 @@ abstract contract BaseTest is TestnetProcedures { vm.stopPrank(); } - function _fundUnderlying(uint256 assets, address user) internal { - deal(underlying, user, assets); + function _fundUnderlying(uint256 assets, address receiver) internal { + deal(underlying, receiver, assets); } - function _fundAToken(uint256 assets, address user) internal { - _fundUnderlying(assets, user); - vm.startPrank(user); + 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, user, 0); + contracts.poolProxy.deposit(underlying, assets, receiver, 0); vm.stopPrank(); } - function _fund4626(uint256 assets, address user) internal returns (uint256) { - _fundAToken(assets, user); - vm.startPrank(user); + 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, user); + uint256 shares = stataTokenV2.depositATokens(assets, receiver); vm.stopPrank(); return shares; } From 843f6d2f2f65d6ec00906edc3baa71f3f3645cf7 Mon Sep 17 00:00:00 2001 From: sakulstra Date: Fri, 16 Aug 2024 15:44:41 +0200 Subject: [PATCH 13/22] docs: add a bit more docs --- src/periphery/contracts/static-a-token/README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/periphery/contracts/static-a-token/README.md b/src/periphery/contracts/static-a-token/README.md index b6bd003e..d5cdfbbe 100644 --- a/src/periphery/contracts/static-a-token/README.md +++ b/src/periphery/contracts/static-a-token/README.md @@ -44,11 +44,15 @@ For this project, the security procedures applied/being finished are: The `StaticATokenLM`(v1) was based on solmate. To allow more flexibility the new `StataTokenV2`(v2) is based on [open-zeppelin-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 `StataTokenV2` is seperated in 3 different contracts, where `StataTokenV2` inherits `ERC4626StataToken` and `ERC20AaveLM`. - -- `ERC20AaveLM` is an abstract contract implementing the forwarding of liquidity mining from an underlying AaveERC20 - an ERC20 implementing `scaled` functions - to a wrapper contract. -- `ERC4626StataToken` is an abstract contract implementing the [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626) methods for an underlying aToken. In addition it adds a `latestAnswer`. -- `StataTokenV2` is the main contract stritching things together, while adding `Pausability`, `Rescuability`, `Permit` and the actual initialization. +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. ### MetaTransactions From 4aa0a58d26ae3500d9a1774dd5e94c2dc237e28d Mon Sep 17 00:00:00 2001 From: sakulstra Date: Fri, 16 Aug 2024 15:47:38 +0200 Subject: [PATCH 14/22] fix: lint on save --- src/periphery/contracts/static-a-token/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/periphery/contracts/static-a-token/README.md b/src/periphery/contracts/static-a-token/README.md index d5cdfbbe..47ed5e2e 100644 --- a/src/periphery/contracts/static-a-token/README.md +++ b/src/periphery/contracts/static-a-token/README.md @@ -47,11 +47,11 @@ To allow more flexibility the new `StataTokenV2`(v2) is based on [open-zeppelin- 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. + 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. + 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. ### MetaTransactions From 25e0a228e4b43100beace9073c0293d6392df0e8 Mon Sep 17 00:00:00 2001 From: sakulstra Date: Mon, 19 Aug 2024 08:58:13 +0200 Subject: [PATCH 15/22] docs: add comment about libraries --- src/periphery/contracts/static-a-token/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/periphery/contracts/static-a-token/README.md b/src/periphery/contracts/static-a-token/README.md index 47ed5e2e..599d3d83 100644 --- a/src/periphery/contracts/static-a-token/README.md +++ b/src/periphery/contracts/static-a-token/README.md @@ -42,7 +42,7 @@ For this project, the security procedures applied/being finished are: ### Inheritance The `StaticATokenLM`(v1) was based on solmate. -To allow more flexibility the new `StataTokenV2`(v2) is based on [open-zeppelin-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. +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. @@ -54,6 +54,11 @@ The implementation is seperated in two ERC20 extentions and one actual "merger" 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. +### 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. From aaa93ec6cca6f64b769676ae4265a40cea524d30 Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 19 Aug 2024 09:11:57 +0200 Subject: [PATCH 16/22] Update tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol Co-authored-by: Ernesto Boado --- tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol b/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol index c436e937..f831be23 100644 --- a/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol +++ b/tests/periphery/static-a-token/ERC20AaveLMUpgradable.t.sol @@ -85,7 +85,7 @@ contract ERC20AaveLMUpgradableTest is TestnetProcedures { contracts.emissionManager.setEmissionAdmin(rewardToken, emissionAdmin); } - function test_2701() external pure { + function test_7201() external pure { assertEq( keccak256(abi.encode(uint256(keccak256('aave-dao.storage.ERC20AaveLM')) - 1)) & ~bytes32(uint256(0xff)), From 58076f38dba079917c9b485831a872ba199c3289 Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 19 Aug 2024 09:12:04 +0200 Subject: [PATCH 17/22] Update tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol Co-authored-by: Ernesto Boado --- .../periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol b/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol index 158d64cc..e975def0 100644 --- a/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol +++ b/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol @@ -42,7 +42,7 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { erc4626Upgradeable.mockInit(address(reserveData.aTokenAddress)); } - function test_2701() external pure { + function test_7201() external pure { assertEq( keccak256(abi.encode(uint256(keccak256('aave-dao.storage.ERC4626StataToken')) - 1)) & ~bytes32(uint256(0xff)), From 3781bb6461fdf803a243708ca8905fe8dd86148a Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 19 Aug 2024 10:56:47 +0200 Subject: [PATCH 18/22] refactor: rename factory + add inheritance graph (#13) * docs: add inheritance image * refactor: rename factory * docs: add some more docs on readm --- .../procedures/AaveV3HelpersProcedureTwo.sol | 6 ++-- .../contracts/static-a-token/README.md | 15 ++++++---- ...TokenFactory.sol => StataTokenFactory.sol} | 24 ++++++++-------- .../contracts/static-a-token/StataTokenV2.sol | 5 ++++ .../contracts/static-a-token/inheritance.png | Bin 0 -> 484058 bytes .../interfaces/IStataTokenFactory.sol | 26 ++++++++++++++++++ .../interfaces/IStaticATokenFactory.sol | 26 ------------------ tests/periphery/static-a-token/TestBase.sol | 10 +++---- 8 files changed, 60 insertions(+), 52 deletions(-) rename src/periphery/contracts/static-a-token/{StaticATokenFactory.sol => StataTokenFactory.sol} (78%) create mode 100644 src/periphery/contracts/static-a-token/inheritance.png create mode 100644 src/periphery/contracts/static-a-token/interfaces/IStataTokenFactory.sol delete mode 100644 src/periphery/contracts/static-a-token/interfaces/IStaticATokenFactory.sol diff --git a/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol b/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol index 01b456e1..e8d40b76 100644 --- a/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol +++ b/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import '../../interfaces/IMarketReportTypes.sol'; import {TransparentProxyFactory, ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/TransparentProxyFactory.sol'; import {StataTokenV2} from 'aave-v3-periphery/contracts/static-a-token/StataTokenV2.sol'; -import {StaticATokenFactory} from 'aave-v3-periphery/contracts/static-a-token/StaticATokenFactory.sol'; +import {StataTokenFactory} from 'aave-v3-periphery/contracts/static-a-token/StataTokenFactory.sol'; import {IErrors} from '../../interfaces/IErrors.sol'; contract AaveV3HelpersProcedureTwo is IErrors { @@ -20,7 +20,7 @@ contract AaveV3HelpersProcedureTwo is IErrors { 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/static-a-token/README.md b/src/periphery/contracts/static-a-token/README.md index 599d3d83..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 [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 @@ -37,6 +36,8 @@ 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 @@ -54,6 +55,8 @@ The implementation is seperated in two ERC20 extentions and one actual "merger" 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. diff --git a/src/periphery/contracts/static-a-token/StaticATokenFactory.sol b/src/periphery/contracts/static-a-token/StataTokenFactory.sol similarity index 78% rename from src/periphery/contracts/static-a-token/StaticATokenFactory.sol rename to src/periphery/contracts/static-a-token/StataTokenFactory.sol index 91af7f72..f8343ba6 100644 --- a/src/periphery/contracts/static-a-token/StaticATokenFactory.sol +++ b/src/periphery/contracts/static-a-token/StataTokenFactory.sol @@ -6,16 +6,16 @@ import {IERC20Metadata} from 'solidity-utils/contracts/oz-common/interfaces/IERC 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 {IStaticATokenFactory} from './interfaces/IStaticATokenFactory.sol'; +import {IStataTokenFactory} from './interfaces/IStataTokenFactory.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. + * @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 StaticATokenFactory is Initializable, IStaticATokenFactory { +contract StataTokenFactory is Initializable, IStataTokenFactory { IPool public immutable POOL; address public immutable PROXY_ADMIN; ITransparentProxyFactory public immutable TRANSPARENT_PROXY_FACTORY; @@ -40,8 +40,8 @@ contract StaticATokenFactory is Initializable, IStaticATokenFactory { function initialize() external initializer {} - ///@inheritdoc IStaticATokenFactory - function createStaticATokens(address[] memory underlyings) external returns (address[] memory) { + ///@inheritdoc IStataTokenFactory + function createStataTokens(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]]; @@ -79,13 +79,13 @@ contract StaticATokenFactory is Initializable, IStaticATokenFactory { return staticATokens; } - ///@inheritdoc IStaticATokenFactory - function getStaticATokens() external view returns (address[] memory) { + ///@inheritdoc IStataTokenFactory + function getStataTokens() external view returns (address[] memory) { return _staticATokens; } - ///@inheritdoc IStaticATokenFactory - function getStaticAToken(address underlying) external view returns (address) { + ///@inheritdoc IStataTokenFactory + function getStataToken(address underlying) external view returns (address) { return _underlyingToStaticAToken[underlying]; } } diff --git a/src/periphery/contracts/static-a-token/StataTokenV2.sol b/src/periphery/contracts/static-a-token/StataTokenV2.sol index 0142fff3..e984b2b6 100644 --- a/src/periphery/contracts/static-a-token/StataTokenV2.sol +++ b/src/periphery/contracts/static-a-token/StataTokenV2.sol @@ -10,6 +10,11 @@ import {ERC4626Upgradeable, ERC4626StataTokenUpgradeable, IPool} from './ERC4626 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, 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 0000000000000000000000000000000000000000..ba79976a8fd89eeb156e8003e3811de9ed4c4670 GIT binary patch literal 484058 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

3v$(pVjH^)s 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 Y<>JvZMKc9@k&k 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 c7Gi21+fu2ddt0Y9FM$-gY?* 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∨pCBNb3@TlqTI_hLCd={|K=3S!O740 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 z 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 literal 0 HcmV?d00001 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/IStaticATokenFactory.sol b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenFactory.sol deleted file mode 100644 index 1aee13d4..00000000 --- a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenFactory.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; - -interface IStaticATokenFactory { - error NotListedUnderlying(address underlying); - - /** - * @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/tests/periphery/static-a-token/TestBase.sol b/tests/periphery/static-a-token/TestBase.sol index cc14fc3e..0365fb21 100644 --- a/tests/periphery/static-a-token/TestBase.sol +++ b/tests/periphery/static-a-token/TestBase.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.10; 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 {StaticATokenFactory} from '../../../src/periphery/contracts/static-a-token/StaticATokenFactory.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'; @@ -24,7 +24,7 @@ abstract contract BaseTest is TestnetProcedures { StataTokenV2 public stataTokenV2; address public proxyAdmin; ITransparentProxyFactory public proxyFactory; - StaticATokenFactory public factory; + StataTokenFactory public factory; address[] rewardTokens; @@ -53,10 +53,10 @@ abstract contract BaseTest is TestnetProcedures { proxyFactory = ITransparentProxyFactory(report.transparentProxyFactory); proxyAdmin = report.proxyAdmin; - factory = StaticATokenFactory(report.staticATokenFactoryProxy); - factory.createStaticATokens(contracts.poolProxy.getReservesList()); + factory = StataTokenFactory(report.staticATokenFactoryProxy); + factory.createStataTokens(contracts.poolProxy.getReservesList()); - stataTokenV2 = StataTokenV2(factory.getStaticAToken(underlying)); + stataTokenV2 = StataTokenV2(factory.getStataToken(underlying)); } function _skipBlocks(uint128 blocks) internal { From 47c58b73b323a9df6c2df7e8004e2645a1a4a332 Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 2 Sep 2024 12:23:17 +0200 Subject: [PATCH 19/22] fix: address certora feedback (#14) * fix: address certora feedback * fix: leftover * fix: one more leftover --- .../ERC4626StataTokenUpgradeable.sol | 12 ++++-- .../static-a-token/StataTokenFactory.sol | 38 +++++++++---------- .../static-a-token/StataTokenV2Getters.sol | 2 +- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol b/src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol index 097e22c5..4f1b2d56 100644 --- a/src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol +++ b/src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol @@ -74,7 +74,7 @@ abstract contract ERC4626StataTokenUpgradeable is ERC4626Upgradeable, IERC4626St } ///@inheritdoc IERC4626StataToken - function depositATokens(uint256 assets, address receiver) public returns (uint256) { + function depositATokens(uint256 assets, address receiver) external returns (uint256) { uint256 shares = previewDeposit(assets); _deposit(_msgSender(), receiver, assets, shares, false); @@ -88,7 +88,7 @@ abstract contract ERC4626StataTokenUpgradeable is ERC4626Upgradeable, IERC4626St uint256 deadline, SignatureParams memory sig, bool depositToAave - ) public returns (uint256) { + ) external returns (uint256) { IERC20Permit assetToDeposit = IERC20Permit( depositToAave ? asset() : address(_getERC4626StataTokenStorage()._aToken) ); @@ -103,7 +103,11 @@ abstract contract ERC4626StataTokenUpgradeable is ERC4626Upgradeable, IERC4626St } ///@inheritdoc IERC4626StataToken - function redeemATokens(uint256 shares, address receiver, address owner) public returns (uint256) { + function redeemATokens( + uint256 shares, + address receiver, + address owner + ) external returns (uint256) { uint256 assets = previewRedeem(shares); _withdraw(_msgSender(), receiver, owner, assets, shares, false); @@ -111,7 +115,7 @@ abstract contract ERC4626StataTokenUpgradeable is ERC4626Upgradeable, IERC4626St } ///@inheritdoc IERC4626StataToken - function aToken() public view returns (IERC20) { + function aToken() external view returns (IERC20) { ERC4626StataTokenStorage storage $ = _getERC4626StataTokenStorage(); return $._aToken; } diff --git a/src/periphery/contracts/static-a-token/StataTokenFactory.sol b/src/periphery/contracts/static-a-token/StataTokenFactory.sol index f8343ba6..e7ba0929 100644 --- a/src/periphery/contracts/static-a-token/StataTokenFactory.sol +++ b/src/periphery/contracts/static-a-token/StataTokenFactory.sol @@ -19,33 +19,33 @@ contract StataTokenFactory is Initializable, IStataTokenFactory { IPool public immutable POOL; address public immutable PROXY_ADMIN; ITransparentProxyFactory public immutable TRANSPARENT_PROXY_FACTORY; - address public immutable STATIC_A_TOKEN_IMPL; + address public immutable STATA_TOKEN_IMPL; - mapping(address => address) internal _underlyingToStaticAToken; - address[] internal _staticATokens; + mapping(address => address) internal _underlyingToStataToken; + address[] internal _stataTokens; - event StaticTokenCreated(address indexed staticAToken, address indexed underlying); + event StataTokenCreated(address indexed stataToken, address indexed underlying); constructor( IPool pool, address proxyAdmin, ITransparentProxyFactory transparentProxyFactory, - address staticATokenImpl + address stataTokenImpl ) { POOL = pool; PROXY_ADMIN = proxyAdmin; TRANSPARENT_PROXY_FACTORY = transparentProxyFactory; - STATIC_A_TOKEN_IMPL = staticATokenImpl; + STATA_TOKEN_IMPL = stataTokenImpl; } function initialize() external initializer {} ///@inheritdoc IStataTokenFactory function createStataTokens(address[] memory underlyings) external returns (address[] memory) { - address[] memory staticATokens = new address[](underlyings.length); + address[] memory stataTokens = new address[](underlyings.length); for (uint256 i = 0; i < underlyings.length; i++) { - address cachedStaticAToken = _underlyingToStaticAToken[underlyings[i]]; - if (cachedStaticAToken == address(0)) { + 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); @@ -54,8 +54,8 @@ contract StataTokenFactory is Initializable, IStataTokenFactory { IERC20Metadata(reserveData.aTokenAddress).symbol(), 'v2' ); - address staticAToken = TRANSPARENT_PROXY_FACTORY.createDeterministic( - STATIC_A_TOKEN_IMPL, + address stataToken = TRANSPARENT_PROXY_FACTORY.createDeterministic( + STATA_TOKEN_IMPL, PROXY_ADMIN, abi.encodeWithSelector( StataTokenV2.initialize.selector, @@ -68,24 +68,24 @@ contract StataTokenFactory is Initializable, IStataTokenFactory { bytes32(uint256(uint160(underlyings[i]))) ); - _underlyingToStaticAToken[underlyings[i]] = staticAToken; - staticATokens[i] = staticAToken; - _staticATokens.push(staticAToken); - emit StaticTokenCreated(staticAToken, underlyings[i]); + _underlyingToStataToken[underlyings[i]] = stataToken; + stataTokens[i] = stataToken; + _stataTokens.push(stataToken); + emit StataTokenCreated(stataToken, underlyings[i]); } else { - staticATokens[i] = cachedStaticAToken; + stataTokens[i] = cachedStataToken; } } - return staticATokens; + return stataTokens; } ///@inheritdoc IStataTokenFactory function getStataTokens() external view returns (address[] memory) { - return _staticATokens; + return _stataTokens; } ///@inheritdoc IStataTokenFactory function getStataToken(address underlying) external view returns (address) { - return _underlyingToStaticAToken[underlying]; + return _underlyingToStataToken[underlying]; } } diff --git a/tests/periphery/static-a-token/StataTokenV2Getters.sol b/tests/periphery/static-a-token/StataTokenV2Getters.sol index 425ada34..c9a8aa1c 100644 --- a/tests/periphery/static-a-token/StataTokenV2Getters.sol +++ b/tests/periphery/static-a-token/StataTokenV2Getters.sol @@ -9,7 +9,7 @@ import {BaseTest} from './TestBase.sol'; contract StataTokenV2GettersTest is BaseTest { function test_initializeShouldRevert() public { - address impl = factory.STATIC_A_TOKEN_IMPL(); + address impl = factory.STATA_TOKEN_IMPL(); vm.expectRevert(Initializable.InvalidInitialization.selector); StataTokenV2(impl).initialize(aToken, 'hey', 'ho'); } From 111ba64cadbcb41d447372df78d77e0cc62c6d8d Mon Sep 17 00:00:00 2001 From: sakulstra Date: Thu, 5 Sep 2024 16:42:30 +0200 Subject: [PATCH 20/22] fix: properly resolve conflict --- tests/DeploymentsGasLimits.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/DeploymentsGasLimits.t.sol b/tests/DeploymentsGasLimits.t.sol index 95126653..54d32c73 100644 --- a/tests/DeploymentsGasLimits.t.sol +++ b/tests/DeploymentsGasLimits.t.sol @@ -208,7 +208,7 @@ contract DeploymentsGasLimits is BatchTestProcedures { ); } - function testCheckInitCodeSizeBatchs() pure view { + function testCheckInitCodeSizeBatchs() public pure { uint16 maxInitCodeSize = 49152; console.log('AaveV3SetupBatch', type(AaveV3SetupBatch).creationCode.length); From 53edf5fcef29023a95ddef2bbb69de7c32b22955 Mon Sep 17 00:00:00 2001 From: sakulstra Date: Thu, 5 Sep 2024 16:57:04 +0200 Subject: [PATCH 21/22] fix: calculate totalAssets --- .../static-a-token/ERC4626StataTokenUpgradeable.sol | 5 +++++ .../static-a-token/ERC4626StataTokenUpgradeable.t.sol | 1 + 2 files changed, 6 insertions(+) diff --git a/src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol b/src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol index 4f1b2d56..d6be297a 100644 --- a/src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol +++ b/src/periphery/contracts/static-a-token/ERC4626StataTokenUpgradeable.sol @@ -132,6 +132,11 @@ abstract contract ERC4626StataTokenUpgradeable is ERC4626Upgradeable, IERC4626St 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()); diff --git a/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol b/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol index e975def0..714d1407 100644 --- a/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol +++ b/tests/periphery/static-a-token/ERC4626StataTokenUpgradeable.t.sol @@ -74,6 +74,7 @@ contract ERC4626StataTokenUpgradeableTest is TestnetProcedures { 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 { From 42aeaa79cee102a6d0c079af1ca413ee2385d263 Mon Sep 17 00:00:00 2001 From: Michael M Date: Sun, 8 Sep 2024 20:26:55 +0300 Subject: [PATCH 22/22] Certora adaptation to stata --- .github/workflows/certora-stata.yml | 79 ++ certora/conf/AToken.conf | 1 + certora/conf/NEW-pool-no-summarizations.conf | 1 + certora/conf/NEW-pool-simple-properties.conf | 1 + certora/conf/ReserveConfiguration.conf | 1 + certora/conf/StableDebtToken.conf | 1 + certora/conf/UserConfiguration.conf | 1 + certora/conf/VariableDebtToken.conf | 1 + certora/scripts/run-all.sh | 2 +- certora/specs/NEW-pool-base.spec | 12 +- certora/specs/NEW-pool-no-summarizations.spec | 2 +- certora/stata/Makefile | 33 + certora/stata/applyHarness.patch | 48 ++ certora/stata/conf/verifyAToken.conf | 40 + certora/stata/conf/verifyDoubleClaim.conf | 40 + certora/stata/conf/verifyERC4626.conf | 40 + .../verifyERC4626DepositSummarization.conf | 39 + certora/stata/conf/verifyERC4626Extended.conf | 40 + ...verifyERC4626MintDepositSummarization.conf | 41 + certora/stata/conf/verifyStataToken.conf | 40 + certora/stata/harness/StataTokenV2Harness.sol | 85 ++ .../harness/pool/SymbolicLendingPool.sol | 108 +++ .../rewards/RewardsControllerHarness.sol | 48 ++ .../rewards/TransferStrategyHarness.sol | 22 + .../stata/harness/tokens/DummyERC20Impl.sol | 53 ++ .../tokens/DummyERC20_aTokenUnderlying.sol | 5 + .../harness/tokens/DummyERC20_rewardToken.sol | 5 + certora/stata/munged/.gitignore | 2 + certora/stata/scripts/run-all.sh | 95 +++ .../stata/specs/StataToken/StataToken.spec | 396 +++++++++ .../specs/StataToken/aTokenProperties.spec | 246 ++++++ .../stata/specs/StataToken/double_claim.spec | 65 ++ certora/stata/specs/erc4626/erc4626.spec | 754 ++++++++++++++++++ .../erc4626/erc4626DepositSummarization.spec | 163 ++++ .../stata/specs/erc4626/erc4626Extended.spec | 254 ++++++ .../erc4626MintDepositSummarization.spec | 220 +++++ certora/stata/specs/methods/CVLMath.spec | 236 ++++++ certora/stata/specs/methods/erc20.spec | 12 + certora/stata/specs/methods/methods_base.spec | 194 +++++ .../specs/methods/methods_multi_reward.spec | 75 ++ 40 files changed, 3493 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/certora-stata.yml create mode 100644 certora/stata/Makefile create mode 100644 certora/stata/applyHarness.patch create mode 100644 certora/stata/conf/verifyAToken.conf create mode 100644 certora/stata/conf/verifyDoubleClaim.conf create mode 100644 certora/stata/conf/verifyERC4626.conf create mode 100644 certora/stata/conf/verifyERC4626DepositSummarization.conf create mode 100644 certora/stata/conf/verifyERC4626Extended.conf create mode 100644 certora/stata/conf/verifyERC4626MintDepositSummarization.conf create mode 100644 certora/stata/conf/verifyStataToken.conf create mode 100644 certora/stata/harness/StataTokenV2Harness.sol create mode 100644 certora/stata/harness/pool/SymbolicLendingPool.sol create mode 100644 certora/stata/harness/rewards/RewardsControllerHarness.sol create mode 100644 certora/stata/harness/rewards/TransferStrategyHarness.sol create mode 100644 certora/stata/harness/tokens/DummyERC20Impl.sol create mode 100644 certora/stata/harness/tokens/DummyERC20_aTokenUnderlying.sol create mode 100644 certora/stata/harness/tokens/DummyERC20_rewardToken.sol create mode 100644 certora/stata/munged/.gitignore create mode 100644 certora/stata/scripts/run-all.sh create mode 100644 certora/stata/specs/StataToken/StataToken.spec create mode 100644 certora/stata/specs/StataToken/aTokenProperties.spec create mode 100644 certora/stata/specs/StataToken/double_claim.spec create mode 100644 certora/stata/specs/erc4626/erc4626.spec create mode 100644 certora/stata/specs/erc4626/erc4626DepositSummarization.spec create mode 100644 certora/stata/specs/erc4626/erc4626Extended.spec create mode 100644 certora/stata/specs/erc4626/erc4626MintDepositSummarization.spec create mode 100644 certora/stata/specs/methods/CVLMath.spec create mode 100644 certora/stata/specs/methods/erc20.spec create mode 100644 certora/stata/specs/methods/methods_base.spec create mode 100644 certora/stata/specs/methods/methods_multi_reward.spec diff --git a/.github/workflows/certora-stata.yml b/.github/workflows/certora-stata.yml new file mode 100644 index 00000000..42dd7adb --- /dev/null +++ b/.github/workflows/certora-stata.yml @@ -0,0 +1,79 @@ +name: certora-stata + +on: + push: + branches: + - main certora + pull_request: + branches: + - main certora + + workflow_dispatch: + +jobs: + verify: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + submodules: recursive + + - name: Install python + uses: actions/setup-python@v2 + with: { python-version: 3.9 } + + - name: Install java + uses: actions/setup-java@v1 + with: { java-version: "11", java-package: jre } + + - name: Install certora cli + run: pip install certora-cli==7.10.2 + + - name: Install solc + run: | + wget https://github.com/ethereum/solidity/releases/download/v0.8.20/solc-static-linux + chmod +x solc-static-linux + sudo mv solc-static-linux /usr/local/bin/solc8.20 + + - name: Verify rule ${{ matrix.rule }} + run: | + cd certora + touch applyHarness.patch + make munged + cd .. + certoraRun certora/conf/${{ matrix.rule }} + env: + CERTORAKEY: ${{ secrets.CERTORAKEY }} + + strategy: + fail-fast: false + max-parallel: 16 + matrix: + rule: + - 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 + 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 + - 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/certora/conf/AToken.conf b/certora/conf/AToken.conf index 299a8f4e..fcf73ac5 100644 --- a/certora/conf/AToken.conf +++ b/certora/conf/AToken.conf @@ -11,5 +11,6 @@ "process": "emv", "solc": "solc8.19", "verify": "ATokenHarness:certora/specs/AToken.spec", +// "build_cache": true, "msg": "aToken spec" } diff --git a/certora/conf/NEW-pool-no-summarizations.conf b/certora/conf/NEW-pool-no-summarizations.conf index 11f62350..080f07f5 100644 --- a/certora/conf/NEW-pool-no-summarizations.conf +++ b/certora/conf/NEW-pool-no-summarizations.conf @@ -36,6 +36,7 @@ "depositUpdatesUserATokenSuperBalance", "depositCannotChangeOthersATokenSuperBalance" ], +// "build_cache": true, "parametric_contracts": ["PoolHarness"], "msg": "pool-no-summarizations::partial rules", } diff --git a/certora/conf/NEW-pool-simple-properties.conf b/certora/conf/NEW-pool-simple-properties.conf index 96a78c78..6c81b8ad 100644 --- a/certora/conf/NEW-pool-simple-properties.conf +++ b/certora/conf/NEW-pool-simple-properties.conf @@ -38,6 +38,7 @@ "cannotBorrowOnReserveDisabledForBorrowing", "cannotBorrowOnFrozenReserve" ], +// "build_cache": true, "parametric_contracts": ["PoolHarness"], "msg": "pool-simple-properties::ALL", } diff --git a/certora/conf/ReserveConfiguration.conf b/certora/conf/ReserveConfiguration.conf index 2e4e50b6..ed3fc42f 100644 --- a/certora/conf/ReserveConfiguration.conf +++ b/certora/conf/ReserveConfiguration.conf @@ -10,5 +10,6 @@ ], "rule_sanity": "basic", // from time to time, use "advanced" instead of "basic" "solc": "solc8.19", +// "build_cache": true, "verify": "ReserveConfigurationHarness:certora/specs/ReserveConfiguration.spec" } diff --git a/certora/conf/StableDebtToken.conf b/certora/conf/StableDebtToken.conf index 1480d348..0d94747f 100644 --- a/certora/conf/StableDebtToken.conf +++ b/certora/conf/StableDebtToken.conf @@ -14,5 +14,6 @@ "optimistic_loop": true, "process": "emv", "solc": "solc8.19", +// "build_cache": true, "verify": "StableDebtTokenHarness:certora/specs/StableDebtToken.spec" } diff --git a/certora/conf/UserConfiguration.conf b/certora/conf/UserConfiguration.conf index 65b23d05..2d85039b 100644 --- a/certora/conf/UserConfiguration.conf +++ b/certora/conf/UserConfiguration.conf @@ -11,5 +11,6 @@ "-useBitVectorTheory" ], "solc": "solc8.19", +// "build_cache": true, "verify": "UserConfigurationHarness:certora/specs/UserConfiguration.spec" } diff --git a/certora/conf/VariableDebtToken.conf b/certora/conf/VariableDebtToken.conf index 52b9f172..90050b61 100644 --- a/certora/conf/VariableDebtToken.conf +++ b/certora/conf/VariableDebtToken.conf @@ -7,5 +7,6 @@ "optimistic_loop": true, "process": "emv", "solc": "solc8.19", +// "build_cache": true, "verify": "VariableDebtTokenHarness:certora/specs/VariableDebtToken.spec" } diff --git a/certora/scripts/run-all.sh b/certora/scripts/run-all.sh index 9e6c5d05..ab9685d4 100644 --- a/certora/scripts/run-all.sh +++ b/certora/scripts/run-all.sh @@ -1,4 +1,4 @@ -CMN="" +#CMN="--compilation_steps_only" diff --git a/certora/specs/NEW-pool-base.spec b/certora/specs/NEW-pool-base.spec index ac7cfdf0..9a9e86ba 100644 --- a/certora/specs/NEW-pool-base.spec +++ b/certora/specs/NEW-pool-base.spec @@ -30,19 +30,19 @@ methods { function _.transfer(address, uint256) external => DISPATCHER(true); function _.transferFrom(address, address, uint256) external => DISPATCHER(true); function _.approve(address, uint256) external => DISPATCHER(true); - function _.mint(address, uint256) external => DISPATCHER(true); - function _.burn(uint256) external => DISPATCHER(true); + //function _.mint(address, uint256) external => DISPATCHER(true); + //function _.burn(uint256) external => DISPATCHER(true); function _.balanceOf(address) external => DISPATCHER(true); function _.totalSupply() external => DISPATCHER(true); // ATOKEN - function _.mint(address user, uint256 amount, uint256 index) external => DISPATCHER(true); - function _.burn(address user, address receiverOfUnderlying, uint256 amount, uint256 index) external => DISPATCHER(true); + //function _.mint(address user, uint256 amount, uint256 index) external => DISPATCHER(true); + //function _.burn(address user, address receiverOfUnderlying, uint256 amount, uint256 index) external => DISPATCHER(true); function _.mintToTreasury(uint256 amount, uint256 index) external => DISPATCHER(true); function _.transferOnLiquidation(address from, address to, uint256 value) external => DISPATCHER(true); function _.transferUnderlyingTo(address user, uint256 amount) external => DISPATCHER(true); - function _.handleRepayment(address user, uint256 amount) external => DISPATCHER(true); + // function _.handleRepayment(address user, uint256 amount) external => DISPATCHER(true); function _.permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external => DISPATCHER(true); function _.ATokenBalanceOf(address user) external => DISPATCHER(true); @@ -62,7 +62,7 @@ methods { function _.getReserveNormalizedIncome(address asset) external => DISPATCHER(true); function _.getReserveNormalizedVariableDebt(address asset) external => DISPATCHER(true); function _.getACLManager() external => DISPATCHER(true); - function _.isBridge(address) external => DISPATCHER(true); + //function _.isBridge(address) external => DISPATCHER(true); // StableDebt function _.mint(address user, address onBehalfOf, uint256 amount, uint256 rate) external => DISPATCHER(true); diff --git a/certora/specs/NEW-pool-no-summarizations.spec b/certora/specs/NEW-pool-no-summarizations.spec index d4dcbe68..aad2c00c 100644 --- a/certora/specs/NEW-pool-no-summarizations.spec +++ b/certora/specs/NEW-pool-no-summarizations.spec @@ -9,7 +9,7 @@ methods { function _.symbol() external => DISPATCHER(true); function _.isFlashBorrower(address a) external => DISPATCHER(true); - function _.executeOperation(address[] a, uint256[]b, uint256[]c, address d, bytes e) external => DISPATCHER(true); + // function _.executeOperation(address[] a, uint256[]b, uint256[]c, address d, bytes e) external => DISPATCHER(true); function _.getAverageStableRate() external => DISPATCHER(true); function _.isPoolAdmin(address a) external => DISPATCHER(true); diff --git a/certora/stata/Makefile b/certora/stata/Makefile new file mode 100644 index 00000000..215e7440 --- /dev/null +++ b/certora/stata/Makefile @@ -0,0 +1,33 @@ +default: help + +PATCH = applyHarness.patch +CONTRACTS_DIR = ../../src +LIBS_DIR = ../../lib +MUNGED_SRC = munged/src +MUNGED_LIB = munged/lib +MUNGED_DIR = munged + +help: + @echo "usage:" + @echo " make clean: remove all generated files (those ignored by git)" + @echo " make $(MUNGED_DIR): create $(MUNGED_DIR) directory by applying the patch file to $(CONTRACTS_DIR)" + @echo " make record: record a new patch file capturing the differences between $(CONTRACTS_DIR) and $(MUNGED_DIR)" + +munged: $(wildcard $(CONTRACTS_DIR)/*.sol) $(PATCH) + rm -rf $@ + mkdir $@ + cp -r ../../lib $@ + cp -r ../../src $@ + patch -p0 -d $@ < $(PATCH) + +record: + mkdir 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 + +clean: + git clean -fdX + touch $(PATCH) + diff --git a/certora/stata/applyHarness.patch b/certora/stata/applyHarness.patch new file mode 100644 index 00000000..98c12412 --- /dev/null +++ b/certora/stata/applyHarness.patch @@ -0,0 +1,48 @@ +diff -ruN .gitignore .gitignore +--- .gitignore 1970-01-01 02:00:00 ++++ .gitignore 2024-09-04 13:59:46 +@@ -0,0 +1,2 @@ ++* ++!.gitignore +\ 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 new file mode 100644 index 00000000..c2bc01e1 --- /dev/null +++ b/certora/stata/conf/verifyAToken.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", + ], + "loop_iter": "1", + "msg": "aToken properties", + "optimistic_hashing": true, + "optimistic_loop": true, + "solc": "solc8.20", + "smt_timeout": "1400", + "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 new file mode 100644 index 00000000..55caf5e7 --- /dev/null +++ b/certora/stata/conf/verifyDoubleClaim.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/staticATokenLM/double_claim.spec", + "solc": "solc8.20", + "msg": "Multi rewards - double claim properties", + "optimistic_loop": true, + "smt_timeout": "2000", + "loop_iter": "2", + "optimistic_hashing": true, + "build_cache": true, +} \ No newline at end of file diff --git a/certora/stata/conf/verifyERC4626.conf b/certora/stata/conf/verifyERC4626.conf new file mode 100644 index 00000000..06900f28 --- /dev/null +++ b/certora/stata/conf/verifyERC4626.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/erc4626/erc4626.spec", + "solc": "solc8.20", + "msg": "ERC4626 properties", + "optimistic_loop": true, + "smt_timeout": "3600", + "loop_iter": "1", + "optimistic_hashing": true, + "build_cache": true, +} \ No newline at end of file diff --git a/certora/stata/conf/verifyERC4626DepositSummarization.conf b/certora/stata/conf/verifyERC4626DepositSummarization.conf new file mode 100644 index 00000000..d2ce588f --- /dev/null +++ b/certora/stata/conf/verifyERC4626DepositSummarization.conf @@ -0,0 +1,39 @@ +{ + "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/erc4626/erc4626DepositSummarization.spec", + "solc": "solc8.20", + "msg": "ERC4626 Deposit summarized", + "optimistic_loop": true, + "loop_iter": "1", + "optimistic_hashing": true, + "build_cache": true, +} \ No newline at end of file diff --git a/certora/stata/conf/verifyERC4626Extended.conf b/certora/stata/conf/verifyERC4626Extended.conf new file mode 100644 index 00000000..6c35861c --- /dev/null +++ b/certora/stata/conf/verifyERC4626Extended.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/erc4626/erc4626Extended.spec", + "solc": "solc8.20", + "msg": "ERC4626 Extended properties", + "optimistic_loop": true, + "smt_timeout": "1000", + "loop_iter": "1", + "optimistic_hashing": true, + "build_cache": true, +} \ No newline at end of file diff --git a/certora/stata/conf/verifyERC4626MintDepositSummarization.conf b/certora/stata/conf/verifyERC4626MintDepositSummarization.conf new file mode 100644 index 00000000..d0c76fba --- /dev/null +++ b/certora/stata/conf/verifyERC4626MintDepositSummarization.conf @@ -0,0 +1,41 @@ +{ + "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/erc4626/erc4626MintDepositSummarization.spec", + "solc": "solc8.20", + "msg": "ERC4626 Summarized no transferFrom properties", + "optimistic_loop": true, + "smt_timeout": "5000", + "loop_iter": "1", + "optimistic_hashing": true, + "build_cache": true, +} \ No newline at end of file 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/StataTokenV2Harness.sol b/certora/stata/harness/StataTokenV2Harness.sol new file mode 100644 index 00000000..ce615d08 --- /dev/null +++ b/certora/stata/harness/StataTokenV2Harness.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.10; + +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 StataTokenV2Harness is StataTokenV2 { + address internal _reward_A; + + constructor( + 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]; + } + + // returns the length of the reward tokens list maintained by the static aToken + function getRewardTokensLength() external view returns (uint256) { + 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; + // } + + // 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 + function claimSingleRewardOnBehalf( + address onBehalfOf, + address receiver, + address reward + ) external + { + require (reward == _reward_A); + address[] memory rewards = new address[](1); + rewards[0] = _reward_A; + + // @MM - think of the best way to get rid of this require + require( + msg.sender == onBehalfOf || + msg.sender == INCENTIVES_CONTROLLER.getClaimer(onBehalfOf) + ); + _claimRewardsOnBehalf(onBehalfOf, receiver, rewards); + } + + // claims rewards for a user on the static aToken. + // the method builds the rewards array with 2 identical rewards and calls the internal claim function with it + function claimDoubleRewardOnBehalfSame( + address onBehalfOf, + address receiver, + address reward + ) external + { + require (reward == _reward_A); + address[] memory rewards = new address[](2); + rewards[0] = _reward_A; + rewards[1] = _reward_A; + + require( + msg.sender == onBehalfOf || + 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); + } + +} diff --git a/certora/stata/harness/pool/SymbolicLendingPool.sol b/certora/stata/harness/pool/SymbolicLendingPool.sol new file mode 100644 index 00000000..ec3b7ef4 --- /dev/null +++ b/certora/stata/harness/pool/SymbolicLendingPool.sol @@ -0,0 +1,108 @@ +pragma solidity ^0.8.10; +pragma experimental ABIEncoderV2; + +import {IERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {IAToken} from "aave-v3-core/contracts/interfaces/IAToken.sol"; +import {DataTypes} from 'aave-v3-core/contracts/protocol/libraries/types/DataTypes.sol'; + +contract SymbolicLendingPool { + // an underlying asset in the pool + IERC20 public underlyingToken; + // the aToken associated with the underlying above + IAToken public aToken; + // This index is used to convert the underlying token to its matching + // AToken inside the pool, and vice versa. + uint256 public liquidityIndex; + + /** + * @dev Deposits underlying token in the Atoken's contract on behalf of the user, + and mints Atoken on behalf of the user in return. + * @param asset The underlying sent by the user and to which Atoken shall be minted + * @param amount The amount of underlying token sent by the user + * @param onBehalfOf The recipient of the minted Atokens + * @param referralCode A unique code (unused) + **/ + function deposit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode + ) external { + require(asset == address(underlyingToken)); + underlyingToken.transferFrom( + msg.sender, + address(aToken), + amount + ); + aToken.mint( + msg.sender, + onBehalfOf, + amount, + liquidityIndex + ); + } + + /** + * @dev Burns Atokens in exchange for underlying asset + * @param asset The underlying asset to which the Atoken is connected + * @param amount The amount of underlying tokens to be burned + * @param to The recipient of the burned Atokens + * @return The `amount` of tokens withdrawn + **/ + function withdraw( + address asset, + uint256 amount, + address to + ) external returns (uint256) { + require(asset == address(underlyingToken)); + aToken.burn( + msg.sender, + to, + amount, + liquidityIndex + ); + return amount; + } + + /** + * @dev A simplification returning a constant + * @param asset The underlying asset to which the Atoken is connected + * @return liquidityIndex the `liquidityIndex` of the asset + **/ + function getReserveNormalizedIncome(address asset) + external + view + virtual + returns (uint256) + { + return liquidityIndex; + } + + DataTypes.ReserveDataLegacy reserveLegacy; + DataTypes.ReserveData reserve; + + 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/rewards/RewardsControllerHarness.sol b/certora/stata/harness/rewards/RewardsControllerHarness.sol new file mode 100644 index 00000000..0cd97b2d --- /dev/null +++ b/certora/stata/harness/rewards/RewardsControllerHarness.sol @@ -0,0 +1,48 @@ + +pragma solidity ^0.8.10; + +import {RewardsController, RewardsDataTypes} from 'aave-v3-periphery/contracts/rewards/RewardsController.sol'; + +contract RewardsControllerHarness is RewardsController{ + + constructor(address emissionManager) RewardsController(emissionManager) {} + + // returns the available rewardscount of a given asset in the rewards controller + function getAvailableRewardsCount(address asset) + external + view + returns (uint128) + { + return _assets[asset].availableRewardsCount; + } + + // returns the i-th available reward of a given asset in the rewards controller + /// @dev assume i < availableRewardsCount + function getRewardsByAsset(address asset, uint128 i) external view returns (address) { + return _assets[asset].availableRewards[i]; + } + + // returns the i-th asset in the reward controller + function getAssetByIndex(uint256 i) external view returns (address) { + return _assetsList[i]; + } + + // returns the length of the asset list in the reward controller + function getAssetListLength() external view returns (uint256) { + return _assetsList.length; + } + + // returns the a user's accrued rewards for a given reward baring asset and a specified reward + function getUserAccruedReward( + address user, + address asset, + address reward + ) external view returns (uint256) { + return _assets[asset].rewards[reward].usersData[user].accrued; + } + + // returns the a user's reward index for a given reward baring asset and a specified reward + function getRewardsIndex(address asset, address reward) external view returns (uint256){ + return _assets[asset].rewards[reward].index; + } +} diff --git a/certora/stata/harness/rewards/TransferStrategyHarness.sol b/certora/stata/harness/rewards/TransferStrategyHarness.sol new file mode 100644 index 00000000..2007e418 --- /dev/null +++ b/certora/stata/harness/rewards/TransferStrategyHarness.sol @@ -0,0 +1,22 @@ + +pragma solidity ^0.8.10; + +import {IERC20} from 'aave-v3-core/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {TransferStrategyBase} from 'aave-v3-periphery/contracts/rewards/transfer-strategies/TransferStrategyBase.sol'; + +contract TransferStrategyHarness is TransferStrategyBase{ + +constructor(address incentivesController, address rewardsAdmin) TransferStrategyBase(incentivesController, rewardsAdmin) {} + + IERC20 public REWARD; + + // executes the actual transfer of the reward to the receiver + function performTransfer( + address to, + address reward, + uint256 amount + ) external override(TransferStrategyBase) returns (bool){ + require(reward == address(REWARD)); + return REWARD.transfer(to, amount); + } +} diff --git a/certora/stata/harness/tokens/DummyERC20Impl.sol b/certora/stata/harness/tokens/DummyERC20Impl.sol new file mode 100644 index 00000000..d6f32d65 --- /dev/null +++ b/certora/stata/harness/tokens/DummyERC20Impl.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +contract DummyERC20Impl { + uint256 t; + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + 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] -= 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; + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool) { + 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_aTokenUnderlying.sol b/certora/stata/harness/tokens/DummyERC20_aTokenUnderlying.sol new file mode 100644 index 00000000..06460386 --- /dev/null +++ b/certora/stata/harness/tokens/DummyERC20_aTokenUnderlying.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.10; +import "./DummyERC20Impl.sol"; + +contract DummyERC20_aTokenUnderlying is DummyERC20Impl {} \ No newline at end of file diff --git a/certora/stata/harness/tokens/DummyERC20_rewardToken.sol b/certora/stata/harness/tokens/DummyERC20_rewardToken.sol new file mode 100644 index 00000000..8b8f7e8a --- /dev/null +++ b/certora/stata/harness/tokens/DummyERC20_rewardToken.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; +import "./DummyERC20Impl.sol"; + +contract DummyERC20_rewardToken is DummyERC20Impl {} diff --git a/certora/stata/munged/.gitignore b/certora/stata/munged/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/certora/stata/munged/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/certora/stata/scripts/run-all.sh b/certora/stata/scripts/run-all.sh new file mode 100644 index 00000000..801e0ffa --- /dev/null +++ b/certora/stata/scripts/run-all.sh @@ -0,0 +1,95 @@ +#CMN="--compilation_steps_only" + +echo "******** Running: 1 ***************" +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" + +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 depositATokensCheckIndexGRayAssert2 depositWithPermitCheckIndexGRayAssert2 depositCheckIndexERayAssert2 depositATokensCheckIndexERayAssert2 depositWithPermitCheckIndexERayAssert2 mintCheckIndexGRayUpperBound mintCheckIndexGRayLowerBound mintCheckIndexEqualsRay \ +--msg "2: verifyERC4626MintDepositSummarization.conf" + +echo "******** Running: 3 ***************" +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: " + +echo "******** Running: 5 ***************" +certoraRun $CMN certora/stata/conf/verifyERC4626Extended.conf --rule redeemSum \ +--msg "5: " + +echo "******** Running: 6 ***************" +certoraRun $CMN certora/stata/conf/verifyERC4626Extended.conf --rule redeemATokensSum \ +--msg "6: " + +echo "******** Running: 7 ***************" +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/verifyAToken.conf --rule aTokenBalanceIsFixed_for_claimSingleRewardOnBehalf aTokenBalanceIsFixed_for_claimRewardsToSelf \ +--msg "8: " + +echo "******** Running: 9 ***************" +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule rewardsConsistencyWhenSufficientRewardsExist \ +--msg "9: " + +echo "******** Running: 10 ***************" +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule rewardsConsistencyWhenInsufficientRewards \ +--msg "10: " + +echo "******** Running: 11 ***************" +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule totalClaimableRewards_stable \ +--msg "11: " + +echo "******** Running: 12 ***************" +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/verifyStataToken.conf --rule solvency_total_asset_geq_total_supply \ +--msg "13: " + +echo "******** Running: 14 ***************" +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule singleAssetAccruedRewards \ +--msg "14: " + +echo "******** Running: 15 ***************" +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule totalAssets_stable \ +--msg "15: " + +echo "******** Running: 16 ***************" +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule getClaimableRewards_stable \ +--msg "16: " + +echo "******** Running: 17 ***************" +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule getClaimableRewards_stable_after_deposit \ +--msg "17: " + +echo "******** Running: 18 ***************" +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule getClaimableRewards_stable_after_refreshRewardTokens \ +--msg "18: " + +echo "******** Running: 19 ***************" +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule getClaimableRewardsBefore_leq_claimed_claimRewardsOnBehalf \ +--msg "19: " + +echo "******** Running: 20 ***************" +certoraRun $CMN certora/stata/conf/verifyStataToken.conf --rule rewardsTotalDeclinesOnlyByClaim \ +--msg "20: " + +echo "******** Running: 21 ***************" +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/verifyDoubleClaim.conf --rule prevent_duplicate_reward_claiming_single_reward_insufficient \ +--msg "22: " \ No newline at end of file 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/StataToken/aTokenProperties.spec b/certora/stata/specs/StataToken/aTokenProperties.spec new file mode 100644 index 00000000..55d4e705 --- /dev/null +++ b/certora/stata/specs/StataToken/aTokenProperties.spec @@ -0,0 +1,246 @@ + +import "../methods/methods_base.spec"; + +////////////////// FUNCTIONS ////////////////////// + + /// @title Sum of scaled balances of AToken + ghost mathint sumAllATokenScaledBalance { + init_state axiom sumAllATokenScaledBalance == 0; + } + + + /// @dev sample struct UserState {uint128 balance; uint128 additionalData; } + hook Sstore _AToken._userState[KEY address a] .(offset 0) uint128 balance (uint128 old_balance) { + sumAllATokenScaledBalance = sumAllATokenScaledBalance + balance - old_balance; + // havoc sumAllATokenScaledBalance() assuming sumAllATokenScaledBalance()@new() == sumAllATokenScaledBalance()@old() + balance - old_balance; + } + + hook Sload uint128 balance _AToken._userState[KEY address a] .(offset 0) { + require to_mathint(balance) <= sumAllATokenScaledBalance; + } + +///////////////// Properties /////////////////////// + + /** + * @title User AToken balance is fixed + * Interaction with `StaticAtokenLM` should not change a user's AToken balance, + * except for the following methods: + * - `withdraw` + * - `deposit` + * - `redeem` + * - `mint` + * - `metaDeposit` + * - `metaWithdraw` + * + * Note. Rewards methods are special cases handled in other rules below. + * + * Rules passed (with rule sanity): job-id=`5fdaf5eeaca249e584c2eef1d66d73c7` + * + * Note. `UNDERLYING_ASSET_ADDRESS()` was unresolved! + */ + rule aTokenBalanceIsFixed(method f) filtered { + // Exclude balance changing methods + f -> (f.selector != sig:depositATokens(uint256,address).selector) && + (f.selector != sig:withdraw(uint256,address,address).selector) && + (f.selector != sig:redeemATokens(uint256,address,address).selector) && + (f.selector != sig:mint(uint256,address).selector) && + (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) + } { + + env e; + + // Limit sender + require e.msg.sender != currentContract; + require e.msg.sender != _AToken; + + uint256 preBalance = _AToken.balanceOf(e.msg.sender); + + calldataarg args; + f(e, args); + + uint256 postBalance = _AToken.balanceOf(e.msg.sender); + assert preBalance == postBalance, "aToken balance changed by static interaction"; + } + + rule aTokenBalanceIsFixed_for_collectAndUpdateRewards() { + env e; + + // Limit sender + require e.msg.sender != currentContract; + require e.msg.sender != _AToken; + require e.msg.sender != _DummyERC20_rewardToken; + + uint256 preBalance = _AToken.balanceOf(e.msg.sender); + + collectAndUpdateRewards(e, _DummyERC20_rewardToken); + + uint256 postBalance = _AToken.balanceOf(e.msg.sender); + assert preBalance == postBalance, "aToken balance changed by collectAndUpdateRewards"; + } + + + rule aTokenBalanceIsFixed_for_claimRewardsOnBehalf(address onBehalfOf, address receiver) { + // Create a rewards array + address[] _rewards; + require _rewards[0] == _DummyERC20_rewardToken; + require _rewards.length == 1; + + env e; + + // Limit sender + require ( + (e.msg.sender != currentContract) && + (onBehalfOf != currentContract) && + (receiver != currentContract) + ); + require ( + (e.msg.sender != _DummyERC20_rewardToken) && + (onBehalfOf != _DummyERC20_rewardToken) && + (receiver != _DummyERC20_rewardToken) + ); + require (e.msg.sender != _AToken) && (onBehalfOf != _AToken) && (receiver != _AToken); + + uint256 preBalance = _AToken.balanceOf(e.msg.sender); + + claimRewardsOnBehalf(e, onBehalfOf, receiver, _rewards); + + uint256 postBalance = _AToken.balanceOf(e.msg.sender); + assert preBalance == postBalance, "aToken balance changed by claimRewardsOnBehalf"; + } + + + rule aTokenBalanceIsFixed_for_claimSingleRewardOnBehalf(address onBehalfOf, address receiver) { + env e; + + // Limit sender + require ( + (e.msg.sender != currentContract) && + (onBehalfOf != currentContract) && + (receiver != currentContract) + ); + require ( + (e.msg.sender != _DummyERC20_rewardToken) && + (onBehalfOf != _DummyERC20_rewardToken) && + (receiver != _DummyERC20_rewardToken) + ); + require (e.msg.sender != _AToken) && (onBehalfOf != _AToken) && (receiver != _AToken); + + uint256 preBalance = _AToken.balanceOf(e.msg.sender); + + claimSingleRewardOnBehalf(e, onBehalfOf, receiver, _DummyERC20_rewardToken); + + uint256 postBalance = _AToken.balanceOf(e.msg.sender); + assert preBalance == postBalance, "aToken balance changed by claimSingleRewardOnBehalf"; + } + + + rule aTokenBalanceIsFixed_for_claimRewardsToSelf() { + // Create a rewards array + address[] _rewards; + require _rewards[0] == _DummyERC20_rewardToken; + require _rewards.length == 1; + + env e; + + // Limit sender + require e.msg.sender != currentContract; + require e.msg.sender != _AToken; + require e.msg.sender != _DummyERC20_rewardToken; + + uint256 preBalance = _AToken.balanceOf(e.msg.sender); + + claimRewardsToSelf(e, _rewards); + + uint256 postBalance = _AToken.balanceOf(e.msg.sender); + assert preBalance == postBalance, "aToken balance changed by claimRewardsToSelf"; + } + + + rule aTokenBalanceIsFixed_for_claimRewards(address receiver) { + // Create a rewards array + address[] _rewards; + require _rewards[0] == _DummyERC20_rewardToken; + require _rewards.length == 1; + + env e; + + // Limit sender + require (e.msg.sender != currentContract) && (receiver != currentContract); + require ( + (e.msg.sender != _DummyERC20_rewardToken) && (receiver != _DummyERC20_rewardToken) + ); + require (e.msg.sender != _AToken) && (receiver != _AToken); + + uint256 preBalance = _AToken.balanceOf(e.msg.sender); + + claimRewards(e, receiver, _rewards); + + uint256 postBalance = _AToken.balanceOf(e.msg.sender); + assert preBalance == postBalance, "aToken balance changed by claimRewards"; + } + + /// @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).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(); + } + } + + /// @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_redeem(address user) + _AToken.balanceOf(user) <= _AToken.totalSupply() + filtered { f -> f.selector == sig:redeem(uint256,address,address).selector } + { + preserved with (env e){ + requireInvariant sumAllATokenScaledBalance_eq_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 + //https://vaas-stg.certora.com/output/99352/4f91637a96d647baab9accb1093f1690/?anonymousKey=53ccda4a9dd8988205d4b614d9989d1e4148533f + invariant sumAllATokenScaledBalance_eq_totalSupply() + sumAllATokenScaledBalance == to_mathint(_AToken.scaledTotalSupply()) + filtered { f -> !harnessOnlyMethods(f) } + + + /// @title AToken scaledBalancerOf(user) <= AToken scaledTotalSupply() + //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() + filtered { f -> !harnessOnlyMethods(f) } + { + preserved { + requireInvariant sumAllATokenScaledBalance_eq_totalSupply(); + } + } diff --git a/certora/stata/specs/StataToken/double_claim.spec b/certora/stata/specs/StataToken/double_claim.spec new file mode 100644 index 00000000..466fa3ee --- /dev/null +++ b/certora/stata/specs/StataToken/double_claim.spec @@ -0,0 +1,65 @@ +import "../methods/methods_multi_reward.spec"; + +///////////////// Properties /////////////////////// + + /// @dev Broke the rule into two cases to speed up verification + + /** + * @title Claiming the same reward twice assuming sufficient rewards + * Using an array with the same reward twice does not give more rewards, + * assuming the contract has sufficient rewards. + * + * @dev Passed in job-id=`54de623f62eb4c95a343ee38834c6d16` + */ + rule prevent_duplicate_reward_claiming_single_reward_sufficient() { + single_RewardToken_setup(); + rewardsController_arbitrary_single_reward_setup(); + + env e; + require e.msg.sender != currentContract; // Cannot claim to contract + + uint256 initialBalance = _DummyERC20_rewardToken.balanceOf(e.msg.sender); + mathint claimable = getClaimableRewards(e, e.msg.sender,_DummyERC20_rewardToken); + + // Ensure contract has sufficient rewards + require to_mathint(_DummyERC20_rewardToken.balanceOf(currentContract)) >= claimable; + + // Duplicate claim + claimDoubleRewardOnBehalfSame(e, e.msg.sender, e.msg.sender, _DummyERC20_rewardToken); + + uint256 duplicateClaimBalance = _DummyERC20_rewardToken.balanceOf(e.msg.sender); + mathint diff = duplicateClaimBalance - initialBalance; + uint256 unclaimed = getUnclaimedRewards(e.msg.sender, _DummyERC20_rewardToken); + + assert diff + unclaimed <= claimable, "Duplicate claim changes rewards"; + } + + /** + * @title Claiming the same reward twice assuming insufficient rewards + * Using an array with the same reward twice does not give more rewards, + * assuming the contract does not have sufficient rewards. + * + * @dev Passed in job-id=`54de623f62eb4c95a343ee38834c6d16` + */ + rule prevent_duplicate_reward_claiming_single_reward_insufficient() { + single_RewardToken_setup(); + rewardsController_arbitrary_single_reward_setup(); + + env e; + require e.msg.sender != currentContract; // Cannot claim to contract + + uint256 initialBalance = _DummyERC20_rewardToken.balanceOf(e.msg.sender); + mathint claimable = getClaimableRewards(e, e.msg.sender,_DummyERC20_rewardToken); + + // Ensure contract does not have sufficient rewards + require to_mathint(_DummyERC20_rewardToken.balanceOf(currentContract)) < claimable; + + // Duplicate claim + claimDoubleRewardOnBehalfSame(e, e.msg.sender, e.msg.sender, _DummyERC20_rewardToken); + + uint256 duplicateClaimBalance = _DummyERC20_rewardToken.balanceOf(e.msg.sender); + mathint diff = duplicateClaimBalance - initialBalance; + uint256 unclaimed = getUnclaimedRewards(e.msg.sender, _DummyERC20_rewardToken); + + assert diff + unclaimed <= claimable, "Duplicate claim changes rewards"; + } diff --git a/certora/stata/specs/erc4626/erc4626.spec b/certora/stata/specs/erc4626/erc4626.spec new file mode 100644 index 00000000..b790c3fb --- /dev/null +++ b/certora/stata/specs/erc4626/erc4626.spec @@ -0,0 +1,754 @@ +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 * + *****************************/ + + /*** + * rule to check the following for the previewDeposit function: + * _1. 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. + */ + // STATUS: Verified, that the amount returned by previewDeposit is exactly equal to that returned by the deposit function. + // This is a stronger property than the one required by the EIP. + // https://vaas-stg.certora.com/output/11775/1488de4bb1e24d37a7972b0c2785df65/?anonymousKey=6f68dd14376fa7d0109ef2687f72d1ef1903dda8 + + ///@title previewDeposit returns the right value + ///@notice EIP4626 dictates that previewDeposit 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. The previewDeposit function in staticAToken contract returns a value exactly equal to that returned by the deposit function. + rule previewDepositAmountCheck(){ + env e1; + env e2; + uint256 assets; + address receiver; + uint256 previewShares; + uint256 shares; + + previewShares = previewDeposit(e1, assets); + shares = deposit(e2, assets, receiver); + + assert previewShares == shares,"preview shares should be equal to actual shares"; + } + + // The EIP4626 spec requires that the previewDeposit function must not account for maxDeposit limit or the allowance of asset tokens. + // The following rule checks that the value returned by the previewDeposit function is independent of allowance that the contract might have + // for transferring assets from any user. + + // STATUS: Verified + // https://vaas-stg.certora.com/output/11775/05df2a231ec74da28ed10f627d3c7f72/?anonymousKey=70c692cbbf781597e0dc0b53a7d4ed6968bb467a + + ///@title previewDeposit independent of Allowance + ///@notice This rule checks that the value returned by the previewDeposit function is independent of allowance that the contract might have for transferring assets from any user. The value retunred is the same regardless of the specified asset amount being more than, equal to or less than the allowance. + rule previewDepositIndependentOfAllowanceApprove() + { + env e1; + env e2; + env e3; + env e4; + env e5; + address user; + uint256 ATokAllowance1 = _AToken.allowance(currentContract, user); + uint256 assets1; + require assets1 < ATokAllowance1; + uint256 previewShares1 = previewDeposit(e1, assets1); + + uint256 amount1; + _AToken.approve(e2, currentContract, amount1); + + uint256 ATokAllowance2 = _AToken.allowance(currentContract, user); + require assets1 == ATokAllowance2; + uint256 previewShares2 = previewDeposit(e3, assets1); + + uint256 amount2; + _AToken.approve(e4, currentContract, amount2); + + uint256 ATokAllowance3 = _AToken.allowance(currentContract, user); + require assets1 > ATokAllowance3; + uint256 previewShares3 = previewDeposit(e5, assets1); + + assert previewShares1 == previewShares2,"previewDeposit should not change regardless of assets > or = allowance"; + assert previewShares2 == previewShares3,"previewDeposit should not change regardless of assets < or = allowance"; + } + + /**************************** + * previewMint * + *****************************/ + + /*** + * rule to check the following for the previewMint function: + * _1. 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. + */ + // STATUS: Verified, that the amount returned by previewMint is exactly equal to that returned by the deposit function. + // This is a stronger property than the one required by the EIP. + // https://vaas-stg.certora.com/output/11775/97ed98809a464668b0bfbfb6f6a6277b/?anonymousKey=e8f91f54cebea2f42d809068cf55511670b817d4 + ///@title previewMint returns the right value + ///@notice EIP4626 dictates that previewMint must return as close to and no more than the exact amount of assets that would be deposited in a mint call in the same transaction. The previewMint function in staticAToken contract returns a value exactly equal to that returned by the mint function. + rule previewMintAmountCheck(env e){ + uint256 shares; + address receiver; + uint256 previewAssets; + uint256 assets; + + previewAssets = previewMint(shares); + assets = mint(e, shares, receiver); + assert previewAssets == assets,"preview should be equal to actual"; + } + + + // The EIP4626 spec requires that the previewMint function must not account for mint limits like those returned from maxMint + // and should always act as though the mint would be accepted, regardless whether the user has approved the contract to transfer + // the specified amount of assets + + // The following rule checks that the previewMint returned value is independent of allowance of assets. The value returned by + // previewMind under three conditions a. amount < allowance from any user b. amount = allowance from any user c. amount > allowance + // from any user. The returned value is the same in all cases thus making it independent of the allowance from any user + // STATUS: Verified + + // https://vaas-stg.certora.com/output/11775/937cb9bc984947de98c9bf759b483017/?anonymousKey=db3080cc2ddcf91fe3e7dab4d4a56dad24e6bbce + ///@title previewMint independent of Allowance + ///@notice This rule checks that the value returned by the previewMint function is independent of allowance that the contract might have for transferring assets from any user. The value returned is the same regardless of the equivalent asset amount being more than, equal to or less than the allowance. + rule previewMintIndependentOfAllowance(){ + // allowance of currentContract for asset transfer from msg.sender to + address user; + uint256 ATokAllowance1 = _AToken.allowance(currentContract, user); + uint256 shares1; + uint256 assets1; + uint256 assets2; + env e1; + require convertToAssets(e1, shares1) < ATokAllowance1; + uint256 previewAssets1 = previewMint(shares1); + + env e2; + address receiver1; + deposit(e2, assets1, receiver1); + + uint256 ATokAllowance2 = _AToken.allowance(currentContract, user); + env e3; + require convertToAssets(e3, shares1) == ATokAllowance2; + uint256 previewAssets2 = previewMint(shares1); + + env e4; + address receiver2; + deposit(e2, assets2, receiver2); + + env e5; + uint256 ATokAllowance3 = _AToken.allowance(currentContract, user); + require convertToAssets(e4, shares1) > ATokAllowance3; + uint256 previewAssets3 = previewMint(shares1); + + assert previewAssets1 == previewAssets2,"previewMint should not change regardless of C2A(shares) > or = allowance"; + assert previewAssets2 == previewAssets3,"previewMint should not change regardless of C2A(shares) < or = allowance"; + } + + /******************************** + * previewWithdraw * + *********************************/ + + /*** + * rule to check the following for the previewWithdraw function: + * _1. 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 + */ + // STATUS: Verified, that the amount returned by previewWithdraw is exactly equal to that returned by the withdraw function. + // This is a stronger property than the one required by the EIP. + // https://vaas-stg.certora.com/output/11775/444832541b5f4f22ab7373f6de1ee782/?anonymousKey=86856741d701630321afe5bc573fc258bbd99739 + ///@title previewWithdraw returns the right value + ///@notice EIP4626 dictates that previewWithdraw must return as close to and no more than the exact amount of shares that would be burned in a withdraw call in the same transaction. The previewWithdraw function in staticAToken contract returns a value exactly equal to that returned by the withdraw function. + rule previewWithdrawAmountCheck(env e){ + uint256 assets; + address receiver; + address owner; + uint256 shares; + uint256 previewShares; + + previewShares = previewWithdraw(assets); + shares = withdraw(e, assets, receiver, owner); + + assert previewShares == shares,"preview should be equal to actual shares"; + } + + // The EIP4626 spec requires that the previewWithdraw function must not account for withdrawal limits like those returned + // from maxWithdraw and should always act as though the withdrawal would be accepted, regardless of whether or not the user + // has enough shares, etc. + // This rules checks that the previewWithdraw function return value is independent of any level of maxWithdraw (relative to + // the asset amount) for any user + + // STATUS: Verified + // https://vaas-stg.certora.com/output/11775/50abf537cd134084ab309788a0d4b95a/?anonymousKey=c9cbb863531b85f4a877260997f0acfb770e7e99 + + ///@title previewWithdraw independent of maxWithdraw + ///@notice This rule checks that the value returned by previewWithdraw is independent of the value returned by maxWithdraw. + rule previewWithdrawIndependentOfMaxWithdraw(env e){ + env e1; + env e2; + address user; + uint256 maxWithdraw1 = maxWithdraw(user); + uint256 assets1; + uint256 shares1; + uint256 shares2; + + require assets1 > maxWithdraw1; + uint256 previewShares1 = previewWithdraw(assets1); + + mint(e1, shares1, user); + + uint256 maxWithdraw2 = maxWithdraw(user); + require assets1 == maxWithdraw2; + uint256 previewShares2 = previewWithdraw(assets1); + + mint(e2, shares2, user); + + uint256 maxWithdraw3 = maxWithdraw(user); + require assets1 < maxWithdraw3; + uint256 previewShares3 = previewWithdraw(assets1); + + assert previewShares1 == previewShares2 && previewShares2 == previewShares3,"preview withdraw should be independent of allowance"; + } + + // The EIP4626 spec requires that the previewWithdraw function must not account for withdrawal limits like those returned by + // maxWithdraw and should always act as though the withdrawal would be accepted, regardless if the user has enough shares, etc. + // The following two rules checks that the previewWithdraw function is independent of any level of share balance(relative to asset amount) of + // any user + // STATUS: Verified + // https://vaas-stg.certora.com/output/11775/8e8fd50a3fba4018b924eb6d8764d77f/?anonymousKey=3fee78908151c06e470add0ed2a9f4479f9bea7b + + ///@title previewWithdraw independent of any user's share balance + ///@notice This rule checks that the value returned by the previewWithdraw function is independent of any user's share balance. The value retunred is the same regardless it being >, = or < any user's balance. + rule previewWithdrawIndependentOfBalance1(){ + env e1; + env e2; + env e3; + + address user; + uint256 shareBal1 = balanceOf(user); + uint256 assets1; + uint256 shares1; + uint256 shares2; + + require assets1 > convertToAssets(e1, shareBal1);//asset amount greater than what the user is entitled to on account of his share balance + uint256 previewShares1 = previewWithdraw(assets1); + + _mintWrapper(e2, user, shares1); + + uint256 shareBal2 = balanceOf(user); + require assets1 == convertToAssets(e3, shareBal2); //asset amount equal to what the user is entitled to on account of his share balance + uint256 previewShares2 = previewWithdraw(assets1); + + assert previewShares1 == previewShares2, + "preview withdraw should be independent of allowance"; + } + + // STATUS: Verified + // https://vaas-stg.certora.com/output/11775/c686d90f1baf4a77a093d5902125f08f/?anonymousKey=da2ce2f7098c87d89abb767139e689017bd618b1 + + rule previewWithdrawIndependentOfBalance2(){ + env e1; + env e2; + env e3; + + address user; + uint256 shareBal1 = balanceOf(user); + uint256 assets1; + uint256 shares1; + uint256 shares2; + + require assets1 == convertToAssets(e1, shareBal1);//asset amount greater than what the user is entitled to on account of his share balance + uint256 previewShares1 = previewWithdraw(assets1); + + _mintWrapper(e2, user, shares1); + + uint256 shareBal2 = balanceOf(user); + require assets1 < convertToAssets(e3, shareBal2); //asset amount equal to what the user is entitled to on account of his share balance + uint256 previewShares2 = previewWithdraw(assets1); + + assert previewShares1 == previewShares2, + "preview withdraw should be independent of allowance"; + } + + /****************************** + * previewRedeem * + *******************************/ + + /*** + * rule to check the following for the previewRedeem function: + * _1. 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. + */ + // STATUS: Verified, that the amount returned by previewRedeem is exactly equal to that returned by the redeem function. + // This is a stronger property than the one required by the EIP. + // https://vaas-stg.certora.com/output/11775/24e2fe4d485a42618e4e38f0d4376dd2/?anonymousKey=a117a61d3d1dea53fbc875be84292f27af3afd6a + + ///@title previewRedeem returns the right value + ///@notice EIP4626 dictates that previewRedeem must return as close to and no more than the exact amount of assets that would be returned in a redeem call in the same transaction. The previewRedeem function in staticAToken contract returns a value exactly equal to that returned by the redeem function. + rule previewRedeemAmountCheck(env e){ + uint256 shares; + address receiver; + address owner; + uint256 previewAssets; + uint256 assets; + + previewAssets = previewRedeem(shares); + assets = redeem(e, shares, receiver, owner); + + assert previewAssets == assets,"preview should the same as the actual assets received"; + } + + // The EIP4626 spec requires that the previewRedeem function must not account for redemption limits like those returned by + // the maxRedeem function and should always act as though the redemption would be accepted, regardless if the user has enough + // shares, etc. + // + // The following two rules checks that the previewRedeem return value is independent of any level of maxRedeem (relative to the share amount) for any user. + + // STATUS: Verified + // https://vaas-stg.certora.com/output/11775/e1d9f84456b04e3caa0c4495f3022bb8/?anonymousKey=d82a8ae9fd795f8206f8c117bf5698079c2239cb + + ///@title previewRedeem independent of maxRedeem + ///@notice This rule checks that the value returned by the previewRedeem function is independent of the value returned by maxRedeem. The value retunred is the same regardless of it being >, = or < the value returned by maxRedeem. + rule previewRedeemIndependentOfMaxRedeem1(){ + env e1; + env e2; + address user; + uint256 shares1; + uint256 shares2; + + uint256 maxRedeemableShares1 = maxRedeem(user); + require shares1 == maxRedeemableShares1; + uint256 previewAssets1 = previewRedeem(shares1); + + _mintWrapper(e1, user, shares2); + + uint256 maxRedeemableShares2 = maxRedeem(user); + require shares1 < maxRedeemableShares2; + uint256 previewAssets2 = previewRedeem(shares1); + + assert previewAssets1 == previewAssets2,"previewRedeem should be independent of maxRedeem"; + } + + // STATUS: Verified + // https://vaas-stg.certora.com/output/11775/16a4a248207b4ae28d778b0f405a3161/?anonymousKey=5efc898c58a75e7fa35d104b23ea3ef4ffe7ecf3 + rule previewRedeemIndependentOfMaxRedeem2(){ + env e1; + env e2; + address user; + uint256 shares1; + uint256 shares2; + + uint256 maxRedeemableShares1 = maxRedeem(user); + require shares1 > maxRedeemableShares1; + uint256 previewAssets1 = previewRedeem(shares1); + + _mintWrapper(e1, user, shares2); + + uint256 maxRedeemableShares2 = maxRedeem(user); + require shares1 == maxRedeemableShares2; + uint256 previewAssets2 = previewRedeem(shares1); + + assert previewAssets1 == previewAssets2,"previewRedeem should be independent of maxRedeem"; + } + + // The EIP4626 spec requires that the previewRedeem function must not account for redemption limits like those returned by maxRedeem + // and should always act as though the redemption would be accepted, regardless of whether the user has enough shares, etc. + // The following rule checks that the previewRedeem return value is independent of any level of share balance (relative to the redemption + // share amount) for any user. + // STATUS: Verified + // https://vaas-stg.certora.com/output/11775/de8e4742dbc44945b94e3a9b8e4375ae/?anonymousKey=65bd53e6365d5dd66f76004a80f45de06f088359 + + ///@title previewRedeem independent of any user's balance + ///@notice This rule checks that the value returned by the previewRedeem function is independent of any user's share balance. The value retunred is the same regardless of it being >, = or < any user's balance. + rule previewRedeemIndependentOfBalance(){ + env e1; + env e2; + env e3; + uint256 shares1; + uint256 shares2; + uint256 shares3; + address user1; + uint256 balance1 = balanceOf(user1); + require shares1 > balance1; + uint256 previewAssets1 = previewRedeem(shares1); + + mint(e1, shares2, user1); + uint256 balance2 = balanceOf(user1); + require shares1 == balance2; + uint256 previewAssets2 = previewRedeem(shares1); + + mint(e1, shares3, user1); + uint256 balance3 = balanceOf(user1); + require shares1 < balance3; + uint256 previewAssets3 = previewRedeem(shares1); + + assert previewAssets1 == previewAssets2 && previewAssets2 == previewAssets3,"previewRedeem should be independent of balance"; + } + + /**************************** + * withdraw * + ****************************/ + + /*** + * rule to check the following for the withdraw function: + * 1. SHOULD check msg.sender can spend owner funds, assets needs to be converted to shares and shares should be checked for allowance. + * 2. MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, slippage, the owner not having enough shares, etc). + */ + // STATUS: VERIFIED + // violates #2 above. For any asset amount worth less than 1/2 AToken, the function will not withdrawn anything and not revert. EIP 4626 non compliant for assets < 1/2 AToken. + // For assets amount worth less than 1/2 AToken 0 assets will be withdrawn. Asset amount worth 1/2 AToken and more the final withdrawn amount would be assets +- 1/2AToken. + // https://vaas-stg.certora.com/output/11775/a2ff16b9d15d405cb11572afd0ea9413/?anonymousKey=2d51005a275559a456558660e33de6870aa19846 + ///@title Allowance and withdrawn amount check for withdraw function + ///@notice This rules checks that the withdraw function burns shares upto the allowance for the msg.sender and that the assets withdrawn are within the specified asset amount +- 1/2ATokens range + rule withdrawCheck(env e){ + address owner; + address receiver; + uint256 assets; + + uint256 allowed = allowance(e, owner, e.msg.sender); + uint256 balBefore = _AToken.balanceOf(receiver); + uint256 shareBalBefore = balanceOf(owner); + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); + require e.msg.sender != currentContract; + require receiver != currentContract; + require owner != currentContract; + + require index >= RAY(); + + uint256 sharesBurnt = withdraw(e, assets, receiver, owner); + + uint256 balAfter = _AToken.balanceOf(receiver); + uint256 shareBalAfter = balanceOf(owner); + + // checking for allowance in case msg.sender is not the owner + assert e.msg.sender != owner => allowed >= sharesBurnt,"msg.sender should have allowane to spend owner's shares"; + + // lower bound. First part means atleast 1/2 AToken worth of UL is being deposited + assert assets * 2 * RAY() >= to_mathint(index) => balAfter - balBefore > assets - index/2*RAY(), + "withdrawn amount should be no less than 1/2 AToken worth of UL less than the assets amount"; + + //upper bound + assert balAfter - balBefore <= assets + index/2*RAY(), + "withdrawn amount should be no more than 1/2 AToken worth of UL more than the number of assets "; + } + + /************************** + * redeem * + **************************/ + + /*** + * 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 redeemCheck(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 = redeem(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"; + } + + /*** + * 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 * + *****************************/ + + /*** + * rule to check the following for the covertToAssets function: + * 1. MUST NOT show any variations depending on the caller. + * 2. MUST round down towards 0. + */ + // STATUS: VERIFIED + // https://vaas-stg.certora.com/output/11775/52075caad70145798090e1038b16e6d0/?anonymousKey=b79fa800a2885356277ca6690c723fece38c7b40 + ///@title convert to assets function check + ///@notice This rule checks that the convertToAssets function will return the same amount for assets for the given number of shares under all conditions and the calculation will always round down. + rule convertToAssetsCheck(){ + env e1; + env e2; + env e3; + uint256 shares1; + uint256 shares2; + storage before = lastStorage; + + mathint assets1 = convertToAssets(e1, shares1) at before; + mathint assets2 = convertToAssets(e2, shares1) at before; + mathint assets3 = convertToAssets(e2, shares2) at before; + mathint combinedAssets = convertToAssets(e3, require_uint256(shares1 +shares2)) at before; + + // assert !lastReverted,"should not revert except for overflow"; + assert assets1 == assets2,"conversion to assets should be independent of env such as msg.sender"; + assert shares1 + shares2 <= max_uint256 => assets1 + assets3 <= combinedAssets,"conversion should round down and not up"; + } + + /// @title Converting amount to shares is properly rounded down + rule amountConversionRoundedDown(uint256 amount) { + env e; + uint256 shares = convertToShares(e, amount); + assert convertToAssets(e, shares) <= amount, "Too many converted shares"; + + /* The next assertion shows that the rounding in `convertToAssets` is tight. This + * protects the user. For example, a function `convertToAssets` that always returns + * zero would have passed the previous assertion, but not the next one. + */ + assert convertToAssets(e, require_uint256(shares + 1)) >= amount, "Too few converted shares"; + } + + /** + * @title ConvertToAssets must not revert unless due to integer overflow + * From EIP4626: + * > MUST NOT revert unless due to integer overflow caused by an unreasonably large input. + * We define large input as 10^45. To be precise we need that `shares * rate < 2^256 ~= 10^77`, + * hence we require that: + * - `shares < 10^45` + * - `rate < 10^32` + */ + rule toAssetsDoesNotRevert(uint256 shares) { + require shares < 10^45; + env e; + require e.msg.value == 0; + + // Prevent revert due to overflow. + // Roughly speaking ConvertToAssets returns shares * rate() / RAY. + mathint ray_math = to_mathint(RAY()); + mathint rate_math = to_mathint(rate()); + mathint shares_math = to_mathint(shares); + require rate_math < 10^32; + + uint256 assets = convertToAssets@withrevert(e, shares); + bool reverted = lastReverted; + + assert !reverted, "Conversion to assets reverted"; + } + + /***************************** + * convertToShares * + *****************************/ + + /*** + * rule to check the following for the convertToShares function: + * 1. MUST NOT show any variations depending on the caller. + * 2. MUST round down towards 0. + */ + // STATUS: VERIFIED + // https://vaas-stg.certora.com/output/11775/a75adca8d9914e80bf09bbaeb168f0f8/?anonymousKey=34ac3fe43e28e4722c7d4211af6e3e1077dc3b22 + ///@title convert to shares function check + ///@notice This rule checks that the convertToShares function will return the same amount for shares for the given number of assets under all conditions and the calculation will always round down. + rule convertToSharesCheck(){ + env e1; + env e2; + env e3; + uint256 assets1; + 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, 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"; + } + + /// @title Converting shares to amount is properly rounded down + rule sharesConversionRoundedDown(uint256 shares) { + env e; + uint256 amount = convertToAssets(e, shares); + assert convertToShares(e, amount) <= shares, "Amount converted is too high"; + + /* The next assertion shows that the rounding in `convertToShares` is tight. + * For example, a function `convertToShares` that always returns zero + * would have passed the previous assertion, but not the next one. + */ + assert convertToShares(e, require_uint256(amount + 1)) >= shares, "Amount converted is too low"; + } + + /** + * @title ConvertToShares must not revert except for overflow + * From EIP4626: + * > MUST NOT revert unless due to integer overflow caused by an unreasonably large input. + * We define large input as `10^50`. To be precise, we need that `RAY * assets < 2^256`, since + * `2^256~=10^77` and `RAY=10^27` we get that `assets < 10^50`. + * + * Note. *We also require that:* **`rate > 0`**. + */ + rule toSharesDoesNotRevert(uint256 assets) { + require assets < 10^50; + env e; + require e.msg.value == 0; + + // Prevent revert due to overflow. + // Roughly speaking ConvertToShares returns assets * RAY / rate(). + mathint ray_math = to_mathint(RAY()); + mathint rate_math = to_mathint(rate()); + mathint assets_math = to_mathint(assets); + require rate_math > 0; + + uint256 shares = convertToShares@withrevert(e, assets); + bool reverted = lastReverted; + + 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() > 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"; + } + + /********************** + * 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)"; + } + + /************************ + * 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() > 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 * + **************************/ + + // totalAssets must not revert + 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() > 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 new file mode 100644 index 00000000..f6675d10 --- /dev/null +++ b/certora/stata/specs/erc4626/erc4626DepositSummarization.spec @@ -0,0 +1,163 @@ +import "../methods/methods_base.spec"; + +/////////////////// Methods //////////////////////// + +methods{ + // static aToken + // ------------- + function previewDeposit(uint256) external returns(uint256) envfree => NONDET; + function ERC20Upgradeable._mint(address, uint256) internal => NONDET; + + // rewards controller + // ------------------ + function _.handleAction(address, uint256, uint128) external => NONDET; +} + +///////////////// Properties /////////////////////// + + /********************* + * deposit * + **********************/ + + /*** + * rule to check the following for the deposit function: + * 1. MUST revert if all of assets cannot be deposited + */ + + // 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 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 depositCheckIndexGRayAssert1(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 = deposit(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 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 + ///@notice This rule checks that, for index == RAY, the deposit function will deposit upto 1/2AToken more than the amount specified deposit amount + rule depositCheckIndexERayAssert1(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 = deposit(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 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 new file mode 100644 index 00000000..c84370b9 --- /dev/null +++ b/certora/stata/specs/erc4626/erc4626Extended.spec @@ -0,0 +1,254 @@ +import "../methods/methods_base.spec"; + +///////////////// Properties /////////////////////// + + /*** + * #### A note on the conversion functions + * The conversion functions are: + * - assets to shares = `S(a) = (a * R) // r` + * - shares to assets = `A(s) = (s * r) // R` + * where a=assets, s=shares, R=RAY, r=rate. + * + * These imply: + * - `a * R - r < S(a) * r <= a * R a*R/r - 1 < S(a) <= a*R/r` + * - `s * r - R < A(s) * R <= s * r s*r/R - 1 < A(s) <= s*r/R` + * + * Hence: + * - `A(S(a)) > S(a)*r/R - 1 > (a*R/r - 1)*r/R - 1 = (a*R - r)/R - 1 = a - r/R - 1` + * - `S(A(s)) > A(s)*R/r - 1 > (s*r/R - 1)*R/r - 1 = (s*r - R)/r - 1 = s - R/r - 1` + */ + + /***************************** + * rounding range * + ******************************/ + + /** + * @title Ensure `previewWithdraw` tightly rounds up shares + * The lower bound (i.e. `previewWithdraw >= convertToShares`) follows from ERC4626. The upper bound + * is based on the current implementation. + */ + rule previewWithdrawRoundingRange(uint256 assets) { + env e; + uint256 shares = convertToShares(e, assets); + + assert previewWithdraw(assets) >= shares, "Preview withdraw takes less shares than converted"; + assert to_mathint(previewWithdraw(assets)) <= shares + 1, "Preview withdraw costs too many shares"; + } + + /** + * @title Ensure `previewRedeem` tightly rounds down assets + * The upper bound (i.e. `previewRedeem <= convertToAssets`) follows from ERC4626. The lower bound + * is based on the current implementation. + */ + rule previewRedeemRoundingRange(uint256 shares) { + env e; + uint256 assets = convertToAssets(e,shares); + + assert previewRedeem(shares) <= assets, "Preview redeem yields more assets than converted"; + assert previewRedeem(shares) + 1 + rate() / RAY() >= to_mathint(assets), "Preview redeem yields too few assets"; + } + + /** + * @title Inequality for conversion of amount to shares and back + * Note the precision depends on the ratio **`rate / RAY`**. + */ + rule amountConversionPreserved(uint256 amount) { + env e; + mathint mathamount = to_mathint(amount); + mathint converted = to_mathint(convertToAssets(e, convertToShares(e, amount))); + + // That `converted <= mathamount` was proved in `amountConversionRoundedDown` + assert mathamount - converted <= 1 + rate() / RAY(), "Too few converted assets"; + } + + /** + * @title Inequality for conversion of shares to amount and back + * Note the precision depends on the ratio **`RAY / rate`**. + */ + rule sharesConversionPreserved(uint256 shares) { + env e; + mathint mathshares = to_mathint(shares); + uint256 amount = convertToAssets(e, shares); + mathint converted = to_mathint(convertToShares(e, amount)); + + // That `converted <= mathshare` was proved in `sharesConversionRoundedDown` + assert mathshares - converted <= 1 + RAY() / rate(), "Too few converted shares"; + } + + /** + * @title Joining and splitting shares provides limited advantage + * This rule verifies that joining accounts (by combining shares), and splitting accounts + * (by splitting shares between accounts) provides limited advantage when converting to + * asset amounts. + */ + rule accountsJoiningSplittingIsLimited(uint256 shares1, uint256 shares2) { + env e; + uint256 amount1 = convertToAssets(e, shares1); + uint256 amount2 = convertToAssets(e, shares2); + uint256 jointShares = require_uint256(shares1 + shares2); + //require jointShares >= shares1 + shares2; // Prevent overflow + mathint jointAmount = convertToAssets(e, jointShares); + + assert jointAmount >= amount1 + amount2, "Found advantage in combining accounts"; + + /* Example as to why the following assertion should be true. Suppose conversion of shares + * to assets is division by 2 rounded down, and suppose shares1 = shares2 = 11. + * Then amount1 + amount2 = 5 + 5 = 10, but jointAmount = 22 // 2 = 11. + */ + assert jointAmount < amount1 + amount2 + 2, "Found advantage in splitting accounts"; + + /* The following assertion fails (as expected): + * assert jointAmount < amount1 + amount2 + 1, "Found advantage in splitting accounts"; + */ + } + + /** + * @title Joining and splitting assets provides limited advantage + * Similar to `accountsJoiningSplittingIsLimited` rule. + */ + rule convertSumOfAssetsPreserved(uint256 assets1, uint256 assets2) { + env e; + uint256 shares1 = convertToShares(e, assets1); + uint256 shares2 = convertToShares(e, assets2); + uint256 sumAssets = require_uint256(assets1 + assets2); + //require sumAssets >= assets1 + assets2; // Prevent overflow + mathint jointShares = convertToShares(e, sumAssets); + + assert jointShares >= shares1 + shares2, "Convert sum of assets bigger than parts"; + assert jointShares < shares1 + shares2 + 2, "Convert sum of assets far smaller than parts"; + } + + /// @title Redeeming sum of assets is nearly equal to sum of redeeming + rule redeemSum(uint256 shares1, uint256 shares2) { + env e; + address owner = e.msg.sender; // Handy alias + + uint256 assets1 = redeem(e, shares1, owner, owner); + uint256 assets2 = redeem(e, shares2, owner, owner); + mathint assetsSum = redeem(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"; + } + + /// @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. + * Let w = withdraw(assets), p = previewWithdraw(assets), s = convertToShares(assets), + * then: + * p - 1 <= w <= p -- by previewWithdrawNearlyWithdraw + * s <= p <= s + 1 -- by previewWithdrawRedeemCompliance + * Hence: s - 1 <= w <= s + 1 + * + * Let w1 = withdraw(assets1), s1 = convertToShares(assets1) + * w2 = withdraw(assets2), s2 = convertToShares(assets2) + * w = withdraw(assets1 + assets2), s = convertToShares(assets1 + assets2) + * By convertSumOfAssetsPreserved: + * s1 + s2 <= s <= s1 + s2 + 1 + * Therefore: + * w1 + w2 - 3 <= s1 + s2 - 1 <= s - 1 <= w <= s + 1 <= s1 + s2 + 2 <= w1 + w2 + 4 + * w1 + w2 - 3 <= w <= w1 + w2 + 4 + * + * The following run of withdrawSum timed out: + * https://vaas-stg.certora.com/output/98279/8f5d36ea63ba4a4ca1d23f781ec8dfa6?anonymousKey=11d8393da339881d925ad4e087252951d1da512d + */ + //rule withdrawSum(uint256 assets1, uint256 assets2) { + // env e; + // address owner = e.msg.sender; // Handy alias + // + // // Additional requirement to speed up calculation + // require balanceOf(owner) > convertToShares(2 * (assets1 + assets2)); + // + // uint256 shares1 = withdraw(e, assets1, owner, owner); + // uint256 shares2 = withdraw(e, assets2, owner, owner); + // uint256 sharesSum = withdraw(e, assets1 + assets2, owner, owner); + // + // assert sharesSum <= shares1 + shares2, "Withdraw sum larger than its parts"; + // assert sharesSum + 2 > shares1 + shares2, "Withdraw sum far smaller than it sparts"; + //} + + /* + * Preview functions rules + * ----------------------- + * The rules below prove that preview functions (e.g. `previewDeposit`) return the same + * values as their non-preview counterparts (e.g. `deposit`). + * The rules below passed with rule sanity: job-id=`2b196ea03b8c408dae6c79ae128fc516` + */ + + /***************************** + * previewDeposit * + *****************************/ + + /// Number of shares returned by `previewDeposit` is the same as `deposit`. + rule previewDepositSameAsDeposit(uint256 assets, address receiver) { + env e; + uint256 previewShares = previewDeposit(e, assets); + uint256 shares = deposit(e, assets, receiver); + assert previewShares == shares, "previewDeposit is unequal to deposit"; + } + + /***************************** + * previewMint * + *****************************/ + + /// Number of assets returned by `previewMint` is the same as `mint`. + rule previewMintSameAsMint(uint256 shares, address receiver) { + env e; + uint256 previewAssets = previewMint(shares); + uint256 assets = mint(e, shares, receiver); + 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. + 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 new file mode 100644 index 00000000..16ea48ae --- /dev/null +++ b/certora/stata/specs/erc4626/erc4626MintDepositSummarization.spec @@ -0,0 +1,220 @@ +import "../methods/erc20.spec"; + +using SymbolicLendingPool as _SymbolicLendingPool; +using ATokenInstance as _AToken; + +/////////////////// Methods //////////////////////// + +methods{ + // static aToken + // ------------- + function asset() external returns (address) envfree; + // erc20 + // ----- + function _.transferFrom(address,address,uint256) external => NONDET; + + // pool + function _SymbolicLendingPool.getReserveNormalizedIncome(address) external returns (uint256) envfree; + + // aToken + // ------ + function _AToken.UNDERLYING_ASSET_ADDRESS() external returns (address) envfree; + function RAY() external returns (uint256) envfree; +} + +///////////////// Properties /////////////////////// + + /******************** + * deposit * + *********************/ + + // The deposit function does not always deposit exactly the amount of assets specified by the user during the function call due to rounding error + // The following two rules check that the user gets an non-zero amount of shares if the specified amount of assets to be deposited is at least + // equivalent of 1 AToken. Refer to the erc4626DepositSummarization spec for rules asserting the upper bound of the amount of assets + // deposited in a deposit function call + + // STATUS: Verified + // https://vaas-stg.certora.com/output/11775/b10cd30ab6fb400baeff6b61c07bb375/?anonymousKey=ccba22e832b7549efea9f0d4b1288da2c1377ccb + ///@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 depositCheckIndexGRayAssert2(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 = deposit(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 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(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 = deposit(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 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 * + ******************/ + + /*** + * rule to check the following for the mint function: + * 1. MUST revert if all of shares cannot be minted + */ + // The mint function doesn't always mint exactly the number of shares specified in the function call due to rounding off. + // The following two rules check that the user will at least get as many shares they wanted to mint and upto one extra share + // over the specified amount + // STATUS: Verified + // https://vaas-stg.certora.com/output/11775/b6f6335e770b42ffa280e40d6f82906d/?anonymousKey=ed369d98039f29134aa774592c533ec0c4a9c08e + ///@title mint function check for upper bound of shares minted + ///@notice This rules checks that the mint function, for index > RAY, mints upto 1 extra share over the amount specified by the caller + rule mintCheckIndexGRayUpperBound(env e){ + uint256 shares; + address receiver; + uint256 assets; + require e.msg.sender != currentContract; + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); + + uint256 receiverBalBefore = balanceOf(e, receiver); + require receiverBalBefore + shares <= max_uint256;//avoiding overflow + require index > RAY(); + + assets = mint(e, shares, receiver); + + uint256 receiverBalAfter = balanceOf(e, receiver); + // upperbound + assert to_mathint(receiverBalAfter) <= receiverBalBefore + shares + 1,"receiver should get no more than the 1 extra share"; + } + + // STATUS: Verified + // https://vaas-stg.certora.com/output/11775/d794a47fa37c4c1e9f9fcb45f33ec6c5/?anonymousKey=8a280f8c9ba94d2c0ce98a7240969c02828ad17b + ///@title mint function check for lower bound of shares minted + ///@notice This rules checks that the mint function, for index > RAY, mints atleast the amount of shares specified by the caller + rule mintCheckIndexGRayLowerBound(env e){ + uint256 shares; + address receiver; + uint256 assets; + require e.msg.sender != currentContract; + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); + + uint256 receiverBalBefore = balanceOf(e, receiver); + require receiverBalBefore + shares <= max_uint256;//avoiding overflow + require index > RAY(); + + assets = mint(e, shares, receiver); + + uint256 receiverBalAfter = balanceOf(e, receiver); + // lowerbound + assert to_mathint(receiverBalAfter) >= receiverBalBefore + shares,"receiver should get no less than the amount of shares requested"; + } + + // STATUS: Verified + // https://vaas-stg.certora.com/output/11775/bdf1ff3daa8542ebaac08c1950fdb89e/?anonymousKey=c5b77c1b715310da8f355d2b27bdb4008e70d519 + ///@title mint function check for index == RAY + ///@notice This rule checks that, for index == RAY, the mind function will mint atleast the specifed amount of shares and upto 1 extra share over the specified amount + rule mintCheckIndexEqualsRay(env e){ + uint256 shares; + address receiver; + uint256 assets; + require e.msg.sender != currentContract; + uint256 index = _SymbolicLendingPool.getReserveNormalizedIncome(asset()); + + uint256 receiverBalBefore = balanceOf(e, receiver); + require receiverBalBefore + shares <= max_uint256;//avoiding overflow + require index == RAY(); + + assets = mint(e, shares, receiver); + + uint256 receiverBalAfter = balanceOf(e, receiver); + + assert to_mathint(receiverBalAfter) <= receiverBalBefore + shares + 1,"receiver should get no more than the 1 extra share"; + assert to_mathint(receiverBalAfter) >= receiverBalBefore + shares,"receiver should get no less than the amount of shares requested"; + } 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/erc20.spec b/certora/stata/specs/methods/erc20.spec new file mode 100644 index 00000000..bab7e156 --- /dev/null +++ b/certora/stata/specs/methods/erc20.spec @@ -0,0 +1,12 @@ +// erc20 methods +methods { + function _.name() external => DISPATCHER(true); + function _.symbol() external => DISPATCHER(true); + function _.decimals() external => DISPATCHER(true); + function _.totalSupply() external => DISPATCHER(true); + function _.balanceOf(address) external => DISPATCHER(true); + function _.allowance(address,address) external => DISPATCHER(true); + function _.approve(address,uint256) external => DISPATCHER(true); + function _.transfer(address,uint256) external => DISPATCHER(true); + // transferFrom(address,address,uint256) returns (bool) => DISPATCHER(true) +} diff --git a/certora/stata/specs/methods/methods_base.spec b/certora/stata/specs/methods/methods_base.spec new file mode 100644 index 00000000..d34f85d1 --- /dev/null +++ b/certora/stata/specs/methods/methods_base.spec @@ -0,0 +1,194 @@ +import "erc20.spec"; +import "CVLMath.spec"; + +using StataTokenV2Harness as _StaticATokenLM; +using SymbolicLendingPool as _SymbolicLendingPool; +using RewardsControllerHarness as _RewardsController; +using TransferStrategyHarness as _TransferStrategy; +using DummyERC20_aTokenUnderlying as _DummyERC20_aTokenUnderlying; +using ATokenInstance as _AToken; +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; + + // 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); + + // 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 ////////////////////// + + /// @notice Claim rewards methods + definition claimFunctions(method f) returns bool = + (f.selector == sig:claimRewardsToSelf(address[]).selector || + f.selector == sig:claimRewards(address, address[]).selector || + f.selector == sig:claimRewardsOnBehalf(address, address,address[]).selector); + + definition collectAndUpdateFunction(method f) returns bool = + f.selector == sig:collectAndUpdateRewards(address).selector; + + definition harnessOnlyMethods(method f) returns bool = + (harnessMethodsMinusHarnessClaimMethods(f) || + f.selector == sig:claimSingleRewardOnBehalf(address, address, address).selector || + f.selector == sig:claimDoubleRewardOnBehalfSame(address, address, address).selector); + + definition harnessMethodsMinusHarnessClaimMethods(method f) returns bool = + (f.selector == sig:getRewardTokensLength().selector || + f.selector == sig:getRewardToken(uint256).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 ////////////////////// + + /** + * @title Single reward setup + * Setup the `StaticATokenLM`'s rewards so they contain a single reward token + * which is` _DummyERC20_rewardToken`. + */ + function single_RewardToken_setup() { + require getRewardTokensLength() == 1; + require getRewardToken(0) == _DummyERC20_rewardToken; + } + + /** + * @title Single reward setup in RewardsController + * Sets (in `_RewardsController`) the first reward for `_AToken` as + * `_DummyERC20_rewardToken`. + */ + function rewardsController_reward_setup() { + require _RewardsController.getAvailableRewardsCount(_AToken) > 0; + require _RewardsController.getRewardsByAsset(_AToken, 0) == _DummyERC20_rewardToken; + } + + /// @title Assumptions that should hold in any run + /// @dev Assume that RewardsController.configureAssets(RewardsDataTypes.RewardsConfigInput[] memory rewardsInput) was called + function setup(env e, address user) { + require getRewardTokensLength() > 0; + require _RewardsController.getAvailableRewardsCount(_AToken) > 0; + require _RewardsController.getRewardsByAsset(_AToken, 0) == _DummyERC20_rewardToken; + require currentContract != e.msg.sender; + require currentContract != user; + + require _AToken != user; + require _RewardsController != user; + require _DummyERC20_aTokenUnderlying != user; + require _DummyERC20_rewardToken != user; + require _SymbolicLendingPool != user; + 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 new file mode 100644 index 00000000..3c305115 --- /dev/null +++ b/certora/stata/specs/methods/methods_multi_reward.spec @@ -0,0 +1,75 @@ +import "erc20.spec"; + +using SymbolicLendingPool as _SymbolicLendingPool; +using RewardsControllerHarness as _RewardsController; +using DummyERC20_aTokenUnderlying as _DummyERC20_aTokenUnderlying; +using ATokenInstance as _AToken; +using DummyERC20_rewardToken as _DummyERC20_rewardToken; + +/////////////////// Methods //////////////////////// + + /// @dev Using mostly `NONDET` in the methods block, to speed up verification. + + methods { + // static aToken + // ------------- + function _.getCurrentRewardsIndex(address reward) external => CONSTANT; + 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; + + // pool + // ---- + // In RewardsDistributor.sol called by RewardsController.sol + function _.getAssetIndex(address, address) external => NONDET; + + // In RewardsDistributor.sol called by RewardsController.sol + function _.finalizeTransfer(address, address, address, uint256, uint256, uint256) external => NONDET; + + // In ScaledBalanceTokenBase.sol called by getAssetIndex + 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 + function _.handleAction(address,uint256,uint256) external => NONDET; + // Called by rewardscontroller.sol + // Defined in scaledbalancetokenbase.sol + function _.getScaledUserBalanceAndSupply(address) external => NONDET; + // Called by RewardsController._transferRewards() + // Defined in TransferStrategyHarness as simple transfer() + function _.performTransfer(address,address,uint256) external => NONDET; + + // aToken + // ------ + 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; + + // reward token + // ------------ + function _DummyERC20_rewardToken.balanceOf(address) external returns (uint256) envfree; + + function _.permit(address,address,uint256,uint256,uint8,bytes32,bytes32) external => NONDET; + } + +///////////////// FUNCTIONS /////////////////////// + + /// @title Set up a single reward token + function single_RewardToken_setup() { + require isRegisteredRewardToken(_DummyERC20_rewardToken); + require getRewardTokensLength() == 1; + } + + /// @title Set up a single reward token for `_AToken` in the `INCENTIVES_CONTROLLER` + function rewardsController_arbitrary_single_reward_setup() { + require _RewardsController.getAvailableRewardsCount(_AToken) == 1; + require _RewardsController.getRewardsByAsset(_AToken, 0) == _DummyERC20_rewardToken; + }