├── .env.example ├── .github └── workflows │ ├── comment.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── .prettierignore ├── .prettierrc ├── LICENSE ├── Makefile ├── README.md ├── audits ├── Certora-Review-StatAToken-Oracle.pdf └── Formal_Verification_Report_staticAToken.pdf ├── foundry.toml ├── package.json ├── remappings.txt ├── scripts ├── Deploy.s.sol └── DeployUpgrade.s.sol ├── src ├── ECDSA.sol ├── ERC20.sol ├── RayMathExplicitRounding.sol ├── StataOracle.sol ├── StaticATokenErrors.sol ├── StaticATokenFactory.sol ├── StaticATokenLM.sol ├── UpgradePayload.sol └── interfaces │ ├── IAToken.sol │ ├── IERC4626.sol │ ├── IInitializableStaticATokenLM.sol │ ├── IStataOracle.sol │ ├── IStaticATokenFactory.sol │ └── IStaticATokenLM.sol ├── tests ├── SigUtils.sol ├── StataOracle.t.sol ├── StaticATokenLM.t.sol ├── StaticATokenMetaTransactions.sol ├── StaticATokenNoLM.t.sol ├── TestBase.sol └── Upgrade.t.sol ├── wrapping.jpg └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | # Deployment via ledger 2 | MNEMONIC_INDEX= 3 | LEDGER_SENDER= 4 | 5 | # Deployment via private key 6 | PRIVATE_KEY= 7 | 8 | # Test rpc_endpoints 9 | RPC_MAINNET=https://rpc.flashbots.net 10 | RPC_AVALANCHE=https://api.avax.network/ext/bc/C/rpc 11 | RPC_OPTIMISM=https://mainnet.optimism.io 12 | RPC_POLYGON=https://polygon-rpc.com 13 | RPC_ARBITRUM=https://arb1.arbitrum.io/rpc 14 | RPC_FANTOM=https://rpc.ftm.tools 15 | RPC_HARMONY=https://api.harmony.one 16 | RPC_METIS=https://lb.nodies.app/v1/f5c5ecde09414b3384842a8740a8c998 17 | 18 | 19 | # Etherscan api keys for verification & download utils 20 | ETHERSCAN_API_KEY_MAINNET= 21 | ETHERSCAN_API_KEY_POLYGON= 22 | ETHERSCAN_API_KEY_AVALANCHE= 23 | ETHERSCAN_API_KEY_FANTOM= 24 | ETHERSCAN_API_KEY_OPTIMISM= 25 | ETHERSCAN_API_KEY_ARBITRUM= -------------------------------------------------------------------------------- /.github/workflows/comment.yml: -------------------------------------------------------------------------------- 1 | name: PR Comment 2 | 3 | on: 4 | workflow_run: 5 | workflows: [Test] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | test: 11 | uses: bgd-labs/github-workflows/.github/workflows/comment.yml@main 12 | secrets: inherit 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Main workflow 2 | 3 | concurrency: 4 | group: ${{ github.head_ref || github.run_id }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | pull_request: 9 | push: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | test: 15 | uses: bgd-labs/github-workflows/.github/workflows/foundry-test.yml@main 16 | secrets: inherit 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build and cache 2 | cache/ 3 | out/ 4 | 5 | # general 6 | .env 7 | 8 | # editors 9 | .idea 10 | .vscode 11 | 12 | # well, looks strange to ignore package-lock, but we have only pretter and it's temproray 13 | package-lock.json 14 | node_modules 15 | 16 | broadcast -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/aave-helpers"] 5 | path = lib/aave-helpers 6 | url = https://github.com/bgd-labs/aave-helpers 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | lib 3 | cache 4 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": "*.sol", 5 | "options": { 6 | "printWidth": 100, 7 | "tabWidth": 2, 8 | "useTabs": false, 9 | "singleQuote": true, 10 | "bracketSpacing": false 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 BGD Labs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # include .env file and export its env vars 2 | # (-include to ignore error if it does not exist) 3 | -include .env 4 | 5 | # deps 6 | update:; forge update 7 | 8 | # Build & test 9 | build :; forge build --sizes 10 | 11 | test :; forge test -vvv 12 | 13 | # Deploy 14 | deploy-ledger :; forge script ${contract} --rpc-url ${chain} $(if ${dry},--sender 0x25F2226B597E8F9514B3F68F00f494cF4f286491 -vvvv,--broadcast --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv --slow) 15 | deploy-pk :; forge script ${contract} --rpc-url ${chain} $(if ${dry},--sender 0x25F2226B597E8F9514B3F68F00f494cF4f286491 -vvvv,--broadcast --private-key ${PRIVATE_KEY} --verify -vvvv --slow) 16 | 17 | # Utilities 18 | download :; cast etherscan-source --chain ${chain} -d src/etherscan/${chain}_${address} ${address} 19 | git-diff : 20 | @mkdir -p diffs 21 | @printf '%s\n%s\n%s\n' "\`\`\`diff" "$$(git diff --no-index --diff-algorithm=patience --ignore-space-at-eol ${before} ${after})" "\`\`\`" > diffs/${out}.md 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stataToken - Static aToken vault/wrapper 2 | 3 | Project has been moved to [Aave V3 Origin](https://github.com/aave-dao/aave-v3-origin/tree/main/src/periphery/contracts/static-a-token); 4 | 5 | ## Disclaimer 6 | 7 |

8 | 9 |

10 | 11 | ## About 12 | 13 | This repository contains an [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626) generic token vault/wrapper for all [Aave v3](https://github.com/aave/aave-v3-core) pools. 14 | 15 | ## Features 16 | 17 | - **Full [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626) compatibility.** 18 | - **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. 19 | 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). 20 | - **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. 21 | - **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`. 22 | - **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. 23 | 24 | See [IStaticATokenLM.sol](./src/interfaces/IStaticATokenLM.sol) for detailed method documentation. 25 | 26 | ## Deployed Addresses 27 | 28 | The staticATokenFactory is deployed for all major Aave v3 pools. 29 | 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#L67). 30 | 31 | ## Limitations 32 | 33 | The `stataToken` is not natively integrated into the aave protocol and therefore cannot hook into the emissionManager. 34 | This means a `reward` added **after** `statToken` creation needs to be registered manually on the token via the permissionless `refreshRewardTokens()` method. 35 | As this process is not currently automated users might be missing out on rewards until the method is called. 36 | 37 | ## Security procedures 38 | 39 | For this project, the security procedures applied/being finished are: 40 | 41 | - The test suite of the codebase itself. 42 | - Certora [audit/property checking](./audits/Formal_Verification_Report_staticAToken.pdf) for all the dynamics of the `stataToken`, including respecting all the specs of [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626). 43 | - Certora [manual review of static aToken oracle](./audits/Certora-Review-StatAToken-Oracle.pdf) 44 | 45 | ## Development 46 | 47 | This project uses [Foundry](https://getfoundry.sh). See the [book](https://book.getfoundry.sh/getting-started/installation.html) for detailed instructions on how to install and use Foundry. 48 | The template ships with sensible default so you can use default `foundry` commands without resorting to `MakeFile`. 49 | 50 | ### Setup 51 | 52 | ```sh 53 | cp .env.example .env 54 | forge install 55 | ``` 56 | 57 | ### Test 58 | 59 | ```sh 60 | forge test 61 | ``` 62 | -------------------------------------------------------------------------------- /audits/Certora-Review-StatAToken-Oracle.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgd-labs/static-a-token-v3/101f5d977889254ca2d2711b9582b45f832d10a0/audits/Certora-Review-StatAToken-Oracle.pdf -------------------------------------------------------------------------------- /audits/Formal_Verification_Report_staticAToken.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgd-labs/static-a-token-v3/101f5d977889254ca2d2711b9582b45f832d10a0/audits/Formal_Verification_Report_staticAToken.pdf -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | test = 'tests' 4 | script = 'scripts' 5 | out = 'out' 6 | libs = ['lib'] 7 | remappings = [ 8 | ] 9 | fs_permissions = [{access = "write", path = "./reports"}] 10 | 11 | [rpc_endpoints] 12 | mainnet = "${RPC_MAINNET}" 13 | optimism = "${RPC_OPTIMISM}" 14 | avalanche = "${RPC_AVALANCHE}" 15 | polygon = "${RPC_POLYGON}" 16 | arbitrum = "${RPC_ARBITRUM}" 17 | fantom = "${RPC_FANTOM}" 18 | harmony = "${RPC_HARMONY}" 19 | metis = "${RPC_METIS}" 20 | base = "${RPC_BASE}" 21 | zkevm = "${RPC_ZKEVM}" 22 | gnosis = "${RPC_GNOSIS}" 23 | bnb = "${RPC_BNB}" 24 | scroll="${RPC_SCROLL}" 25 | 26 | [etherscan] 27 | mainnet = { key="${ETHERSCAN_API_KEY_MAINNET}", chain=1 } 28 | optimism = { key="${ETHERSCAN_API_KEY_OPTIMISM}", chain=10 } 29 | avalanche = { key="${ETHERSCAN_API_KEY_AVALANCHE}", chain=43114 } 30 | polygon = { key="${ETHERSCAN_API_KEY_POLYGON}", chain=137 } 31 | arbitrum = { key="${ETHERSCAN_API_KEY_ARBITRUM}", chain=42161 } 32 | fantom = { key="${ETHERSCAN_API_KEY_FANTOM}", chain=250 } 33 | metis = { key="any", chainId=1088, url='https://andromeda-explorer.metis.io/' } 34 | base = { key="${ETHERSCAN_API_KEY_BASE}", chainId=8453 } 35 | zkevm = { key="${ETHERSCAN_API_KEY_ZKEVM}", chainId=1101 } 36 | gnosis = { key="${ETHERSCAN_API_KEY_GNOSIS}", chainId=100 } 37 | bnb= { key="${ETHERSCAN_API_KEY_BNB}",chainId=56,url='https://api.bscscan.com/api'} 38 | scroll = {key="${ETHERSCAN_API_KEY_SCROLL}", chainId=534352} 39 | 40 | # See more config options https://github.com/gakonst/foundry/tree/master/config -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "static-a-token-v3", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "lib": "lib", 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "lint": "prettier . --write" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/bgd-labs/static-a-token-v3.git" 16 | }, 17 | "author": "", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/bgd-labs/static-a-token-v3/issues" 21 | }, 22 | "homepage": "https://github.com/bgd-labs/static-a-token-v3#readme", 23 | "devDependencies": { 24 | "prettier": "^2.8.3", 25 | "prettier-plugin-solidity": "^1.1.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | openzeppelin-contracts/=lib/openzeppelin-contracts/ 2 | ds-test/=lib/aave-helpers/lib/forge-std/lib/ds-test/src/ 3 | forge-std/=lib/aave-helpers/lib/forge-std/src/ 4 | solidity-utils/=lib/aave-helpers/lib/solidity-utils/src/ 5 | aave-address-book/=lib/aave-helpers/lib/aave-address-book/src/ 6 | aave-helpers/=lib/aave-helpers/src/ 7 | @aave/core-v3/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-core/ 8 | aave-v3-core/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-core/ 9 | aave-v3-periphery/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-periphery/ 10 | @aave/periphery-v3/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-periphery/ 11 | -------------------------------------------------------------------------------- /scripts/Deploy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import 'forge-std/Test.sol'; 5 | import {EthereumScript, PolygonScript, AvalancheScript, ArbitrumScript, OptimismScript, MetisScript, BaseScript, BNBScript, ScrollScript} from 'aave-helpers/ScriptUtils.sol'; 6 | import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; 7 | import {MiscPolygon} from 'aave-address-book/MiscPolygon.sol'; 8 | import {MiscAvalanche} from 'aave-address-book/MiscAvalanche.sol'; 9 | import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; 10 | import {MiscOptimism} from 'aave-address-book/MiscOptimism.sol'; 11 | import {MiscMetis} from 'aave-address-book/MiscMetis.sol'; 12 | import {MiscBase} from 'aave-address-book/MiscBase.sol'; 13 | import {MiscBNB} from 'aave-address-book/MiscBNB.sol'; 14 | import {MiscScroll} from 'aave-address-book/MiscScroll.sol'; 15 | import {AaveV3Ethereum, IPool} from 'aave-address-book/AaveV3Ethereum.sol'; 16 | import {AaveV3Polygon} from 'aave-address-book/AaveV3Polygon.sol'; 17 | import {AaveV3Avalanche} from 'aave-address-book/AaveV3Avalanche.sol'; 18 | import {AaveV3Optimism} from 'aave-address-book/AaveV3Optimism.sol'; 19 | import {AaveV3Arbitrum} from 'aave-address-book/AaveV3Arbitrum.sol'; 20 | import {AaveV3Metis} from 'aave-address-book/AaveV3Metis.sol'; 21 | import {AaveV3Base} from 'aave-address-book/AaveV3Base.sol'; 22 | import {AaveV3BNB} from 'aave-address-book/AaveV3BNB.sol'; 23 | import {AaveV3Scroll} from 'aave-address-book/AaveV3Scroll.sol'; 24 | import {ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/interfaces/ITransparentProxyFactory.sol'; 25 | import {StaticATokenFactory} from '../src/StaticATokenFactory.sol'; 26 | import {StaticATokenLM} from '../src/StaticATokenLM.sol'; 27 | import {IRewardsController} from 'aave-v3-periphery/contracts/rewards/interfaces/IRewardsController.sol'; 28 | 29 | library DeployATokenFactory { 30 | function _deploy( 31 | ITransparentProxyFactory proxyFactory, 32 | address sharedProxyAdmin, 33 | IPool pool, 34 | IRewardsController rewardsController 35 | ) internal returns (StaticATokenFactory) { 36 | // deploy and initialize static token impl 37 | StaticATokenLM staticImpl = new StaticATokenLM(pool, rewardsController); 38 | 39 | // deploy staticATokenFactory impl 40 | StaticATokenFactory factoryImpl = new StaticATokenFactory( 41 | pool, 42 | sharedProxyAdmin, 43 | proxyFactory, 44 | address(staticImpl) 45 | ); 46 | 47 | // deploy factory proxy 48 | StaticATokenFactory factory = StaticATokenFactory( 49 | proxyFactory.create( 50 | address(factoryImpl), 51 | sharedProxyAdmin, 52 | abi.encodeWithSelector(StaticATokenFactory.initialize.selector) 53 | ) 54 | ); 55 | factory.createStaticATokens(pool.getReservesList()); 56 | return factory; 57 | } 58 | } 59 | 60 | contract DeployMainnet is EthereumScript { 61 | function run() external broadcast { 62 | DeployATokenFactory._deploy( 63 | ITransparentProxyFactory(MiscEthereum.TRANSPARENT_PROXY_FACTORY), 64 | MiscEthereum.PROXY_ADMIN, 65 | AaveV3Ethereum.POOL, 66 | IRewardsController(AaveV3Ethereum.DEFAULT_INCENTIVES_CONTROLLER) 67 | ); 68 | } 69 | } 70 | 71 | contract DeployPolygon is PolygonScript { 72 | function run() external broadcast { 73 | DeployATokenFactory._deploy( 74 | ITransparentProxyFactory(MiscPolygon.TRANSPARENT_PROXY_FACTORY), 75 | MiscPolygon.PROXY_ADMIN, 76 | AaveV3Polygon.POOL, 77 | IRewardsController(AaveV3Polygon.DEFAULT_INCENTIVES_CONTROLLER) 78 | ); 79 | } 80 | } 81 | 82 | contract DeployAvalanche is AvalancheScript { 83 | function run() external broadcast { 84 | DeployATokenFactory._deploy( 85 | ITransparentProxyFactory(MiscAvalanche.TRANSPARENT_PROXY_FACTORY), 86 | MiscAvalanche.PROXY_ADMIN, 87 | AaveV3Avalanche.POOL, 88 | IRewardsController(AaveV3Avalanche.DEFAULT_INCENTIVES_CONTROLLER) 89 | ); 90 | } 91 | } 92 | 93 | contract DeployOptimism is OptimismScript { 94 | function run() external broadcast { 95 | DeployATokenFactory._deploy( 96 | ITransparentProxyFactory(MiscOptimism.TRANSPARENT_PROXY_FACTORY), 97 | MiscOptimism.PROXY_ADMIN, 98 | AaveV3Optimism.POOL, 99 | IRewardsController(AaveV3Optimism.DEFAULT_INCENTIVES_CONTROLLER) 100 | ); 101 | } 102 | } 103 | 104 | contract DeployArbitrum is ArbitrumScript { 105 | function run() external broadcast { 106 | DeployATokenFactory._deploy( 107 | ITransparentProxyFactory(MiscArbitrum.TRANSPARENT_PROXY_FACTORY), 108 | MiscArbitrum.PROXY_ADMIN, 109 | AaveV3Arbitrum.POOL, 110 | IRewardsController(AaveV3Arbitrum.DEFAULT_INCENTIVES_CONTROLLER) 111 | ); 112 | } 113 | } 114 | 115 | contract DeployMetis is MetisScript { 116 | function run() external broadcast { 117 | DeployATokenFactory._deploy( 118 | ITransparentProxyFactory(MiscMetis.TRANSPARENT_PROXY_FACTORY), 119 | MiscMetis.PROXY_ADMIN, 120 | AaveV3Metis.POOL, 121 | IRewardsController(AaveV3Metis.DEFAULT_INCENTIVES_CONTROLLER) 122 | ); 123 | } 124 | } 125 | 126 | contract DeployBase is BaseScript { 127 | function run() external broadcast { 128 | DeployATokenFactory._deploy( 129 | ITransparentProxyFactory(MiscBase.TRANSPARENT_PROXY_FACTORY), 130 | MiscBase.PROXY_ADMIN, 131 | AaveV3Base.POOL, 132 | IRewardsController(AaveV3Base.DEFAULT_INCENTIVES_CONTROLLER) 133 | ); 134 | } 135 | } 136 | 137 | /** 138 | * make deploy-ledger contract=scripts/Deploy.s.sol:DeployBNB chain=bnb 139 | */ 140 | contract DeployBNB is BNBScript { 141 | function run() external broadcast { 142 | DeployATokenFactory._deploy( 143 | ITransparentProxyFactory(MiscBNB.TRANSPARENT_PROXY_FACTORY), 144 | MiscBNB.PROXY_ADMIN, 145 | AaveV3BNB.POOL, 146 | IRewardsController(AaveV3BNB.DEFAULT_INCENTIVES_CONTROLLER) 147 | ); 148 | } 149 | } 150 | 151 | /** 152 | * make deploy-ledger contract=scripts/Deploy.s.sol:DeployScroll chain=scroll 153 | */ 154 | contract DeployScroll is ScrollScript { 155 | function run() external broadcast { 156 | DeployATokenFactory._deploy( 157 | ITransparentProxyFactory(MiscScroll.TRANSPARENT_PROXY_FACTORY), 158 | MiscScroll.PROXY_ADMIN, 159 | AaveV3Scroll.POOL, 160 | IRewardsController(AaveV3Scroll.DEFAULT_INCENTIVES_CONTROLLER) 161 | ); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /scripts/DeployUpgrade.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import 'forge-std/Test.sol'; 5 | import {EthereumScript, PolygonScript, AvalancheScript, ArbitrumScript, OptimismScript, MetisScript, BaseScript, BNBScript, ScrollScript, BaseScript, GnosisScript} from 'aave-helpers/ScriptUtils.sol'; 6 | import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; 7 | import {MiscPolygon} from 'aave-address-book/MiscPolygon.sol'; 8 | import {MiscAvalanche} from 'aave-address-book/MiscAvalanche.sol'; 9 | import {MiscArbitrum} from 'aave-address-book/MiscArbitrum.sol'; 10 | import {MiscOptimism} from 'aave-address-book/MiscOptimism.sol'; 11 | import {MiscMetis} from 'aave-address-book/MiscMetis.sol'; 12 | import {MiscBNB} from 'aave-address-book/MiscBNB.sol'; 13 | import {MiscScroll} from 'aave-address-book/MiscScroll.sol'; 14 | import {MiscGnosis} from 'aave-address-book/MiscGnosis.sol'; 15 | import {MiscBase} from 'aave-address-book/MiscBase.sol'; 16 | import {AaveV3Ethereum, IPool} from 'aave-address-book/AaveV3Ethereum.sol'; 17 | import {AaveV3Polygon} from 'aave-address-book/AaveV3Polygon.sol'; 18 | import {AaveV3Avalanche} from 'aave-address-book/AaveV3Avalanche.sol'; 19 | import {AaveV3Optimism} from 'aave-address-book/AaveV3Optimism.sol'; 20 | import {AaveV3Arbitrum} from 'aave-address-book/AaveV3Arbitrum.sol'; 21 | import {AaveV3BNB} from 'aave-address-book/AaveV3BNB.sol'; 22 | import {AaveV3Scroll} from 'aave-address-book/AaveV3Scroll.sol'; 23 | import {AaveV3Metis} from 'aave-address-book/AaveV3Metis.sol'; 24 | import {AaveV3Gnosis} from 'aave-address-book/AaveV3Gnosis.sol'; 25 | import {AaveV3Base} from 'aave-address-book/AaveV3Base.sol'; 26 | import {ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/interfaces/ITransparentProxyFactory.sol'; 27 | import {StaticATokenFactory} from '../src/StaticATokenFactory.sol'; 28 | import {StaticATokenLM} from '../src/StaticATokenLM.sol'; 29 | import {UpgradePayload} from '../src/UpgradePayload.sol'; 30 | import {IRewardsController} from 'aave-v3-periphery/contracts/rewards/interfaces/IRewardsController.sol'; 31 | 32 | library DeployUpgrade { 33 | function _deploy( 34 | ITransparentProxyFactory proxyFactory, 35 | address sharedProxyAdmin, 36 | IPool pool, 37 | IRewardsController rewardsController, 38 | address staticATokenFactory 39 | ) internal returns (UpgradePayload) { 40 | // deploy and initialize static token impl 41 | StaticATokenLM staticImpl = new StaticATokenLM(pool, rewardsController); 42 | 43 | // deploy staticATokenFactory impl 44 | StaticATokenFactory factoryImpl = new StaticATokenFactory( 45 | pool, 46 | sharedProxyAdmin, 47 | proxyFactory, 48 | address(staticImpl) 49 | ); 50 | 51 | return 52 | new UpgradePayload( 53 | sharedProxyAdmin, 54 | StaticATokenFactory(staticATokenFactory), 55 | factoryImpl, 56 | address(staticImpl) 57 | ); 58 | } 59 | 60 | function deployMainnet() internal returns (UpgradePayload) { 61 | return 62 | _deploy( 63 | ITransparentProxyFactory(MiscEthereum.TRANSPARENT_PROXY_FACTORY), 64 | MiscEthereum.PROXY_ADMIN, 65 | AaveV3Ethereum.POOL, 66 | IRewardsController(AaveV3Ethereum.DEFAULT_INCENTIVES_CONTROLLER), 67 | AaveV3Ethereum.STATIC_A_TOKEN_FACTORY 68 | ); 69 | } 70 | 71 | function deployPolygon() internal returns (UpgradePayload) { 72 | return 73 | _deploy( 74 | ITransparentProxyFactory(MiscPolygon.TRANSPARENT_PROXY_FACTORY), 75 | MiscPolygon.PROXY_ADMIN, 76 | AaveV3Polygon.POOL, 77 | IRewardsController(AaveV3Polygon.DEFAULT_INCENTIVES_CONTROLLER), 78 | AaveV3Polygon.STATIC_A_TOKEN_FACTORY 79 | ); 80 | } 81 | 82 | function deployAvalanche() internal returns (UpgradePayload) { 83 | return 84 | _deploy( 85 | ITransparentProxyFactory(MiscAvalanche.TRANSPARENT_PROXY_FACTORY), 86 | MiscAvalanche.PROXY_ADMIN, 87 | AaveV3Avalanche.POOL, 88 | IRewardsController(AaveV3Avalanche.DEFAULT_INCENTIVES_CONTROLLER), 89 | AaveV3Avalanche.STATIC_A_TOKEN_FACTORY 90 | ); 91 | } 92 | 93 | function deployOptimism() internal returns (UpgradePayload) { 94 | return 95 | _deploy( 96 | ITransparentProxyFactory(MiscOptimism.TRANSPARENT_PROXY_FACTORY), 97 | MiscOptimism.PROXY_ADMIN, 98 | AaveV3Optimism.POOL, 99 | IRewardsController(AaveV3Optimism.DEFAULT_INCENTIVES_CONTROLLER), 100 | AaveV3Optimism.STATIC_A_TOKEN_FACTORY 101 | ); 102 | } 103 | 104 | function deployArbitrum() internal returns (UpgradePayload) { 105 | return 106 | _deploy( 107 | ITransparentProxyFactory(MiscArbitrum.TRANSPARENT_PROXY_FACTORY), 108 | MiscArbitrum.PROXY_ADMIN, 109 | AaveV3Arbitrum.POOL, 110 | IRewardsController(AaveV3Arbitrum.DEFAULT_INCENTIVES_CONTROLLER), 111 | AaveV3Arbitrum.STATIC_A_TOKEN_FACTORY 112 | ); 113 | } 114 | 115 | function deployMetis() internal returns (UpgradePayload) { 116 | return 117 | _deploy( 118 | ITransparentProxyFactory(MiscMetis.TRANSPARENT_PROXY_FACTORY), 119 | MiscMetis.PROXY_ADMIN, 120 | AaveV3Metis.POOL, 121 | IRewardsController(AaveV3Metis.DEFAULT_INCENTIVES_CONTROLLER), 122 | AaveV3Metis.STATIC_A_TOKEN_FACTORY 123 | ); 124 | } 125 | 126 | function deployBNB() internal returns (UpgradePayload) { 127 | return 128 | _deploy( 129 | ITransparentProxyFactory(MiscBNB.TRANSPARENT_PROXY_FACTORY), 130 | MiscBNB.PROXY_ADMIN, 131 | AaveV3BNB.POOL, 132 | IRewardsController(AaveV3BNB.DEFAULT_INCENTIVES_CONTROLLER), 133 | AaveV3BNB.STATIC_A_TOKEN_FACTORY 134 | ); 135 | } 136 | 137 | function deployScroll() internal returns (UpgradePayload) { 138 | return 139 | _deploy( 140 | ITransparentProxyFactory(MiscScroll.TRANSPARENT_PROXY_FACTORY), 141 | MiscScroll.PROXY_ADMIN, 142 | AaveV3Scroll.POOL, 143 | IRewardsController(AaveV3Scroll.DEFAULT_INCENTIVES_CONTROLLER), 144 | AaveV3Scroll.STATIC_A_TOKEN_FACTORY 145 | ); 146 | } 147 | 148 | function deployBase() internal returns (UpgradePayload) { 149 | return 150 | _deploy( 151 | ITransparentProxyFactory(MiscBase.TRANSPARENT_PROXY_FACTORY), 152 | MiscBase.PROXY_ADMIN, 153 | AaveV3Base.POOL, 154 | IRewardsController(AaveV3Base.DEFAULT_INCENTIVES_CONTROLLER), 155 | AaveV3Base.STATIC_A_TOKEN_FACTORY 156 | ); 157 | } 158 | 159 | function deployGnosis() internal returns (UpgradePayload) { 160 | return 161 | _deploy( 162 | ITransparentProxyFactory(MiscGnosis.TRANSPARENT_PROXY_FACTORY), 163 | MiscGnosis.PROXY_ADMIN, 164 | AaveV3Gnosis.POOL, 165 | IRewardsController(AaveV3Gnosis.DEFAULT_INCENTIVES_CONTROLLER), 166 | AaveV3Gnosis.STATIC_A_TOKEN_FACTORY 167 | ); 168 | } 169 | } 170 | 171 | // make deploy-ledger contract=scripts/DeployUpgrade.s.sol:DeployMainnet chain=mainnet 172 | contract DeployMainnet is EthereumScript { 173 | function run() external broadcast { 174 | DeployUpgrade.deployMainnet(); 175 | } 176 | } 177 | 178 | // make deploy-ledger contract=scripts/DeployUpgrade.s.sol:DeployPolygon chain=polygon 179 | contract DeployPolygon is PolygonScript { 180 | function run() external broadcast { 181 | DeployUpgrade.deployPolygon(); 182 | } 183 | } 184 | 185 | // make deploy-ledger contract=scripts/DeployUpgrade.s.sol:DeployAvalanche chain=avalanche 186 | contract DeployAvalanche is AvalancheScript { 187 | function run() external broadcast { 188 | DeployUpgrade.deployAvalanche(); 189 | } 190 | } 191 | 192 | // make deploy-ledger contract=scripts/DeployUpgrade.s.sol:DeployOptimism chain=optimism 193 | contract DeployOptimism is OptimismScript { 194 | function run() external broadcast { 195 | DeployUpgrade.deployOptimism(); 196 | } 197 | } 198 | 199 | // make deploy-ledger contract=scripts/DeployUpgrade.s.sol:DeployArbitrum chain=arbitrum 200 | contract DeployArbitrum is ArbitrumScript { 201 | function run() external broadcast { 202 | DeployUpgrade.deployArbitrum(); 203 | } 204 | } 205 | 206 | // make deploy-ledger contract=scripts/DeployUpgrade.s.sol:DeployMetis chain=metis 207 | contract DeployMetis is MetisScript { 208 | function run() external broadcast { 209 | DeployUpgrade.deployMetis(); 210 | } 211 | } 212 | 213 | // make deploy-ledger contract=scripts/DeployUpgrade.s.sol:DeployBNB chain=bnb 214 | contract DeployBNB is BNBScript { 215 | function run() external broadcast { 216 | DeployUpgrade.deployBNB(); 217 | } 218 | } 219 | 220 | // make deploy-ledger contract=scripts/DeployUpgrade.s.sol:DeployScroll chain=scroll 221 | contract DeployScroll is ScrollScript { 222 | function run() external broadcast { 223 | DeployUpgrade.deployScroll(); 224 | } 225 | } 226 | 227 | // make deploy-ledger contract=scripts/DeployUpgrade.s.sol:DeployBase chain=base 228 | contract DeployBase is BaseScript { 229 | function run() external broadcast { 230 | DeployUpgrade.deployBase(); 231 | } 232 | } 233 | 234 | // make deploy-ledger contract=scripts/DeployUpgrade.s.sol:DeployGnosis chain=gnosis 235 | contract DeployGnosis is GnosisScript { 236 | function run() external broadcast { 237 | DeployUpgrade.deployGnosis(); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/ECDSA.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/ECDSA.sol) 3 | 4 | pragma solidity ^0.8.20; 5 | 6 | /** 7 | * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. 8 | * 9 | * These functions can be used to verify that a message was signed by the holder 10 | * of the private keys of a given address. 11 | */ 12 | library ECDSA { 13 | enum RecoverError { 14 | NoError, 15 | InvalidSignature, 16 | InvalidSignatureLength, 17 | InvalidSignatureS 18 | } 19 | 20 | /** 21 | * @dev The signature derives the `address(0)`. 22 | */ 23 | error ECDSAInvalidSignature(); 24 | 25 | /** 26 | * @dev The signature has an invalid length. 27 | */ 28 | error ECDSAInvalidSignatureLength(uint256 length); 29 | 30 | /** 31 | * @dev The signature has an S value that is in the upper half order. 32 | */ 33 | error ECDSAInvalidSignatureS(bytes32 s); 34 | 35 | /** 36 | * @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not 37 | * return address(0) without also returning an error description. Errors are documented using an enum (error type) 38 | * and a bytes32 providing additional information about the error. 39 | * 40 | * If no error is returned, then the address can be used for verification purposes. 41 | * 42 | * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: 43 | * this function rejects them by requiring the `s` value to be in the lower 44 | * half order, and the `v` value to be either 27 or 28. 45 | * 46 | * IMPORTANT: `hash` _must_ be the result of a hash operation for the 47 | * verification to be secure: it is possible to craft signatures that 48 | * recover to arbitrary addresses for non-hashed data. A safe way to ensure 49 | * this is by receiving a hash of the original message (which may otherwise 50 | * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. 51 | * 52 | * Documentation for signature generation: 53 | * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] 54 | * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] 55 | */ 56 | function tryRecover( 57 | bytes32 hash, 58 | bytes memory signature 59 | ) internal pure returns (address, RecoverError, bytes32) { 60 | if (signature.length == 65) { 61 | bytes32 r; 62 | bytes32 s; 63 | uint8 v; 64 | // ecrecover takes the signature parameters, and the only way to get them 65 | // currently is to use assembly. 66 | /// @solidity memory-safe-assembly 67 | assembly { 68 | r := mload(add(signature, 0x20)) 69 | s := mload(add(signature, 0x40)) 70 | v := byte(0, mload(add(signature, 0x60))) 71 | } 72 | return tryRecover(hash, v, r, s); 73 | } else { 74 | return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length)); 75 | } 76 | } 77 | 78 | /** 79 | * @dev Returns the address that signed a hashed message (`hash`) with 80 | * `signature`. This address can then be used for verification purposes. 81 | * 82 | * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: 83 | * this function rejects them by requiring the `s` value to be in the lower 84 | * half order, and the `v` value to be either 27 or 28. 85 | * 86 | * IMPORTANT: `hash` _must_ be the result of a hash operation for the 87 | * verification to be secure: it is possible to craft signatures that 88 | * recover to arbitrary addresses for non-hashed data. A safe way to ensure 89 | * this is by receiving a hash of the original message (which may otherwise 90 | * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. 91 | */ 92 | function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { 93 | (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature); 94 | _throwError(error, errorArg); 95 | return recovered; 96 | } 97 | 98 | /** 99 | * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately. 100 | * 101 | * See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures] 102 | */ 103 | function tryRecover( 104 | bytes32 hash, 105 | bytes32 r, 106 | bytes32 vs 107 | ) internal pure returns (address, RecoverError, bytes32) { 108 | unchecked { 109 | bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); 110 | // We do not check for an overflow here since the shift operation results in 0 or 1. 111 | uint8 v = uint8((uint256(vs) >> 255) + 27); 112 | return tryRecover(hash, v, r, s); 113 | } 114 | } 115 | 116 | /** 117 | * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately. 118 | */ 119 | function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { 120 | (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs); 121 | _throwError(error, errorArg); 122 | return recovered; 123 | } 124 | 125 | /** 126 | * @dev Overload of {ECDSA-tryRecover} that receives the `v`, 127 | * `r` and `s` signature fields separately. 128 | */ 129 | function tryRecover( 130 | bytes32 hash, 131 | uint8 v, 132 | bytes32 r, 133 | bytes32 s 134 | ) internal pure returns (address, RecoverError, bytes32) { 135 | // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature 136 | // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines 137 | // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most 138 | // signatures from current libraries generate a unique signature with an s-value in the lower half order. 139 | // 140 | // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value 141 | // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or 142 | // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept 143 | // these malleable signatures as well. 144 | if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { 145 | return (address(0), RecoverError.InvalidSignatureS, s); 146 | } 147 | 148 | // If the signature is valid (and not malleable), return the signer address 149 | address signer = ecrecover(hash, v, r, s); 150 | if (signer == address(0)) { 151 | return (address(0), RecoverError.InvalidSignature, bytes32(0)); 152 | } 153 | 154 | return (signer, RecoverError.NoError, bytes32(0)); 155 | } 156 | 157 | /** 158 | * @dev Overload of {ECDSA-recover} that receives the `v`, 159 | * `r` and `s` signature fields separately. 160 | */ 161 | function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { 162 | (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s); 163 | _throwError(error, errorArg); 164 | return recovered; 165 | } 166 | 167 | /** 168 | * @dev Optionally reverts with the corresponding custom error according to the `error` argument provided. 169 | */ 170 | function _throwError(RecoverError error, bytes32 errorArg) private pure { 171 | if (error == RecoverError.NoError) { 172 | return; // no error: do nothing 173 | } else if (error == RecoverError.InvalidSignature) { 174 | revert ECDSAInvalidSignature(); 175 | } else if (error == RecoverError.InvalidSignatureLength) { 176 | revert ECDSAInvalidSignatureLength(uint256(errorArg)); 177 | } else if (error == RecoverError.InvalidSignatureS) { 178 | revert ECDSAInvalidSignatureS(errorArg); 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/ERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.0; 3 | 4 | import {ECDSA} from './ECDSA.sol'; 5 | 6 | /// @notice Modern and gas efficient ERC20 + EIP-2612 implementation. 7 | /// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol) 8 | /// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) 9 | /// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. 10 | abstract contract ERC20 { 11 | bytes32 public constant PERMIT_TYPEHASH = 12 | keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)'); 13 | 14 | /* ////////////////////////////////////////////////////////////// 15 | EVENTS 16 | ////////////////////////////////////////////////////////////// */ 17 | 18 | event Transfer(address indexed from, address indexed to, uint256 amount); 19 | 20 | event Approval(address indexed owner, address indexed spender, uint256 amount); 21 | 22 | /* ////////////////////////////////////////////////////////////// 23 | METADATA STORAGE 24 | ////////////////////////////////////////////////////////////// */ 25 | 26 | string public name; 27 | 28 | string public symbol; 29 | 30 | uint8 public decimals; 31 | 32 | /* ////////////////////////////////////////////////////////////// 33 | ERC20 STORAGE 34 | ////////////////////////////////////////////////////////////// */ 35 | 36 | uint256 public totalSupply; 37 | 38 | mapping(address => uint256) public balanceOf; 39 | 40 | mapping(address => mapping(address => uint256)) public allowance; 41 | 42 | /* ////////////////////////////////////////////////////////////// 43 | EIP-2612 STORAGE 44 | ////////////////////////////////////////////////////////////// */ 45 | 46 | mapping(address => uint256) public nonces; 47 | 48 | /* ////////////////////////////////////////////////////////////// 49 | CONSTRUCTOR 50 | ////////////////////////////////////////////////////////////// */ 51 | 52 | constructor(string memory _name, string memory _symbol, uint8 _decimals) { 53 | name = _name; 54 | symbol = _symbol; 55 | decimals = _decimals; 56 | } 57 | 58 | /* ////////////////////////////////////////////////////////////// 59 | ERC20 LOGIC 60 | ////////////////////////////////////////////////////////////// */ 61 | 62 | function approve(address spender, uint256 amount) public virtual returns (bool) { 63 | allowance[msg.sender][spender] = amount; 64 | 65 | emit Approval(msg.sender, spender, amount); 66 | 67 | return true; 68 | } 69 | 70 | function transfer(address to, uint256 amount) public virtual returns (bool) { 71 | _beforeTokenTransfer(msg.sender, to, amount); 72 | balanceOf[msg.sender] -= amount; 73 | 74 | // Cannot overflow because the sum of all user 75 | // balances can't exceed the max uint256 value. 76 | unchecked { 77 | balanceOf[to] += amount; 78 | } 79 | 80 | emit Transfer(msg.sender, to, amount); 81 | 82 | return true; 83 | } 84 | 85 | function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) { 86 | _beforeTokenTransfer(from, to, amount); 87 | uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. 88 | 89 | if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; 90 | 91 | balanceOf[from] -= amount; 92 | 93 | // Cannot overflow because the sum of all user 94 | // balances can't exceed the max uint256 value. 95 | unchecked { 96 | balanceOf[to] += amount; 97 | } 98 | 99 | emit Transfer(from, to, amount); 100 | 101 | return true; 102 | } 103 | 104 | /* ////////////////////////////////////////////////////////////// 105 | EIP-2612 LOGIC 106 | ////////////////////////////////////////////////////////////// */ 107 | 108 | function permit( 109 | address owner, 110 | address spender, 111 | uint256 value, 112 | uint256 deadline, 113 | uint8 v, 114 | bytes32 r, 115 | bytes32 s 116 | ) public virtual { 117 | require(deadline >= block.timestamp, 'PERMIT_DEADLINE_EXPIRED'); 118 | 119 | // Unchecked because the only math done is incrementing 120 | // the owner's nonce which cannot realistically overflow. 121 | unchecked { 122 | address signer = ECDSA.recover( 123 | keccak256( 124 | abi.encodePacked( 125 | '\x19\x01', 126 | DOMAIN_SEPARATOR(), 127 | keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) 128 | ) 129 | ), 130 | v, 131 | r, 132 | s 133 | ); 134 | 135 | require(signer == owner, 'INVALID_SIGNER'); 136 | 137 | allowance[signer][spender] = value; 138 | } 139 | 140 | emit Approval(owner, spender, value); 141 | } 142 | 143 | function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { 144 | return computeDomainSeparator(); 145 | } 146 | 147 | function computeDomainSeparator() internal view virtual returns (bytes32) { 148 | return 149 | keccak256( 150 | abi.encode( 151 | keccak256( 152 | 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' 153 | ), 154 | keccak256(bytes(name)), 155 | keccak256('1'), 156 | block.chainid, 157 | address(this) 158 | ) 159 | ); 160 | } 161 | 162 | /* ////////////////////////////////////////////////////////////// 163 | INTERNAL MINT/BURN LOGIC 164 | ////////////////////////////////////////////////////////////// */ 165 | 166 | function _mint(address to, uint256 amount) internal virtual { 167 | _beforeTokenTransfer(address(0), to, amount); 168 | totalSupply += amount; 169 | 170 | // Cannot overflow because the sum of all user 171 | // balances can't exceed the max uint256 value. 172 | unchecked { 173 | balanceOf[to] += amount; 174 | } 175 | 176 | emit Transfer(address(0), to, amount); 177 | } 178 | 179 | function _burn(address from, uint256 amount) internal virtual { 180 | _beforeTokenTransfer(from, address(0), amount); 181 | balanceOf[from] -= amount; 182 | 183 | // Cannot underflow because a user's balance 184 | // will never be larger than the total supply. 185 | unchecked { 186 | totalSupply -= amount; 187 | } 188 | 189 | emit Transfer(from, address(0), amount); 190 | } 191 | 192 | /** 193 | * @dev Hook that is called before any transfer of tokens. This includes 194 | * minting and burning. 195 | * 196 | * Calling conditions: 197 | * 198 | * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens 199 | * will be to transferred to `to`. 200 | * - when `from` is zero, `amount` tokens will be minted for `to`. 201 | * - when `to` is zero, `amount` of ``from``'s tokens will be burned. 202 | * - `from` and `to` are never both zero. 203 | * 204 | * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. 205 | */ 206 | function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {} 207 | } 208 | -------------------------------------------------------------------------------- /src/RayMathExplicitRounding.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity ^0.8.10; 3 | 4 | enum Rounding { 5 | UP, 6 | DOWN 7 | } 8 | 9 | /** 10 | * Simplified version of RayMath that instead of half-up rounding does explicit rounding in a specified direction. 11 | * This is needed to have a 4626 complient implementation, that always predictable rounds in favor of the vault / static a token. 12 | */ 13 | library RayMathExplicitRounding { 14 | uint256 internal constant RAY = 1e27; 15 | uint256 internal constant WAD_RAY_RATIO = 1e9; 16 | 17 | function rayMulRoundDown(uint256 a, uint256 b) internal pure returns (uint256) { 18 | if (a == 0 || b == 0) { 19 | return 0; 20 | } 21 | return (a * b) / RAY; 22 | } 23 | 24 | function rayMulRoundUp(uint256 a, uint256 b) internal pure returns (uint256) { 25 | if (a == 0 || b == 0) { 26 | return 0; 27 | } 28 | return ((a * b) + RAY - 1) / RAY; 29 | } 30 | 31 | function rayDivRoundDown(uint256 a, uint256 b) internal pure returns (uint256) { 32 | return (a * RAY) / b; 33 | } 34 | 35 | function rayDivRoundUp(uint256 a, uint256 b) internal pure returns (uint256) { 36 | return ((a * RAY) + b - 1) / b; 37 | } 38 | 39 | function rayToWadRoundDown(uint256 a) internal pure returns (uint256) { 40 | return a / WAD_RAY_RATIO; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/StataOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.10; 3 | 4 | import {IPool} from 'aave-v3-core/contracts/interfaces/IPool.sol'; 5 | import {IPoolAddressesProvider} from 'aave-v3-core/contracts/interfaces/IPoolAddressesProvider.sol'; 6 | import {IAaveOracle} from 'aave-v3-core/contracts/interfaces/IAaveOracle.sol'; 7 | import {IERC20Metadata} from 'solidity-utils/contracts/oz-common/interfaces/IERC20Metadata.sol'; 8 | import {IStaticATokenLM} from './interfaces/IStaticATokenLM.sol'; 9 | import {IStataOracle} from './interfaces/IStataOracle.sol'; 10 | import {IERC4626} from './interfaces/IERC4626.sol'; 11 | 12 | /** 13 | * @title StataOracle 14 | * @author BGD Labs 15 | * @notice Contract to get asset prices of stata tokens 16 | */ 17 | contract StataOracle is IStataOracle { 18 | /// @inheritdoc IStataOracle 19 | IPool public immutable POOL; 20 | /// @inheritdoc IStataOracle 21 | IAaveOracle public immutable AAVE_ORACLE; 22 | 23 | constructor(IPoolAddressesProvider provider) { 24 | POOL = IPool(provider.getPool()); 25 | AAVE_ORACLE = IAaveOracle(provider.getPriceOracle()); 26 | } 27 | 28 | /// @inheritdoc IStataOracle 29 | function getAssetPrice(address asset) public view returns (uint256) { 30 | address underlying = IERC4626(asset).asset(); 31 | return 32 | (AAVE_ORACLE.getAssetPrice(underlying) * POOL.getReserveNormalizedIncome(underlying)) / 1e27; 33 | } 34 | 35 | /// @inheritdoc IStataOracle 36 | function getAssetsPrices(address[] calldata assets) external view returns (uint256[] memory) { 37 | uint256[] memory prices = new uint256[](assets.length); 38 | for (uint256 i = 0; i < assets.length; i++) { 39 | prices[i] = getAssetPrice(assets[i]); 40 | } 41 | return prices; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/StaticATokenErrors.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity ^0.8.10; 3 | 4 | library StaticATokenErrors { 5 | string public constant INVALID_OWNER = '1'; 6 | string public constant INVALID_EXPIRATION = '2'; 7 | string public constant INVALID_SIGNATURE = '3'; 8 | string public constant INVALID_DEPOSITOR = '4'; 9 | string public constant INVALID_RECIPIENT = '5'; 10 | string public constant INVALID_CLAIMER = '6'; 11 | string public constant ONLY_ONE_AMOUNT_FORMAT_ALLOWED = '7'; 12 | string public constant INVALID_ZERO_AMOUNT = '8'; 13 | string public constant REWARD_NOT_INITIALIZED = '9'; 14 | } 15 | -------------------------------------------------------------------------------- /src/StaticATokenFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.10; 3 | 4 | import {IPool, DataTypes} from 'aave-address-book/AaveV3.sol'; 5 | import {IERC20Metadata} from 'solidity-utils/contracts/oz-common/interfaces/IERC20Metadata.sol'; 6 | import {ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/interfaces/ITransparentProxyFactory.sol'; 7 | import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol'; 8 | import {StaticATokenLM} from './StaticATokenLM.sol'; 9 | import {IStaticATokenFactory} from './interfaces/IStaticATokenFactory.sol'; 10 | 11 | /** 12 | * @title StaticATokenFactory 13 | * @notice Factory contract that keeps track of all deployed static aToken wrappers for a specified pool. 14 | * This registry also acts as a factory, allowing to deploy new static aTokens on demand. 15 | * There can only be one static aToken per underlying on the registry at a time. 16 | * @author BGD labs 17 | */ 18 | contract StaticATokenFactory is Initializable, IStaticATokenFactory { 19 | IPool public immutable POOL; 20 | address public immutable ADMIN; 21 | ITransparentProxyFactory public immutable TRANSPARENT_PROXY_FACTORY; 22 | address public immutable STATIC_A_TOKEN_IMPL; 23 | 24 | mapping(address => address) internal _underlyingToStaticAToken; 25 | address[] internal _staticATokens; 26 | 27 | event StaticTokenCreated(address indexed staticAToken, address indexed underlying); 28 | 29 | constructor( 30 | IPool pool, 31 | address proxyAdmin, 32 | ITransparentProxyFactory transparentProxyFactory, 33 | address staticATokenImpl 34 | ) { 35 | POOL = pool; 36 | ADMIN = proxyAdmin; 37 | TRANSPARENT_PROXY_FACTORY = transparentProxyFactory; 38 | STATIC_A_TOKEN_IMPL = staticATokenImpl; 39 | } 40 | 41 | function initialize() external initializer {} 42 | 43 | ///@inheritdoc IStaticATokenFactory 44 | function createStaticATokens(address[] memory underlyings) external returns (address[] memory) { 45 | address[] memory staticATokens = new address[](underlyings.length); 46 | for (uint256 i = 0; i < underlyings.length; i++) { 47 | address cachedStaticAToken = _underlyingToStaticAToken[underlyings[i]]; 48 | if (cachedStaticAToken == address(0)) { 49 | DataTypes.ReserveData memory reserveData = POOL.getReserveData(underlyings[i]); 50 | require(reserveData.aTokenAddress != address(0), 'UNDERLYING_NOT_LISTED'); 51 | bytes memory symbol = abi.encodePacked( 52 | 'stat', 53 | IERC20Metadata(reserveData.aTokenAddress).symbol() 54 | ); 55 | address staticAToken = TRANSPARENT_PROXY_FACTORY.createDeterministic( 56 | STATIC_A_TOKEN_IMPL, 57 | ADMIN, 58 | abi.encodeWithSelector( 59 | StaticATokenLM.initialize.selector, 60 | reserveData.aTokenAddress, 61 | string(abi.encodePacked('Static ', IERC20Metadata(reserveData.aTokenAddress).name())), 62 | string(symbol) 63 | ), 64 | bytes32(uint256(uint160(underlyings[i]))) 65 | ); 66 | _underlyingToStaticAToken[underlyings[i]] = staticAToken; 67 | staticATokens[i] = staticAToken; 68 | _staticATokens.push(staticAToken); 69 | emit StaticTokenCreated(staticAToken, underlyings[i]); 70 | } else { 71 | staticATokens[i] = cachedStaticAToken; 72 | } 73 | } 74 | return staticATokens; 75 | } 76 | 77 | ///@inheritdoc IStaticATokenFactory 78 | function getStaticATokens() external view returns (address[] memory) { 79 | return _staticATokens; 80 | } 81 | 82 | ///@inheritdoc IStaticATokenFactory 83 | function getStaticAToken(address underlying) external view returns (address) { 84 | return _underlyingToStaticAToken[underlying]; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/StaticATokenLM.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.10; 3 | 4 | import {IPool} from 'aave-v3-core/contracts/interfaces/IPool.sol'; 5 | import {DataTypes, ReserveConfiguration} from 'aave-v3-core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; 6 | import {IScaledBalanceToken} from 'aave-v3-core/contracts/interfaces/IScaledBalanceToken.sol'; 7 | import {IRewardsController} from 'aave-v3-periphery/contracts/rewards/interfaces/IRewardsController.sol'; 8 | import {WadRayMath} from 'aave-v3-core/contracts/protocol/libraries/math/WadRayMath.sol'; 9 | import {MathUtils} from 'aave-v3-core/contracts/protocol/libraries/math/MathUtils.sol'; 10 | import {SafeCast} from 'solidity-utils/contracts/oz-common/SafeCast.sol'; 11 | import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol'; 12 | import {SafeERC20} from 'solidity-utils/contracts/oz-common/SafeERC20.sol'; 13 | import {IERC20Metadata} from 'solidity-utils/contracts/oz-common/interfaces/IERC20Metadata.sol'; 14 | import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; 15 | import {IERC20WithPermit} from 'solidity-utils/contracts/oz-common/interfaces/IERC20WithPermit.sol'; 16 | 17 | import {IStaticATokenLM} from './interfaces/IStaticATokenLM.sol'; 18 | import {IAToken} from './interfaces/IAToken.sol'; 19 | import {ERC20} from './ERC20.sol'; 20 | import {IInitializableStaticATokenLM} from './interfaces/IInitializableStaticATokenLM.sol'; 21 | import {StaticATokenErrors} from './StaticATokenErrors.sol'; 22 | import {RayMathExplicitRounding, Rounding} from './RayMathExplicitRounding.sol'; 23 | import {IERC4626} from './interfaces/IERC4626.sol'; 24 | 25 | /** 26 | * @title StaticATokenLM 27 | * @notice Wrapper smart contract that allows to deposit tokens on the Aave protocol and receive 28 | * a token which balance doesn't increase automatically, but uses an ever-increasing exchange rate. 29 | * It supports claiming liquidity mining rewards from the Aave system. 30 | * @author BGD labs 31 | */ 32 | contract StaticATokenLM is 33 | Initializable, 34 | ERC20('STATIC__aToken_IMPL', 'STATIC__aToken_IMPL', 18), 35 | IStaticATokenLM, 36 | IERC4626 37 | { 38 | using SafeERC20 for IERC20; 39 | using SafeCast for uint256; 40 | using WadRayMath for uint256; 41 | using RayMathExplicitRounding for uint256; 42 | 43 | bytes32 public constant METADEPOSIT_TYPEHASH = 44 | keccak256( 45 | 'Deposit(address depositor,address receiver,uint256 assets,uint16 referralCode,bool depositToAave,uint256 nonce,uint256 deadline,PermitParams permit)' 46 | ); 47 | bytes32 public constant METAWITHDRAWAL_TYPEHASH = 48 | keccak256( 49 | 'Withdraw(address owner,address receiver,uint256 shares,uint256 assets,bool withdrawFromAave,uint256 nonce,uint256 deadline)' 50 | ); 51 | 52 | uint256 public constant STATIC__ATOKEN_LM_REVISION = 2; 53 | 54 | IPool public immutable POOL; 55 | IRewardsController public immutable INCENTIVES_CONTROLLER; 56 | 57 | IERC20 internal _aToken; 58 | address internal _aTokenUnderlying; 59 | address[] internal _rewardTokens; 60 | mapping(address => RewardIndexCache) internal _startIndex; 61 | mapping(address => mapping(address => UserRewardsData)) internal _userRewardsData; 62 | 63 | constructor(IPool pool, IRewardsController rewardsController) { 64 | POOL = pool; 65 | INCENTIVES_CONTROLLER = rewardsController; 66 | } 67 | 68 | ///@inheritdoc IInitializableStaticATokenLM 69 | function initialize( 70 | address newAToken, 71 | string calldata staticATokenName, 72 | string calldata staticATokenSymbol 73 | ) external initializer { 74 | require(IAToken(newAToken).POOL() == address(POOL)); 75 | _aToken = IERC20(newAToken); 76 | 77 | name = staticATokenName; 78 | symbol = staticATokenSymbol; 79 | decimals = IERC20Metadata(newAToken).decimals(); 80 | 81 | _aTokenUnderlying = IAToken(newAToken).UNDERLYING_ASSET_ADDRESS(); 82 | IERC20(_aTokenUnderlying).forceApprove(address(POOL), type(uint256).max); 83 | 84 | if (INCENTIVES_CONTROLLER != IRewardsController(address(0))) { 85 | refreshRewardTokens(); 86 | } 87 | 88 | emit Initialized(newAToken, staticATokenName, staticATokenSymbol); 89 | } 90 | 91 | ///@inheritdoc IStaticATokenLM 92 | function refreshRewardTokens() public override { 93 | address[] memory rewards = INCENTIVES_CONTROLLER.getRewardsByAsset(address(_aToken)); 94 | for (uint256 i = 0; i < rewards.length; i++) { 95 | _registerRewardToken(rewards[i]); 96 | } 97 | } 98 | 99 | ///@inheritdoc IStaticATokenLM 100 | function isRegisteredRewardToken(address reward) public view override returns (bool) { 101 | return _startIndex[reward].isRegistered; 102 | } 103 | 104 | ///@inheritdoc IStaticATokenLM 105 | function deposit( 106 | uint256 assets, 107 | address receiver, 108 | uint16 referralCode, 109 | bool depositToAave 110 | ) external returns (uint256) { 111 | (uint256 shares, ) = _deposit(msg.sender, receiver, 0, assets, referralCode, depositToAave); 112 | return shares; 113 | } 114 | 115 | ///@inheritdoc IStaticATokenLM 116 | function metaDeposit( 117 | address depositor, 118 | address receiver, 119 | uint256 assets, 120 | uint16 referralCode, 121 | bool depositToAave, 122 | uint256 deadline, 123 | PermitParams calldata permit, 124 | SignatureParams calldata sigParams 125 | ) external returns (uint256) { 126 | require(depositor != address(0), StaticATokenErrors.INVALID_DEPOSITOR); 127 | //solium-disable-next-line 128 | require(deadline >= block.timestamp, StaticATokenErrors.INVALID_EXPIRATION); 129 | uint256 nonce = nonces[depositor]; 130 | 131 | // Unchecked because the only math done is incrementing 132 | // the owner's nonce which cannot realistically overflow. 133 | unchecked { 134 | bytes32 digest = keccak256( 135 | abi.encodePacked( 136 | '\x19\x01', 137 | DOMAIN_SEPARATOR(), 138 | keccak256( 139 | abi.encode( 140 | METADEPOSIT_TYPEHASH, 141 | depositor, 142 | receiver, 143 | assets, 144 | referralCode, 145 | depositToAave, 146 | nonce, 147 | deadline, 148 | permit 149 | ) 150 | ) 151 | ) 152 | ); 153 | nonces[depositor] = nonce + 1; 154 | require( 155 | depositor == ecrecover(digest, sigParams.v, sigParams.r, sigParams.s), 156 | StaticATokenErrors.INVALID_SIGNATURE 157 | ); 158 | } 159 | // assume if deadline 0 no permit was supplied 160 | if (permit.deadline != 0) { 161 | try 162 | IERC20WithPermit(depositToAave ? address(_aTokenUnderlying) : address(_aToken)).permit( 163 | depositor, 164 | address(this), 165 | permit.value, 166 | permit.deadline, 167 | permit.v, 168 | permit.r, 169 | permit.s 170 | ) 171 | {} catch {} 172 | } 173 | (uint256 shares, ) = _deposit(depositor, receiver, 0, assets, referralCode, depositToAave); 174 | return shares; 175 | } 176 | 177 | ///@inheritdoc IStaticATokenLM 178 | function metaWithdraw( 179 | address owner, 180 | address receiver, 181 | uint256 shares, 182 | uint256 assets, 183 | bool withdrawFromAave, 184 | uint256 deadline, 185 | SignatureParams calldata sigParams 186 | ) external returns (uint256, uint256) { 187 | require(owner != address(0), StaticATokenErrors.INVALID_OWNER); 188 | //solium-disable-next-line 189 | require(deadline >= block.timestamp, StaticATokenErrors.INVALID_EXPIRATION); 190 | uint256 nonce = nonces[owner]; 191 | // Unchecked because the only math done is incrementing 192 | // the owner's nonce which cannot realistically overflow. 193 | unchecked { 194 | bytes32 digest = keccak256( 195 | abi.encodePacked( 196 | '\x19\x01', 197 | DOMAIN_SEPARATOR(), 198 | keccak256( 199 | abi.encode( 200 | METAWITHDRAWAL_TYPEHASH, 201 | owner, 202 | receiver, 203 | shares, 204 | assets, 205 | withdrawFromAave, 206 | nonce, 207 | deadline 208 | ) 209 | ) 210 | ) 211 | ); 212 | nonces[owner] = nonce + 1; 213 | require( 214 | owner == ecrecover(digest, sigParams.v, sigParams.r, sigParams.s), 215 | StaticATokenErrors.INVALID_SIGNATURE 216 | ); 217 | } 218 | return _withdraw(owner, receiver, shares, assets, withdrawFromAave); 219 | } 220 | 221 | ///@inheritdoc IERC4626 222 | function previewRedeem(uint256 shares) public view virtual returns (uint256) { 223 | return _convertToAssets(shares, Rounding.DOWN); 224 | } 225 | 226 | ///@inheritdoc IERC4626 227 | function previewMint(uint256 shares) public view virtual returns (uint256) { 228 | return _convertToAssets(shares, Rounding.UP); 229 | } 230 | 231 | ///@inheritdoc IERC4626 232 | function previewWithdraw(uint256 assets) public view virtual returns (uint256) { 233 | return _convertToShares(assets, Rounding.UP); 234 | } 235 | 236 | ///@inheritdoc IERC4626 237 | function previewDeposit(uint256 assets) public view virtual returns (uint256) { 238 | return _convertToShares(assets, Rounding.DOWN); 239 | } 240 | 241 | ///@inheritdoc IStaticATokenLM 242 | function rate() public view returns (uint256) { 243 | return POOL.getReserveNormalizedIncome(_aTokenUnderlying); 244 | } 245 | 246 | ///@inheritdoc IStaticATokenLM 247 | function collectAndUpdateRewards(address reward) public returns (uint256) { 248 | if (reward == address(0)) { 249 | return 0; 250 | } 251 | 252 | address[] memory assets = new address[](1); 253 | assets[0] = address(_aToken); 254 | 255 | return INCENTIVES_CONTROLLER.claimRewards(assets, type(uint256).max, address(this), reward); 256 | } 257 | 258 | ///@inheritdoc IStaticATokenLM 259 | function claimRewardsOnBehalf( 260 | address onBehalfOf, 261 | address receiver, 262 | address[] memory rewards 263 | ) external { 264 | require( 265 | msg.sender == onBehalfOf || msg.sender == INCENTIVES_CONTROLLER.getClaimer(onBehalfOf), 266 | StaticATokenErrors.INVALID_CLAIMER 267 | ); 268 | _claimRewardsOnBehalf(onBehalfOf, receiver, rewards); 269 | } 270 | 271 | ///@inheritdoc IStaticATokenLM 272 | function claimRewards(address receiver, address[] memory rewards) external { 273 | _claimRewardsOnBehalf(msg.sender, receiver, rewards); 274 | } 275 | 276 | ///@inheritdoc IStaticATokenLM 277 | function claimRewardsToSelf(address[] memory rewards) external { 278 | _claimRewardsOnBehalf(msg.sender, msg.sender, rewards); 279 | } 280 | 281 | ///@inheritdoc IStaticATokenLM 282 | function getCurrentRewardsIndex(address reward) public view returns (uint256) { 283 | if (address(reward) == address(0)) { 284 | return 0; 285 | } 286 | (, uint256 nextIndex) = INCENTIVES_CONTROLLER.getAssetIndex(address(_aToken), reward); 287 | return nextIndex; 288 | } 289 | 290 | ///@inheritdoc IStaticATokenLM 291 | function getTotalClaimableRewards(address reward) external view returns (uint256) { 292 | if (reward == address(0)) { 293 | return 0; 294 | } 295 | 296 | address[] memory assets = new address[](1); 297 | assets[0] = address(_aToken); 298 | uint256 freshRewards = INCENTIVES_CONTROLLER.getUserRewards(assets, address(this), reward); 299 | return IERC20(reward).balanceOf(address(this)) + freshRewards; 300 | } 301 | 302 | ///@inheritdoc IStaticATokenLM 303 | function getClaimableRewards(address user, address reward) external view returns (uint256) { 304 | return _getClaimableRewards(user, reward, balanceOf[user], getCurrentRewardsIndex(reward)); 305 | } 306 | 307 | ///@inheritdoc IStaticATokenLM 308 | function getUnclaimedRewards(address user, address reward) external view returns (uint256) { 309 | return _userRewardsData[user][reward].unclaimedRewards; 310 | } 311 | 312 | ///@inheritdoc IERC4626 313 | function asset() external view returns (address) { 314 | return address(_aTokenUnderlying); 315 | } 316 | 317 | ///@inheritdoc IStaticATokenLM 318 | function aToken() external view returns (IERC20) { 319 | return _aToken; 320 | } 321 | 322 | ///@inheritdoc IStaticATokenLM 323 | function rewardTokens() external view returns (address[] memory) { 324 | return _rewardTokens; 325 | } 326 | 327 | ///@inheritdoc IERC4626 328 | function totalAssets() external view returns (uint256) { 329 | return _aToken.balanceOf(address(this)); 330 | } 331 | 332 | ///@inheritdoc IERC4626 333 | function convertToShares(uint256 assets) external view returns (uint256) { 334 | return _convertToShares(assets, Rounding.DOWN); 335 | } 336 | 337 | ///@inheritdoc IERC4626 338 | function convertToAssets(uint256 shares) external view returns (uint256) { 339 | return _convertToAssets(shares, Rounding.DOWN); 340 | } 341 | 342 | ///@inheritdoc IERC4626 343 | function maxMint(address) public view virtual returns (uint256) { 344 | uint256 assets = maxDeposit(address(0)); 345 | if (assets == type(uint256).max) return type(uint256).max; 346 | return _convertToShares(assets, Rounding.DOWN); 347 | } 348 | 349 | ///@inheritdoc IERC4626 350 | function maxWithdraw(address owner) public view virtual returns (uint256) { 351 | uint256 shares = maxRedeem(owner); 352 | return _convertToAssets(shares, Rounding.DOWN); 353 | } 354 | 355 | ///@inheritdoc IERC4626 356 | function maxRedeem(address owner) public view virtual returns (uint256) { 357 | address cachedATokenUnderlying = _aTokenUnderlying; 358 | DataTypes.ReserveData memory reserveData = POOL.getReserveData(cachedATokenUnderlying); 359 | 360 | // if paused or inactive users cannot withdraw underlying 361 | if ( 362 | !ReserveConfiguration.getActive(reserveData.configuration) || 363 | ReserveConfiguration.getPaused(reserveData.configuration) 364 | ) { 365 | return 0; 366 | } 367 | 368 | // otherwise users can withdraw up to the available amount 369 | uint256 underlyingTokenBalanceInShares = _convertToShares( 370 | IERC20(cachedATokenUnderlying).balanceOf(reserveData.aTokenAddress), 371 | Rounding.DOWN 372 | ); 373 | uint256 cachedUserBalance = balanceOf[owner]; 374 | return 375 | underlyingTokenBalanceInShares >= cachedUserBalance 376 | ? cachedUserBalance 377 | : underlyingTokenBalanceInShares; 378 | } 379 | 380 | ///@inheritdoc IERC4626 381 | function maxDeposit(address) public view virtual returns (uint256) { 382 | DataTypes.ReserveData memory reserveData = POOL.getReserveData(_aTokenUnderlying); 383 | 384 | // if inactive, paused or frozen users cannot deposit underlying 385 | if ( 386 | !ReserveConfiguration.getActive(reserveData.configuration) || 387 | ReserveConfiguration.getPaused(reserveData.configuration) || 388 | ReserveConfiguration.getFrozen(reserveData.configuration) 389 | ) { 390 | return 0; 391 | } 392 | 393 | uint256 supplyCap = ReserveConfiguration.getSupplyCap(reserveData.configuration) * 394 | (10 ** ReserveConfiguration.getDecimals(reserveData.configuration)); 395 | // if no supply cap deposit is unlimited 396 | if (supplyCap == 0) return type(uint256).max; 397 | // return remaining supply cap margin 398 | uint256 currentSupply = (IAToken(reserveData.aTokenAddress).scaledTotalSupply() + 399 | reserveData.accruedToTreasury).rayMulRoundUp(_getNormalizedIncome(reserveData)); 400 | return currentSupply > supplyCap ? 0 : supplyCap - currentSupply; 401 | } 402 | 403 | ///@inheritdoc IERC4626 404 | function deposit(uint256 assets, address receiver) external virtual returns (uint256) { 405 | (uint256 shares, ) = _deposit(msg.sender, receiver, 0, assets, 0, true); 406 | return shares; 407 | } 408 | 409 | ///@inheritdoc IERC4626 410 | function mint(uint256 shares, address receiver) external virtual returns (uint256) { 411 | (, uint256 assets) = _deposit(msg.sender, receiver, shares, 0, 0, true); 412 | 413 | return assets; 414 | } 415 | 416 | ///@inheritdoc IERC4626 417 | function withdraw( 418 | uint256 assets, 419 | address receiver, 420 | address owner 421 | ) external virtual returns (uint256) { 422 | (uint256 shares, ) = _withdraw(owner, receiver, 0, assets, true); 423 | 424 | return shares; 425 | } 426 | 427 | ///@inheritdoc IERC4626 428 | function redeem( 429 | uint256 shares, 430 | address receiver, 431 | address owner 432 | ) external virtual returns (uint256) { 433 | (, uint256 assets) = _withdraw(owner, receiver, shares, 0, true); 434 | 435 | return assets; 436 | } 437 | 438 | ///@inheritdoc IStaticATokenLM 439 | function redeem( 440 | uint256 shares, 441 | address receiver, 442 | address owner, 443 | bool withdrawFromAave 444 | ) external virtual returns (uint256, uint256) { 445 | return _withdraw(owner, receiver, shares, 0, withdrawFromAave); 446 | } 447 | 448 | function _deposit( 449 | address depositor, 450 | address receiver, 451 | uint256 _shares, 452 | uint256 _assets, 453 | uint16 referralCode, 454 | bool depositToAave 455 | ) internal returns (uint256, uint256) { 456 | require(receiver != address(0), StaticATokenErrors.INVALID_RECIPIENT); 457 | require(_shares == 0 || _assets == 0, StaticATokenErrors.ONLY_ONE_AMOUNT_FORMAT_ALLOWED); 458 | 459 | uint256 assets = _assets; 460 | uint256 shares = _shares; 461 | if (shares > 0) { 462 | if (depositToAave) { 463 | require(shares <= maxMint(receiver), 'ERC4626: mint more than max'); 464 | } 465 | assets = previewMint(shares); 466 | } else { 467 | if (depositToAave) { 468 | require(assets <= maxDeposit(receiver), 'ERC4626: deposit more than max'); 469 | } 470 | shares = previewDeposit(assets); 471 | } 472 | require(shares != 0, StaticATokenErrors.INVALID_ZERO_AMOUNT); 473 | 474 | if (depositToAave) { 475 | address cachedATokenUnderlying = _aTokenUnderlying; 476 | IERC20(cachedATokenUnderlying).safeTransferFrom(depositor, address(this), assets); 477 | POOL.deposit(cachedATokenUnderlying, assets, address(this), referralCode); 478 | } else { 479 | _aToken.safeTransferFrom(depositor, address(this), assets); 480 | } 481 | 482 | _mint(receiver, shares); 483 | 484 | emit Deposit(depositor, receiver, assets, shares); 485 | 486 | return (shares, assets); 487 | } 488 | 489 | function _withdraw( 490 | address owner, 491 | address receiver, 492 | uint256 _shares, 493 | uint256 _assets, 494 | bool withdrawFromAave 495 | ) internal returns (uint256, uint256) { 496 | require(receiver != address(0), StaticATokenErrors.INVALID_RECIPIENT); 497 | require(_shares == 0 || _assets == 0, StaticATokenErrors.ONLY_ONE_AMOUNT_FORMAT_ALLOWED); 498 | require(_shares != _assets, StaticATokenErrors.INVALID_ZERO_AMOUNT); 499 | 500 | uint256 assets = _assets; 501 | uint256 shares = _shares; 502 | 503 | if (shares > 0) { 504 | if (withdrawFromAave) { 505 | require(shares <= maxRedeem(owner), 'ERC4626: redeem more than max'); 506 | } 507 | assets = previewRedeem(shares); 508 | } else { 509 | if (withdrawFromAave) { 510 | require(assets <= maxWithdraw(owner), 'ERC4626: withdraw more than max'); 511 | } 512 | shares = previewWithdraw(assets); 513 | } 514 | 515 | if (msg.sender != owner) { 516 | uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. 517 | 518 | if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; 519 | } 520 | 521 | _burn(owner, shares); 522 | 523 | emit Withdraw(msg.sender, receiver, owner, assets, shares); 524 | 525 | if (withdrawFromAave) { 526 | POOL.withdraw(_aTokenUnderlying, assets, receiver); 527 | } else { 528 | _aToken.safeTransfer(receiver, assets); 529 | } 530 | 531 | return (shares, assets); 532 | } 533 | 534 | /** 535 | * @notice Updates rewards for senders and receiver in a transfer (not updating rewards for address(0)) 536 | * @param from The address of the sender of tokens 537 | * @param to The address of the receiver of tokens 538 | */ 539 | function _beforeTokenTransfer(address from, address to, uint256) internal override { 540 | for (uint256 i = 0; i < _rewardTokens.length; i++) { 541 | address rewardToken = address(_rewardTokens[i]); 542 | uint256 rewardsIndex = getCurrentRewardsIndex(rewardToken); 543 | if (from != address(0)) { 544 | _updateUser(from, rewardsIndex, rewardToken); 545 | } 546 | if (to != address(0) && from != to) { 547 | _updateUser(to, rewardsIndex, rewardToken); 548 | } 549 | } 550 | } 551 | 552 | /** 553 | * @notice Adding the pending rewards to the unclaimed for specific user and updating user index 554 | * @param user The address of the user to update 555 | * @param currentRewardsIndex The current rewardIndex 556 | * @param rewardToken The address of the reward token 557 | */ 558 | function _updateUser(address user, uint256 currentRewardsIndex, address rewardToken) internal { 559 | uint256 balance = balanceOf[user]; 560 | if (balance > 0) { 561 | _userRewardsData[user][rewardToken].unclaimedRewards = _getClaimableRewards( 562 | user, 563 | rewardToken, 564 | balance, 565 | currentRewardsIndex 566 | ).toUint128(); 567 | } 568 | _userRewardsData[user][rewardToken].rewardsIndexOnLastInteraction = currentRewardsIndex 569 | .toUint128(); 570 | } 571 | 572 | /** 573 | * @notice Compute the pending in WAD. Pending is the amount to add (not yet unclaimed) rewards in WAD. 574 | * @param balance The balance of the user 575 | * @param rewardsIndexOnLastInteraction The index which was on the last interaction of the user 576 | * @param currentRewardsIndex The current rewards index in the system 577 | * @param assetUnit One unit of asset (10**decimals) 578 | * @return The amount of pending rewards in WAD 579 | */ 580 | function _getPendingRewards( 581 | uint256 balance, 582 | uint256 rewardsIndexOnLastInteraction, 583 | uint256 currentRewardsIndex, 584 | uint256 assetUnit 585 | ) internal pure returns (uint256) { 586 | if (balance == 0) { 587 | return 0; 588 | } 589 | return (balance * (currentRewardsIndex - rewardsIndexOnLastInteraction)) / assetUnit; 590 | } 591 | 592 | /** 593 | * @notice Compute the claimable rewards for a user 594 | * @param user The address of the user 595 | * @param reward The address of the reward 596 | * @param balance The balance of the user in WAD 597 | * @param currentRewardsIndex The current rewards index 598 | * @return The total rewards that can be claimed by the user (if `fresh` flag true, after updating rewards) 599 | */ 600 | function _getClaimableRewards( 601 | address user, 602 | address reward, 603 | uint256 balance, 604 | uint256 currentRewardsIndex 605 | ) internal view returns (uint256) { 606 | RewardIndexCache memory rewardsIndexCache = _startIndex[reward]; 607 | require(rewardsIndexCache.isRegistered == true, StaticATokenErrors.REWARD_NOT_INITIALIZED); 608 | UserRewardsData memory currentUserRewardsData = _userRewardsData[user][reward]; 609 | uint256 assetUnit = 10 ** decimals; 610 | return 611 | currentUserRewardsData.unclaimedRewards + 612 | _getPendingRewards( 613 | balance, 614 | currentUserRewardsData.rewardsIndexOnLastInteraction == 0 615 | ? rewardsIndexCache.lastUpdatedIndex 616 | : currentUserRewardsData.rewardsIndexOnLastInteraction, 617 | currentRewardsIndex, 618 | assetUnit 619 | ); 620 | } 621 | 622 | /** 623 | * @notice Claim rewards on behalf of a user and send them to a receiver 624 | * @param onBehalfOf The address to claim on behalf of 625 | * @param rewards The addresses of the rewards 626 | * @param receiver The address to receive the rewards 627 | */ 628 | function _claimRewardsOnBehalf( 629 | address onBehalfOf, 630 | address receiver, 631 | address[] memory rewards 632 | ) internal { 633 | for (uint256 i = 0; i < rewards.length; i++) { 634 | if (address(rewards[i]) == address(0)) { 635 | continue; 636 | } 637 | uint256 currentRewardsIndex = getCurrentRewardsIndex(rewards[i]); 638 | uint256 balance = balanceOf[onBehalfOf]; 639 | uint256 userReward = _getClaimableRewards( 640 | onBehalfOf, 641 | rewards[i], 642 | balance, 643 | currentRewardsIndex 644 | ); 645 | uint256 totalRewardTokenBalance = IERC20(rewards[i]).balanceOf(address(this)); 646 | uint256 unclaimedReward = 0; 647 | 648 | if (userReward > totalRewardTokenBalance) { 649 | totalRewardTokenBalance += collectAndUpdateRewards(address(rewards[i])); 650 | } 651 | 652 | if (userReward > totalRewardTokenBalance) { 653 | unclaimedReward = userReward - totalRewardTokenBalance; 654 | userReward = totalRewardTokenBalance; 655 | } 656 | if (userReward > 0) { 657 | _userRewardsData[onBehalfOf][rewards[i]].unclaimedRewards = unclaimedReward.toUint128(); 658 | _userRewardsData[onBehalfOf][rewards[i]].rewardsIndexOnLastInteraction = currentRewardsIndex 659 | .toUint128(); 660 | IERC20(rewards[i]).safeTransfer(receiver, userReward); 661 | } 662 | } 663 | } 664 | 665 | function _convertToShares(uint256 assets, Rounding rounding) internal view returns (uint256) { 666 | if (rounding == Rounding.UP) return assets.rayDivRoundUp(rate()); 667 | return assets.rayDivRoundDown(rate()); 668 | } 669 | 670 | function _convertToAssets(uint256 shares, Rounding rounding) internal view returns (uint256) { 671 | if (rounding == Rounding.UP) return shares.rayMulRoundUp(rate()); 672 | return shares.rayMulRoundDown(rate()); 673 | } 674 | 675 | /** 676 | * @notice Initializes a new rewardToken 677 | * @param reward The reward token to be registered 678 | */ 679 | function _registerRewardToken(address reward) internal { 680 | if (isRegisteredRewardToken(reward)) return; 681 | uint256 startIndex = getCurrentRewardsIndex(reward); 682 | 683 | _rewardTokens.push(reward); 684 | _startIndex[reward] = RewardIndexCache(true, startIndex.toUint240()); 685 | 686 | emit RewardTokenRegistered(reward, startIndex); 687 | } 688 | 689 | /** 690 | * Copy of https://github.com/aave/aave-v3-core/blob/29ff9b9f89af7cd8255231bc5faf26c3ce0fb7ce/contracts/protocol/libraries/logic/ReserveLogic.sol#L47 with memory instead of calldata 691 | * @notice Returns the ongoing normalized income for the reserve. 692 | * @dev A value of 1e27 means there is no income. As time passes, the income is accrued 693 | * @dev A value of 2*1e27 means for each unit of asset one unit of income has been accrued 694 | * @param reserve The reserve object 695 | * @return The normalized income, expressed in ray 696 | */ 697 | function _getNormalizedIncome( 698 | DataTypes.ReserveData memory reserve 699 | ) internal view returns (uint256) { 700 | uint40 timestamp = reserve.lastUpdateTimestamp; 701 | 702 | //solium-disable-next-line 703 | if (timestamp == block.timestamp) { 704 | //if the index was updated in the same block, no need to perform any calculation 705 | return reserve.liquidityIndex; 706 | } else { 707 | return 708 | MathUtils.calculateLinearInterest(reserve.currentLiquidityRate, timestamp).rayMul( 709 | reserve.liquidityIndex 710 | ); 711 | } 712 | } 713 | } 714 | -------------------------------------------------------------------------------- /src/UpgradePayload.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | import {StaticATokenFactory} from './StaticATokenFactory.sol'; 4 | import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; 5 | import {ProxyAdmin} from 'solidity-utils/contracts/transparent-proxy/ProxyAdmin.sol'; 6 | 7 | contract UpgradePayload { 8 | ProxyAdmin public immutable ADMIN; 9 | StaticATokenFactory public immutable FACTORY; 10 | address public immutable NEW_TOKEN_IMPLEMENTATION; 11 | StaticATokenFactory public immutable NEW_FACTORY_IMPLEMENTATION; 12 | 13 | constructor( 14 | address admin, 15 | StaticATokenFactory factory, 16 | StaticATokenFactory newFactoryImpl, 17 | address newTokenImplementation 18 | ) { 19 | ADMIN = ProxyAdmin(admin); 20 | FACTORY = factory; 21 | NEW_FACTORY_IMPLEMENTATION = newFactoryImpl; 22 | NEW_TOKEN_IMPLEMENTATION = newTokenImplementation; 23 | } 24 | 25 | function execute() external { 26 | address[] memory tokens = FACTORY.getStaticATokens(); 27 | for (uint256 i = 0; i < tokens.length; i++) { 28 | ADMIN.upgrade(TransparentUpgradeableProxy(payable(tokens[i])), NEW_TOKEN_IMPLEMENTATION); 29 | } 30 | ADMIN.upgrade( 31 | TransparentUpgradeableProxy(payable(address(FACTORY))), 32 | address(NEW_FACTORY_IMPLEMENTATION) 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/interfaces/IAToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity ^0.8.10; 3 | 4 | import {IAaveIncentivesController} from 'aave-v3-core/contracts/interfaces/IAaveIncentivesController.sol'; 5 | 6 | interface IAToken { 7 | function POOL() external view returns (address); 8 | 9 | function getIncentivesController() external view returns (address); 10 | 11 | function UNDERLYING_ASSET_ADDRESS() external view returns (address); 12 | 13 | /** 14 | * @notice Returns the scaled total supply of the scaled balance token. Represents sum(debt/index) 15 | * @return The scaled total supply 16 | */ 17 | function scaledTotalSupply() external view returns (uint256); 18 | } 19 | -------------------------------------------------------------------------------- /src/interfaces/IERC4626.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.7.0) (interfaces/IERC4626.sol) 3 | 4 | pragma solidity ^0.8.10; 5 | 6 | /** 7 | * @dev Interface of the ERC4626 "Tokenized Vault Standard", as defined in 8 | * https://eips.ethereum.org/EIPS/eip-4626[ERC-4626]. 9 | * 10 | * _Available since v4.7._ 11 | */ 12 | interface IERC4626 { 13 | event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); 14 | 15 | event Withdraw( 16 | address indexed sender, 17 | address indexed receiver, 18 | address indexed owner, 19 | uint256 assets, 20 | uint256 shares 21 | ); 22 | 23 | /** 24 | * @dev Returns the address of the underlying token used for the Vault for accounting, depositing, and withdrawing. 25 | * 26 | * - MUST be an ERC-20 token contract. 27 | * - MUST NOT revert. 28 | */ 29 | function asset() external view returns (address assetTokenAddress); 30 | 31 | /** 32 | * @dev Returns the total amount of the underlying asset that is “managed” by Vault. 33 | * 34 | * - SHOULD include any compounding that occurs from yield. 35 | * - MUST be inclusive of any fees that are charged against assets in the Vault. 36 | * - MUST NOT revert. 37 | */ 38 | function totalAssets() external view returns (uint256 totalManagedAssets); 39 | 40 | /** 41 | * @dev Returns the amount of shares that the Vault would exchange for the amount of assets provided, in an ideal 42 | * scenario where all the conditions are met. 43 | * 44 | * - MUST NOT be inclusive of any fees that are charged against assets in the Vault. 45 | * - MUST NOT show any variations depending on the caller. 46 | * - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. 47 | * - MUST NOT revert. 48 | * 49 | * NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the 50 | * “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and 51 | * from. 52 | */ 53 | function convertToShares(uint256 assets) external view returns (uint256 shares); 54 | 55 | /** 56 | * @dev Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an ideal 57 | * scenario where all the conditions are met. 58 | * 59 | * - MUST NOT be inclusive of any fees that are charged against assets in the Vault. 60 | * - MUST NOT show any variations depending on the caller. 61 | * - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. 62 | * - MUST NOT revert unless due to integer overflow caused by an unreasonably large input. 63 | * 64 | * NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the 65 | * “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and 66 | * from. 67 | */ 68 | function convertToAssets(uint256 shares) external view returns (uint256 assets); 69 | 70 | /** 71 | * @dev Returns the maximum amount of the underlying asset that can be deposited into the Vault for the receiver, 72 | * through a deposit call. 73 | * While deposit of aToken is not affected by aave pool configrations, deposit of the aTokenUnderlying will need to deposit to aave 74 | * so it is affected by current aave pool configuration. 75 | * Reference: https://github.com/aave/aave-v3-core/blob/29ff9b9f89af7cd8255231bc5faf26c3ce0fb7ce/contracts/protocol/libraries/logic/ValidationLogic.sol#L57 76 | * - MUST return a limited value if receiver is subject to some deposit limit. 77 | * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited. 78 | * - MUST NOT revert unless due to integer overflow caused by an unreasonably large input. 79 | */ 80 | function maxDeposit(address receiver) external view returns (uint256 maxAssets); 81 | 82 | /** 83 | * @dev Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given 84 | * current on-chain conditions. 85 | * 86 | * - MUST return as close to and no more than the exact amount of Vault shares that would be minted in a deposit 87 | * call in the same transaction. I.e. deposit should return the same or more shares as previewDeposit if called 88 | * in the same transaction. 89 | * - MUST NOT account for deposit limits like those returned from maxDeposit and should always act as though the 90 | * deposit would be accepted, regardless if the user has enough tokens approved, etc. 91 | * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees. 92 | * - MUST NOT revert. 93 | * 94 | * NOTE: any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage in 95 | * share price or some other type of condition, meaning the depositor will lose assets by depositing. 96 | */ 97 | function previewDeposit(uint256 assets) external view returns (uint256 shares); 98 | 99 | /** 100 | * @dev Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens. 101 | * 102 | * - MUST emit the Deposit event. 103 | * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the 104 | * deposit execution, and are accounted for during deposit. 105 | * - MUST revert if all of assets cannot be deposited (due to deposit limit being reached, slippage, the user not 106 | * approving enough underlying tokens to the Vault contract, etc). 107 | * 108 | * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. 109 | */ 110 | function deposit(uint256 assets, address receiver) external returns (uint256 shares); 111 | 112 | /** 113 | * @dev Returns the maximum amount of the Vault shares that can be minted for the receiver, through a mint call. 114 | * - MUST return a limited value if receiver is subject to some mint limit. 115 | * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted. 116 | * - MUST NOT revert. 117 | */ 118 | function maxMint(address receiver) external view returns (uint256 maxShares); 119 | 120 | /** 121 | * @dev Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given 122 | * current on-chain conditions. 123 | * 124 | * - MUST return as close to and no fewer than the exact amount of assets that would be deposited in a mint call 125 | * in the same transaction. I.e. mint should return the same or fewer assets as previewMint if called in the 126 | * same transaction. 127 | * - MUST NOT account for mint limits like those returned from maxMint and should always act as though the mint 128 | * would be accepted, regardless if the user has enough tokens approved, etc. 129 | * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees. 130 | * - MUST NOT revert. 131 | * 132 | * NOTE: any unfavorable discrepancy between convertToAssets and previewMint SHOULD be considered slippage in 133 | * share price or some other type of condition, meaning the depositor will lose assets by minting. 134 | */ 135 | function previewMint(uint256 shares) external view returns (uint256 assets); 136 | 137 | /** 138 | * @dev Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. 139 | * 140 | * - MUST emit the Deposit event. 141 | * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the mint 142 | * execution, and are accounted for during mint. 143 | * - MUST revert if all of shares cannot be minted (due to deposit limit being reached, slippage, the user not 144 | * approving enough underlying tokens to the Vault contract, etc). 145 | * 146 | * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. 147 | */ 148 | function mint(uint256 shares, address receiver) external returns (uint256 assets); 149 | 150 | /** 151 | * @dev Returns the maximum amount of the underlying asset that can be withdrawn from the owner balance in the 152 | * Vault, through a withdraw call. 153 | * 154 | * - MUST return a limited value if owner is subject to some withdrawal limit or timelock. 155 | * - MUST NOT revert. 156 | */ 157 | function maxWithdraw(address owner) external view returns (uint256 maxAssets); 158 | 159 | /** 160 | * @dev Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, 161 | * given current on-chain conditions. 162 | * 163 | * - MUST return as close to and no fewer than the exact amount of Vault shares that would be burned in a withdraw 164 | * call in the same transaction. I.e. withdraw should return the same or fewer shares as previewWithdraw if 165 | * called 166 | * in the same transaction. 167 | * - MUST NOT account for withdrawal limits like those returned from maxWithdraw and should always act as though 168 | * the withdrawal would be accepted, regardless if the user has enough shares, etc. 169 | * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees. 170 | * - MUST NOT revert. 171 | * 172 | * NOTE: any unfavorable discrepancy between convertToShares and previewWithdraw SHOULD be considered slippage in 173 | * share price or some other type of condition, meaning the depositor will lose assets by depositing. 174 | */ 175 | function previewWithdraw(uint256 assets) external view returns (uint256 shares); 176 | 177 | /** 178 | * @dev Burns shares from owner and sends exactly assets of underlying tokens to receiver. 179 | * 180 | * - MUST emit the Withdraw event. 181 | * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the 182 | * withdraw execution, and are accounted for during withdraw. 183 | * - MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, slippage, the owner 184 | * not having enough shares, etc). 185 | * 186 | * Note that some implementations will require pre-requesting to the Vault before a withdrawal may be performed. 187 | * Those methods should be performed separately. 188 | */ 189 | function withdraw( 190 | uint256 assets, 191 | address receiver, 192 | address owner 193 | ) external returns (uint256 shares); 194 | 195 | /** 196 | * @dev Returns the maximum amount of Vault shares that can be redeemed from the owner balance in the Vault, 197 | * through a redeem call to the aToken underlying. 198 | * While redeem of aToken is not affected by aave pool configrations, redeeming of the aTokenUnderlying will need to redeem from aave 199 | * so it is affected by current aave pool configuration. 200 | * Reference: https://github.com/aave/aave-v3-core/blob/29ff9b9f89af7cd8255231bc5faf26c3ce0fb7ce/contracts/protocol/libraries/logic/ValidationLogic.sol#L87 201 | * - MUST return a limited value if owner is subject to some withdrawal limit or timelock. 202 | * - MUST return balanceOf(owner) if owner is not subject to any withdrawal limit or timelock. 203 | * - MUST NOT revert. 204 | */ 205 | function maxRedeem(address owner) external view returns (uint256 maxShares); 206 | 207 | /** 208 | * @dev Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, 209 | * given current on-chain conditions. 210 | * 211 | * - MUST return as close to and no more than the exact amount of assets that would be withdrawn in a redeem call 212 | * in the same transaction. I.e. redeem should return the same or more assets as previewRedeem if called in the 213 | * same transaction. 214 | * - MUST NOT account for redemption limits like those returned from maxRedeem and should always act as though the 215 | * redemption would be accepted, regardless if the user has enough shares, etc. 216 | * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees. 217 | * - MUST NOT revert. 218 | * 219 | * NOTE: any unfavorable discrepancy between convertToAssets and previewRedeem SHOULD be considered slippage in 220 | * share price or some other type of condition, meaning the depositor will lose assets by redeeming. 221 | */ 222 | function previewRedeem(uint256 shares) external view returns (uint256 assets); 223 | 224 | /** 225 | * @dev Burns exactly shares from owner and sends assets of underlying tokens to receiver. 226 | * 227 | * - MUST emit the Withdraw event. 228 | * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the 229 | * redeem execution, and are accounted for during redeem. 230 | * - MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, slippage, the owner 231 | * not having enough shares, etc). 232 | * 233 | * NOTE: some implementations will require pre-requesting to the Vault before a withdrawal may be performed. 234 | * Those methods should be performed separately. 235 | */ 236 | function redeem( 237 | uint256 shares, 238 | address receiver, 239 | address owner 240 | ) external returns (uint256 assets); 241 | } 242 | -------------------------------------------------------------------------------- /src/interfaces/IInitializableStaticATokenLM.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity ^0.8.10; 3 | 4 | import {IPool} from 'aave-v3-core/contracts/interfaces/IPool.sol'; 5 | import {IAaveIncentivesController} from 'aave-v3-core/contracts/interfaces/IAaveIncentivesController.sol'; 6 | 7 | /** 8 | * @title IInitializableStaticATokenLM 9 | * @notice Interface for the initialize function on StaticATokenLM 10 | * @author Aave 11 | **/ 12 | interface IInitializableStaticATokenLM { 13 | /** 14 | * @dev Emitted when a StaticATokenLM is initialized 15 | * @param aToken The address of the underlying aToken (aWETH) 16 | * @param staticATokenName The name of the Static aToken 17 | * @param staticATokenSymbol The symbol of the Static aToken 18 | **/ 19 | event Initialized(address indexed aToken, string staticATokenName, string staticATokenSymbol); 20 | 21 | /** 22 | * @dev Initializes the StaticATokenLM 23 | * @param aToken The address of the underlying aToken (aWETH) 24 | * @param staticATokenName The name of the Static aToken 25 | * @param staticATokenSymbol The symbol of the Static aToken 26 | */ 27 | function initialize( 28 | address aToken, 29 | string calldata staticATokenName, 30 | string calldata staticATokenSymbol 31 | ) external; 32 | } 33 | -------------------------------------------------------------------------------- /src/interfaces/IStataOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.10; 3 | import {IPool} from 'aave-v3-core/contracts/interfaces/IPool.sol'; 4 | import {IAaveOracle} from 'aave-v3-core/contracts/interfaces/IAaveOracle.sol'; 5 | 6 | interface IStataOracle { 7 | /** 8 | * @return The pool used for fetching the rate on the aggregator oracle 9 | */ 10 | function POOL() external view returns (IPool); 11 | 12 | /** 13 | * @return The aave oracle used for fetching the price of the underlying 14 | */ 15 | function AAVE_ORACLE() external view returns (IAaveOracle); 16 | 17 | /** 18 | * @notice Returns the prices of an asset address 19 | * @param asset The asset address 20 | * @return The prices of the given asset 21 | */ 22 | function getAssetPrice(address asset) external view returns (uint256); 23 | 24 | /** 25 | * @notice Returns a list of prices from a list of assets addresses 26 | * @param assets The list of assets addresses 27 | * @return The prices of the given assets 28 | */ 29 | function getAssetsPrices(address[] calldata assets) external view returns (uint256[] memory); 30 | } 31 | -------------------------------------------------------------------------------- /src/interfaces/IStaticATokenFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity ^0.8.10; 3 | 4 | import {IPool, DataTypes} from 'aave-address-book/AaveV3.sol'; 5 | import {IERC20Metadata} from 'solidity-utils/contracts/oz-common/interfaces/IERC20Metadata.sol'; 6 | import {ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/interfaces/ITransparentProxyFactory.sol'; 7 | import {Ownable} from 'solidity-utils/contracts/oz-common/Ownable.sol'; 8 | 9 | interface IStaticATokenFactory { 10 | /** 11 | * @notice Creates new staticATokens 12 | * @param underlyings the addresses of the underlyings to create. 13 | * @return address[] addresses of the new staticATokens. 14 | */ 15 | function createStaticATokens(address[] memory underlyings) external returns (address[] memory); 16 | 17 | /** 18 | * @notice Returns all tokens deployed via this registry. 19 | * @return address[] list of tokens 20 | */ 21 | function getStaticATokens() external view returns (address[] memory); 22 | 23 | /** 24 | * @notice Returns the staticAToken for a given underlying. 25 | * @param underlying the address of the underlying. 26 | * @return address the staticAToken address. 27 | */ 28 | function getStaticAToken(address underlying) external view returns (address); 29 | } 30 | -------------------------------------------------------------------------------- /src/interfaces/IStaticATokenLM.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity ^0.8.10; 3 | 4 | import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; 5 | import {IPool} from 'aave-v3-core/contracts/interfaces/IPool.sol'; 6 | import {IAaveIncentivesController} from 'aave-v3-core/contracts/interfaces/IAaveIncentivesController.sol'; 7 | import {IInitializableStaticATokenLM} from './IInitializableStaticATokenLM.sol'; 8 | 9 | interface IStaticATokenLM is IInitializableStaticATokenLM { 10 | struct SignatureParams { 11 | uint8 v; 12 | bytes32 r; 13 | bytes32 s; 14 | } 15 | 16 | struct PermitParams { 17 | address owner; 18 | address spender; 19 | uint256 value; 20 | uint256 deadline; 21 | uint8 v; 22 | bytes32 r; 23 | bytes32 s; 24 | } 25 | 26 | struct UserRewardsData { 27 | uint128 rewardsIndexOnLastInteraction; // (in RAYs) 28 | uint128 unclaimedRewards; // (in RAYs) 29 | } 30 | 31 | struct RewardIndexCache { 32 | bool isRegistered; 33 | uint248 lastUpdatedIndex; 34 | } 35 | 36 | event RewardTokenRegistered(address indexed reward, uint256 startIndex); 37 | 38 | /** 39 | * @notice Burns `amount` of static aToken, with receiver receiving the corresponding amount of `ASSET` 40 | * @param shares The amount to withdraw, in static balance of StaticAToken 41 | * @param receiver The address that will receive the amount of `ASSET` withdrawn from the Aave protocol 42 | * @param withdrawFromAave bool 43 | * - `true` for the receiver to get underlying tokens (e.g. USDC) 44 | * - `false` for the receiver to get aTokens (e.g. aUSDC) 45 | * @return amountToBurn: StaticATokens burnt, static balance 46 | * @return amountToWithdraw: underlying/aToken send to `receiver`, dynamic balance 47 | **/ 48 | function redeem( 49 | uint256 shares, 50 | address receiver, 51 | address owner, 52 | bool withdrawFromAave 53 | ) external returns (uint256, uint256); 54 | 55 | /** 56 | * @notice Deposits `ASSET` in the Aave protocol and mints static aTokens to msg.sender 57 | * @param assets The amount of underlying `ASSET` to deposit (e.g. deposit of 100 USDC) 58 | * @param receiver The address that will receive the static aTokens 59 | * @param referralCode Code used to register the integrator originating the operation, for potential rewards. 60 | * 0 if the action is executed directly by the user, without any middle-man 61 | * @param depositToAave bool 62 | * - `true` if the msg.sender comes with underlying tokens (e.g. USDC) 63 | * - `false` if the msg.sender comes already with aTokens (e.g. aUSDC) 64 | * @return uint256 The amount of StaticAToken minted, static balance 65 | **/ 66 | function deposit( 67 | uint256 assets, 68 | address receiver, 69 | uint16 referralCode, 70 | bool depositToAave 71 | ) external returns (uint256); 72 | 73 | /** 74 | * @notice Allows to deposit on Aave via meta-transaction 75 | * https://github.com/ethereum/EIPs/blob/8a34d644aacf0f9f8f00815307fd7dd5da07655f/EIPS/eip-2612.md 76 | * @param depositor Address from which the funds to deposit are going to be pulled 77 | * @param receiver Address that will receive the staticATokens, in the average case, same as the `depositor` 78 | * @param assets The amount to deposit 79 | * @param referralCode Code used to register the integrator originating the operation, for potential rewards. 80 | * 0 if the action is executed directly by the user, without any middle-man 81 | * @param depositToAave bool 82 | * - `true` if the msg.sender comes with underlying tokens (e.g. USDC) 83 | * - `false` if the msg.sender comes already with aTokens (e.g. aUSDC) 84 | * @param deadline The deadline timestamp, type(uint256).max for max deadline 85 | * @param sigParams Signature params: v,r,s 86 | * @return uint256 The amount of StaticAToken minted, static balance 87 | */ 88 | function metaDeposit( 89 | address depositor, 90 | address receiver, 91 | uint256 assets, 92 | uint16 referralCode, 93 | bool depositToAave, 94 | uint256 deadline, 95 | PermitParams calldata permit, 96 | SignatureParams calldata sigParams 97 | ) external returns (uint256); 98 | 99 | /** 100 | * @notice Allows to withdraw from Aave via meta-transaction 101 | * https://github.com/ethereum/EIPs/blob/8a34d644aacf0f9f8f00815307fd7dd5da07655f/EIPS/eip-2612.md 102 | * @param owner Address owning the staticATokens 103 | * @param receiver Address that will receive the underlying withdrawn from Aave 104 | * @param shares The amount of staticAToken to withdraw. If > 0, `assets` needs to be 0 105 | * @param assets The amount of underlying/aToken to withdraw. If > 0, `shares` needs to be 0 106 | * @param withdrawFromAave bool 107 | * - `true` for the receiver to get underlying tokens (e.g. USDC) 108 | * - `false` for the receiver to get aTokens (e.g. aUSDC) 109 | * @param deadline The deadline timestamp, type(uint256).max for max deadline 110 | * @param sigParams Signature params: v,r,s 111 | * @return amountToBurn: StaticATokens burnt, static balance 112 | * @return amountToWithdraw: underlying/aToken send to `receiver`, dynamic balance 113 | */ 114 | function metaWithdraw( 115 | address owner, 116 | address receiver, 117 | uint256 shares, 118 | uint256 assets, 119 | bool withdrawFromAave, 120 | uint256 deadline, 121 | SignatureParams calldata sigParams 122 | ) external returns (uint256, uint256); 123 | 124 | /** 125 | * @notice Returns the Aave liquidity index of the underlying aToken, denominated rate here 126 | * as it can be considered as an ever-increasing exchange rate 127 | * @return The liquidity index 128 | **/ 129 | function rate() external view returns (uint256); 130 | 131 | /** 132 | * @notice Claims rewards from `INCENTIVES_CONTROLLER` and updates internal accounting of rewards. 133 | * @param reward The reward to claim 134 | * @return uint256 Amount collected 135 | */ 136 | function collectAndUpdateRewards(address reward) external returns (uint256); 137 | 138 | /** 139 | * @notice Claim rewards on behalf of a user and send them to a receiver 140 | * @dev Only callable by if sender is onBehalfOf or sender is approved claimer 141 | * @param onBehalfOf The address to claim on behalf of 142 | * @param receiver The address to receive the rewards 143 | * @param rewards The rewards to claim 144 | */ 145 | function claimRewardsOnBehalf( 146 | address onBehalfOf, 147 | address receiver, 148 | address[] memory rewards 149 | ) external; 150 | 151 | /** 152 | * @notice Claim rewards and send them to a receiver 153 | * @param receiver The address to receive the rewards 154 | * @param rewards The rewards to claim 155 | */ 156 | function claimRewards(address receiver, address[] memory rewards) external; 157 | 158 | /** 159 | * @notice Claim rewards 160 | * @param rewards The rewards to claim 161 | */ 162 | function claimRewardsToSelf(address[] memory rewards) external; 163 | 164 | /** 165 | * @notice Get the total claimable rewards of the contract. 166 | * @param reward The reward to claim 167 | * @return uint256 The current balance + pending rewards from the `_incentivesController` 168 | */ 169 | function getTotalClaimableRewards(address reward) external view returns (uint256); 170 | 171 | /** 172 | * @notice Get the total claimable rewards for a user in WAD 173 | * @param user The address of the user 174 | * @param reward The reward to claim 175 | * @return uint256 The claimable amount of rewards in WAD 176 | */ 177 | function getClaimableRewards(address user, address reward) external view returns (uint256); 178 | 179 | /** 180 | * @notice The unclaimed rewards for a user in WAD 181 | * @param user The address of the user 182 | * @param reward The reward to claim 183 | * @return uint256 The unclaimed amount of rewards in WAD 184 | */ 185 | function getUnclaimedRewards(address user, address reward) external view returns (uint256); 186 | 187 | /** 188 | * @notice The underlying asset reward index in RAY 189 | * @param reward The reward to claim 190 | * @return uint256 The underlying asset reward index in RAY 191 | */ 192 | function getCurrentRewardsIndex(address reward) external view returns (uint256); 193 | 194 | /** 195 | * @notice The aToken used inside the 4626 vault. 196 | * @return IERC20 The aToken IERC20. 197 | */ 198 | function aToken() external view returns (IERC20); 199 | 200 | /** 201 | * @notice The IERC20s that are currently rewarded to addresses of the vault via LM on incentivescontroller. 202 | * @return IERC20 The IERC20s of the rewards. 203 | */ 204 | function rewardTokens() external view returns (address[] memory); 205 | 206 | /** 207 | * @notice Fetches all rewardTokens from the incentivecontroller and registers the missing ones. 208 | */ 209 | function refreshRewardTokens() external; 210 | 211 | /** 212 | * @notice Checks if the passed token is a registered reward. 213 | * @return bool signaling if token is a registered reward. 214 | */ 215 | function isRegisteredRewardToken(address reward) external view returns (bool); 216 | } 217 | -------------------------------------------------------------------------------- /tests/SigUtils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.10; 3 | import {IStaticATokenLM} from '../src/interfaces/IStaticATokenLM.sol'; 4 | 5 | library SigUtils { 6 | struct Permit { 7 | address owner; 8 | address spender; 9 | uint256 value; 10 | uint256 nonce; 11 | uint256 deadline; 12 | } 13 | 14 | struct WithdrawPermit { 15 | address owner; 16 | address spender; 17 | uint256 staticAmount; 18 | uint256 dynamicAmount; 19 | bool toUnderlying; 20 | uint256 nonce; 21 | uint256 deadline; 22 | } 23 | 24 | struct DepositPermit { 25 | address owner; 26 | address spender; 27 | uint256 value; 28 | uint16 referralCode; 29 | bool fromUnderlying; 30 | uint256 nonce; 31 | uint256 deadline; 32 | IStaticATokenLM.PermitParams permit; 33 | } 34 | 35 | // computes the hash of a permit 36 | function getStructHash(Permit memory _permit, bytes32 typehash) internal pure returns (bytes32) { 37 | return 38 | keccak256( 39 | abi.encode( 40 | typehash, 41 | _permit.owner, 42 | _permit.spender, 43 | _permit.value, 44 | _permit.nonce, 45 | _permit.deadline 46 | ) 47 | ); 48 | } 49 | 50 | function getWithdrawHash( 51 | WithdrawPermit memory permit, 52 | bytes32 typehash 53 | ) internal pure returns (bytes32) { 54 | return 55 | keccak256( 56 | abi.encode( 57 | typehash, 58 | permit.owner, 59 | permit.spender, 60 | permit.staticAmount, 61 | permit.dynamicAmount, 62 | permit.toUnderlying, 63 | permit.nonce, 64 | permit.deadline 65 | ) 66 | ); 67 | } 68 | 69 | function getDepositHash( 70 | DepositPermit memory permit, 71 | bytes32 typehash 72 | ) internal pure returns (bytes32) { 73 | return 74 | keccak256( 75 | abi.encode( 76 | typehash, 77 | permit.owner, 78 | permit.spender, 79 | permit.value, 80 | permit.referralCode, 81 | permit.fromUnderlying, 82 | permit.nonce, 83 | permit.deadline, 84 | permit.permit 85 | ) 86 | ); 87 | } 88 | 89 | // computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer 90 | function getTypedDataHash( 91 | Permit memory permit, 92 | bytes32 typehash, 93 | bytes32 domainSeparator 94 | ) public pure returns (bytes32) { 95 | return 96 | keccak256(abi.encodePacked('\x19\x01', domainSeparator, getStructHash(permit, typehash))); 97 | } 98 | 99 | function getTypedWithdrawHash( 100 | WithdrawPermit memory permit, 101 | bytes32 typehash, 102 | bytes32 domainSeparator 103 | ) public pure returns (bytes32) { 104 | return 105 | keccak256(abi.encodePacked('\x19\x01', domainSeparator, getWithdrawHash(permit, typehash))); 106 | } 107 | 108 | function getTypedDepositHash( 109 | DepositPermit memory permit, 110 | bytes32 typehash, 111 | bytes32 domainSeparator 112 | ) public pure returns (bytes32) { 113 | return 114 | keccak256(abi.encodePacked('\x19\x01', domainSeparator, getDepositHash(permit, typehash))); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/StataOracle.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.10; 3 | 4 | import 'forge-std/Test.sol'; 5 | import {AaveV3Avalanche, IPool, AaveV3AvalancheAssets} from 'aave-address-book/AaveV3Avalanche.sol'; 6 | import {StaticATokenLM, IERC20, IERC20Metadata, ERC20} from '../src/StaticATokenLM.sol'; 7 | import {StataOracle} from '../src/StataOracle.sol'; 8 | import {RayMathExplicitRounding, Rounding} from '../src/RayMathExplicitRounding.sol'; 9 | import {IStaticATokenLM} from '../src/interfaces/IStaticATokenLM.sol'; 10 | import {BaseTest} from './TestBase.sol'; 11 | 12 | contract StataOracleTest is BaseTest { 13 | using RayMathExplicitRounding for uint256; 14 | 15 | address public constant override UNDERLYING = AaveV3AvalancheAssets.DAIe_UNDERLYING; 16 | address public constant override A_TOKEN = AaveV3AvalancheAssets.DAIe_A_TOKEN; 17 | address public constant EMISSION_ADMIN = 0xCba0B614f13eCdd98B8C0026fcAD11cec8Eb4343; 18 | 19 | IPool public override pool = IPool(AaveV3Avalanche.POOL); 20 | StataOracle public oracle; 21 | 22 | function setUp() public override { 23 | vm.createSelectFork(vm.rpcUrl('avalanche'), 38011791); 24 | super.setUp(); 25 | oracle = new StataOracle(AaveV3Avalanche.POOL_ADDRESSES_PROVIDER); 26 | } 27 | 28 | function test_oraclePrice() public { 29 | uint256 stataPrice = oracle.getAssetPrice(address(staticATokenLM)); 30 | uint256 underlyingPrice = AaveV3Avalanche.ORACLE.getAssetPrice(UNDERLYING); 31 | assertGt(stataPrice, underlyingPrice); 32 | assertEq(stataPrice, (underlyingPrice * staticATokenLM.convertToAssets(1e18)) / 1e18); 33 | } 34 | 35 | function test_error(uint256 shares) public { 36 | vm.assume(shares <= staticATokenLM.maxMint(address(0))); 37 | uint256 pricePerShare = oracle.getAssetPrice(address(staticATokenLM)); 38 | uint256 pricePerAsset = AaveV3Avalanche.ORACLE.getAssetPrice(UNDERLYING); 39 | uint256 assets = staticATokenLM.convertToAssets(shares); 40 | 41 | assertApproxEqAbs( 42 | (pricePerShare * shares) / 1e18, 43 | (pricePerAsset * assets) / 1e18, 44 | (assets / 1e18) + 1 // there can be imprecision of 1 wei, which will accumulate for each asset 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/StaticATokenLM.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.10; 3 | 4 | import 'forge-std/Test.sol'; 5 | import {AToken} from 'aave-v3-core/contracts/protocol/tokenization/AToken.sol'; 6 | import {TransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/TransparentProxyFactory.sol'; 7 | import {AaveV3Avalanche, IPool, AaveV3AvalancheAssets} from 'aave-address-book/AaveV3Avalanche.sol'; 8 | import {DataTypes, ReserveConfiguration} from 'aave-v3-core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; 9 | import {StaticATokenLM, IERC20, IERC20Metadata, ERC20} from '../src/StaticATokenLM.sol'; 10 | import {RayMathExplicitRounding, Rounding} from '../src/RayMathExplicitRounding.sol'; 11 | import {IStaticATokenLM} from '../src/interfaces/IStaticATokenLM.sol'; 12 | import {SigUtils} from './SigUtils.sol'; 13 | import {BaseTest} from './TestBase.sol'; 14 | 15 | contract StaticATokenLMTest is BaseTest { 16 | using RayMathExplicitRounding for uint256; 17 | 18 | address public constant override UNDERLYING = AaveV3AvalancheAssets.WETHe_UNDERLYING; 19 | address public constant override A_TOKEN = AaveV3AvalancheAssets.WETHe_A_TOKEN; 20 | address public constant EMISSION_ADMIN = 0xCba0B614f13eCdd98B8C0026fcAD11cec8Eb4343; 21 | 22 | IPool public override pool = IPool(AaveV3Avalanche.POOL); 23 | 24 | address[] rewardTokens; 25 | 26 | function REWARD_TOKEN() public returns (address) { 27 | return rewardTokens[0]; 28 | } 29 | 30 | function setUp() public override { 31 | vm.createSelectFork(vm.rpcUrl('avalanche'), 38011791); 32 | rewardTokens.push(0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7); 33 | 34 | super.setUp(); 35 | } 36 | 37 | function test_initializeShouldRevert() public { 38 | address impl = factory.STATIC_A_TOKEN_IMPL(); 39 | vm.expectRevert(); 40 | IStaticATokenLM(impl).initialize(0xe50fA9b3c56FfB159cB0FCA61F5c9D750e8128c8, 'hey', 'ho'); 41 | } 42 | 43 | function test_getters() public { 44 | assertEq(staticATokenLM.name(), 'Static Aave Avalanche WETH'); 45 | assertEq(staticATokenLM.symbol(), 'stataAvaWETH'); 46 | 47 | IERC20 aToken = staticATokenLM.aToken(); 48 | assertEq(address(aToken), A_TOKEN); 49 | 50 | address underlyingAddress = address(staticATokenLM.asset()); 51 | assertEq(underlyingAddress, UNDERLYING); 52 | 53 | IERC20Metadata underlying = IERC20Metadata(underlyingAddress); 54 | assertEq(staticATokenLM.decimals(), underlying.decimals()); 55 | 56 | assertEq( 57 | address(staticATokenLM.INCENTIVES_CONTROLLER()), 58 | address(AToken(A_TOKEN).getIncentivesController()) 59 | ); 60 | } 61 | 62 | function test_convertersAndPreviews() public { 63 | uint128 amount = 5 ether; 64 | uint256 shares = staticATokenLM.convertToShares(amount); 65 | assertLe(shares, amount, 'SHARES LOWER'); 66 | assertEq(shares, staticATokenLM.previewDeposit(amount), 'PREVIEW_DEPOSIT'); 67 | assertLe(shares, staticATokenLM.previewWithdraw(amount), 'PREVIEW_WITHDRAW'); 68 | uint256 assets = staticATokenLM.convertToAssets(amount); 69 | assertGe(assets, shares, 'ASSETS GREATER'); 70 | assertLe(assets, staticATokenLM.previewMint(amount), 'PREVIEW_MINT'); 71 | assertEq(assets, staticATokenLM.previewRedeem(amount), 'PREVIEW_REDEEM'); 72 | } 73 | 74 | // Redeem tests 75 | function test_redeem() public { 76 | uint128 amountToDeposit = 5 ether; 77 | _fundUser(amountToDeposit, user); 78 | 79 | _depositAToken(amountToDeposit, user); 80 | 81 | assertEq(staticATokenLM.maxRedeem(user), staticATokenLM.balanceOf(user)); 82 | staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user); 83 | assertEq(staticATokenLM.balanceOf(user), 0); 84 | assertLe(IERC20(UNDERLYING).balanceOf(user), amountToDeposit); 85 | assertApproxEqAbs(IERC20(UNDERLYING).balanceOf(user), amountToDeposit, 1); 86 | } 87 | 88 | function test_redeemAToken() public { 89 | uint128 amountToDeposit = 5 ether; 90 | _fundUser(amountToDeposit, user); 91 | 92 | _depositAToken(amountToDeposit, user); 93 | 94 | assertEq(staticATokenLM.maxRedeem(user), staticATokenLM.balanceOf(user)); 95 | staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user, false); 96 | assertEq(staticATokenLM.balanceOf(user), 0); 97 | assertLe(IERC20(A_TOKEN).balanceOf(user), amountToDeposit); 98 | assertApproxEqAbs(IERC20(A_TOKEN).balanceOf(user), amountToDeposit, 1); 99 | } 100 | 101 | function test_redeemAllowance() public { 102 | uint128 amountToDeposit = 5 ether; 103 | _fundUser(amountToDeposit, user); 104 | 105 | _depositAToken(amountToDeposit, user); 106 | 107 | staticATokenLM.approve(user1, staticATokenLM.maxRedeem(user)); 108 | vm.stopPrank(); 109 | vm.startPrank(user1); 110 | staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user1, user); 111 | assertEq(staticATokenLM.balanceOf(user), 0); 112 | assertLe(IERC20(UNDERLYING).balanceOf(user1), amountToDeposit); 113 | assertApproxEqAbs(IERC20(UNDERLYING).balanceOf(user1), amountToDeposit, 1); 114 | } 115 | 116 | function testFail_redeemOverflowAllowance() public { 117 | uint128 amountToDeposit = 5 ether; 118 | _fundUser(amountToDeposit, user); 119 | 120 | _depositAToken(amountToDeposit, user); 121 | 122 | staticATokenLM.approve(user1, staticATokenLM.maxRedeem(user) / 2); 123 | vm.stopPrank(); 124 | vm.startPrank(user1); 125 | staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user1, user); 126 | assertEq(staticATokenLM.balanceOf(user), 0); 127 | assertEq(IERC20(A_TOKEN).balanceOf(user1), amountToDeposit); 128 | } 129 | 130 | function testFail_redeemAboveBalance() public { 131 | uint128 amountToDeposit = 5 ether; 132 | _fundUser(amountToDeposit, user); 133 | 134 | _depositAToken(amountToDeposit, user); 135 | staticATokenLM.redeem(staticATokenLM.maxRedeem(user) + 1, user, user); 136 | } 137 | 138 | // Withdraw tests 139 | function test_withdraw() public { 140 | uint128 amountToDeposit = 5 ether; 141 | _fundUser(amountToDeposit, user); 142 | 143 | _depositAToken(amountToDeposit, user); 144 | 145 | assertLe(staticATokenLM.maxWithdraw(user), amountToDeposit); 146 | staticATokenLM.withdraw(staticATokenLM.maxWithdraw(user), user, user); 147 | assertEq(staticATokenLM.balanceOf(user), 0); 148 | assertLe(IERC20(UNDERLYING).balanceOf(user), amountToDeposit); 149 | assertApproxEqAbs(IERC20(UNDERLYING).balanceOf(user), amountToDeposit, 1); 150 | } 151 | 152 | function testFail_withdrawAboveBalance() public { 153 | uint128 amountToDeposit = 5 ether; 154 | _fundUser(amountToDeposit, user); 155 | _fundUser(amountToDeposit, user1); 156 | 157 | _depositAToken(amountToDeposit, user); 158 | _depositAToken(amountToDeposit, user1); 159 | 160 | assertEq(staticATokenLM.maxWithdraw(user), amountToDeposit); 161 | staticATokenLM.withdraw(staticATokenLM.maxWithdraw(user) + 1, user, user); 162 | } 163 | 164 | // mint 165 | function test_mint() public { 166 | uint128 amountToDeposit = 5 ether; 167 | _fundUser(amountToDeposit, user); 168 | 169 | IERC20(UNDERLYING).approve(address(staticATokenLM), amountToDeposit); 170 | uint256 shares = 1 ether; 171 | staticATokenLM.mint(shares, user); 172 | assertEq(shares, staticATokenLM.balanceOf(user)); 173 | } 174 | 175 | function testFail_mintAboveBalance() public { 176 | uint128 amountToDeposit = 5 ether; 177 | _fundUser(amountToDeposit, user); 178 | 179 | _underlyingToAToken(amountToDeposit, user); 180 | IERC20(A_TOKEN).approve(address(staticATokenLM), amountToDeposit); 181 | staticATokenLM.mint(amountToDeposit, user); 182 | } 183 | 184 | // test rewards 185 | function test_collectAndUpdateRewards() public { 186 | uint128 amountToDeposit = 5 ether; 187 | _fundUser(amountToDeposit, user); 188 | 189 | _depositAToken(amountToDeposit, user); 190 | 191 | _skipBlocks(60); 192 | assertEq(IERC20(REWARD_TOKEN()).balanceOf(address(staticATokenLM)), 0); 193 | uint256 claimable = staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN()); 194 | staticATokenLM.collectAndUpdateRewards(REWARD_TOKEN()); 195 | assertEq(IERC20(REWARD_TOKEN()).balanceOf(address(staticATokenLM)), claimable); 196 | } 197 | 198 | function test_claimRewardsToSelf() public { 199 | uint128 amountToDeposit = 5 ether; 200 | _fundUser(amountToDeposit, user); 201 | 202 | _depositAToken(amountToDeposit, user); 203 | 204 | _skipBlocks(60); 205 | 206 | uint256 claimable = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN()); 207 | staticATokenLM.claimRewardsToSelf(rewardTokens); 208 | assertEq(IERC20(REWARD_TOKEN()).balanceOf(user), claimable); 209 | assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN()), 0); 210 | } 211 | 212 | function test_claimRewards() public { 213 | uint128 amountToDeposit = 5 ether; 214 | _fundUser(amountToDeposit, user); 215 | 216 | _depositAToken(amountToDeposit, user); 217 | 218 | _skipBlocks(60); 219 | 220 | uint256 claimable = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN()); 221 | staticATokenLM.claimRewards(user, rewardTokens); 222 | assertEq(claimable, IERC20(REWARD_TOKEN()).balanceOf(user)); 223 | assertEq(IERC20(REWARD_TOKEN()).balanceOf(address(staticATokenLM)), 0); 224 | assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN()), 0); 225 | } 226 | 227 | // should fail as user1 is not a valid claimer 228 | function testFail_claimRewardsOnBehalfOf() public { 229 | uint128 amountToDeposit = 5 ether; 230 | _fundUser(amountToDeposit, user); 231 | 232 | _depositAToken(amountToDeposit, user); 233 | 234 | _skipBlocks(60); 235 | 236 | vm.stopPrank(); 237 | vm.startPrank(user1); 238 | 239 | uint256 claimable = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN()); 240 | staticATokenLM.claimRewardsOnBehalf(user, user1, rewardTokens); 241 | } 242 | 243 | function test_depositATokenClaimWithdrawClaim() public { 244 | uint128 amountToDeposit = 5 ether; 245 | _fundUser(amountToDeposit, user); 246 | 247 | // deposit aweth 248 | _depositAToken(amountToDeposit, user); 249 | 250 | // forward time 251 | _skipBlocks(60); 252 | 253 | // claim 254 | assertEq(IERC20(REWARD_TOKEN()).balanceOf(user), 0); 255 | uint256 claimable0 = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN()); 256 | assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN()), claimable0); 257 | assertGt(claimable0, 0); 258 | staticATokenLM.claimRewardsToSelf(rewardTokens); 259 | assertEq(IERC20(REWARD_TOKEN()).balanceOf(user), claimable0); 260 | 261 | // forward time 262 | _skipBlocks(60); 263 | 264 | // redeem 265 | staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user); 266 | uint256 claimable1 = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN()); 267 | assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN()), claimable1); 268 | assertGt(claimable1, 0); 269 | 270 | // claim on behalf of other user 271 | staticATokenLM.claimRewardsToSelf(rewardTokens); 272 | assertEq(IERC20(REWARD_TOKEN()).balanceOf(user), claimable1 + claimable0); 273 | assertEq(staticATokenLM.balanceOf(user), 0); 274 | assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN()), 0); 275 | assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN()), 0); 276 | assertGt(AToken(UNDERLYING).balanceOf(user), 5 ether); 277 | } 278 | 279 | function test_depositWETHClaimWithdrawClaim() public { 280 | uint128 amountToDeposit = 5 ether; 281 | _fundUser(amountToDeposit, user); 282 | 283 | _depositAToken(amountToDeposit, user); 284 | 285 | // forward time 286 | _skipBlocks(60); 287 | 288 | // claim 289 | assertEq(IERC20(REWARD_TOKEN()).balanceOf(user), 0); 290 | uint256 claimable0 = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN()); 291 | assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN()), claimable0); 292 | assertGt(claimable0, 0); 293 | staticATokenLM.claimRewardsToSelf(rewardTokens); 294 | assertEq(IERC20(REWARD_TOKEN()).balanceOf(user), claimable0); 295 | 296 | // forward time 297 | _skipBlocks(60); 298 | 299 | // redeem 300 | staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user); 301 | uint256 claimable1 = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN()); 302 | assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN()), claimable1); 303 | assertGt(claimable1, 0); 304 | 305 | // claim on behalf of other user 306 | staticATokenLM.claimRewardsToSelf(rewardTokens); 307 | assertEq(IERC20(REWARD_TOKEN()).balanceOf(user), claimable1 + claimable0); 308 | assertEq(staticATokenLM.balanceOf(user), 0); 309 | assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN()), 0); 310 | assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN()), 0); 311 | assertGt(AToken(UNDERLYING).balanceOf(user), 5 ether); 312 | } 313 | 314 | function test_transfer() public { 315 | uint128 amountToDeposit = 10 ether; 316 | _fundUser(amountToDeposit, user); 317 | 318 | _depositAToken(amountToDeposit, user); 319 | 320 | // transfer to 2nd user 321 | staticATokenLM.transfer(user1, amountToDeposit / 2); 322 | assertEq(staticATokenLM.getClaimableRewards(user1, REWARD_TOKEN()), 0); 323 | 324 | // forward time 325 | _skipBlocks(60); 326 | 327 | // redeem for both 328 | uint256 claimableUser = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN()); 329 | staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user); 330 | staticATokenLM.claimRewardsToSelf(rewardTokens); 331 | assertEq(IERC20(REWARD_TOKEN()).balanceOf(user), claimableUser); 332 | vm.stopPrank(); 333 | vm.startPrank(user1); 334 | uint256 claimableUser1 = staticATokenLM.getClaimableRewards(user1, REWARD_TOKEN()); 335 | staticATokenLM.redeem(staticATokenLM.maxRedeem(user1), user1, user1); 336 | staticATokenLM.claimRewardsToSelf(rewardTokens); 337 | assertEq(IERC20(REWARD_TOKEN()).balanceOf(user1), claimableUser1); 338 | assertGt(claimableUser1, 0); 339 | 340 | assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN()), 0); 341 | assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN()), 0); 342 | assertEq(staticATokenLM.getClaimableRewards(user1, REWARD_TOKEN()), 0); 343 | } 344 | 345 | // getUnclaimedRewards 346 | function test_getUnclaimedRewards() public { 347 | uint128 amountToDeposit = 5 ether; 348 | _fundUser(amountToDeposit, user); 349 | 350 | uint256 shares = _depositAToken(amountToDeposit, user); 351 | assertEq(staticATokenLM.getUnclaimedRewards(user, REWARD_TOKEN()), 0); 352 | _skipBlocks(1000); 353 | staticATokenLM.redeem(shares, user, user); 354 | assertGt(staticATokenLM.getUnclaimedRewards(user, REWARD_TOKEN()), 0); 355 | } 356 | 357 | /** 358 | * maxDeposit test 359 | */ 360 | function test_maxDeposit_freeze() public { 361 | vm.stopPrank(); 362 | vm.startPrank(address(AaveV3Avalanche.ACL_ADMIN)); 363 | AaveV3Avalanche.POOL_CONFIGURATOR.setReserveFreeze(UNDERLYING, true); 364 | 365 | uint256 max = staticATokenLM.maxDeposit(address(0)); 366 | 367 | assertEq(max, 0); 368 | } 369 | 370 | function test_maxDeposit_paused() public { 371 | vm.stopPrank(); 372 | vm.startPrank(address(AaveV3Avalanche.ACL_ADMIN)); 373 | AaveV3Avalanche.POOL_CONFIGURATOR.setReservePause(UNDERLYING, true); 374 | 375 | uint256 max = staticATokenLM.maxDeposit(address(0)); 376 | 377 | assertEq(max, 0); 378 | } 379 | 380 | function test_maxDeposit_noCap() public { 381 | vm.stopPrank(); 382 | vm.startPrank(address(AaveV3Avalanche.ACL_ADMIN)); 383 | AaveV3Avalanche.POOL_CONFIGURATOR.setSupplyCap(UNDERLYING, 0); 384 | 385 | uint256 maxDeposit = staticATokenLM.maxDeposit(address(0)); 386 | uint256 maxMint = staticATokenLM.maxMint(address(0)); 387 | 388 | assertEq(maxDeposit, type(uint256).max); 389 | assertEq(maxMint, type(uint256).max); 390 | } 391 | 392 | // should be 0 as supply is ~14.04k in forked block 393 | function test_maxDeposit_10kCap() public { 394 | vm.stopPrank(); 395 | vm.startPrank(address(AaveV3Avalanche.ACL_ADMIN)); 396 | AaveV3Avalanche.POOL_CONFIGURATOR.setSupplyCap(UNDERLYING, 10_000); 397 | 398 | uint256 max = staticATokenLM.maxDeposit(address(0)); 399 | assertEq(max, 0); 400 | } 401 | 402 | function test_maxDeposit_50kCap() public { 403 | vm.stopPrank(); 404 | vm.startPrank(address(AaveV3Avalanche.ACL_ADMIN)); 405 | AaveV3Avalanche.POOL_CONFIGURATOR.setSupplyCap(UNDERLYING, 50_000); 406 | 407 | uint256 max = staticATokenLM.maxDeposit(address(0)); 408 | DataTypes.ReserveData memory reserveData = this.pool().getReserveData(UNDERLYING); 409 | assertEq( 410 | max, 411 | 50_000 * 412 | (10 ** IERC20Metadata(UNDERLYING).decimals()) - 413 | (IERC20Metadata(A_TOKEN).totalSupply() + 414 | uint256(reserveData.accruedToTreasury).rayMulRoundUp(staticATokenLM.rate())) 415 | ); 416 | } 417 | 418 | /** 419 | * maxRedeem test 420 | */ 421 | function test_maxRedeem_paused() public { 422 | uint128 amountToDeposit = 5 ether; 423 | _fundUser(amountToDeposit, user); 424 | 425 | _depositAToken(amountToDeposit, user); 426 | 427 | vm.stopPrank(); 428 | vm.startPrank(address(AaveV3Avalanche.ACL_ADMIN)); 429 | AaveV3Avalanche.POOL_CONFIGURATOR.setReservePause(UNDERLYING, true); 430 | 431 | uint256 max = staticATokenLM.maxRedeem(address(user)); 432 | 433 | assertEq(max, 0); 434 | } 435 | 436 | function test_maxRedeem_allAvailable() public { 437 | uint128 amountToDeposit = 5 ether; 438 | _fundUser(amountToDeposit, user); 439 | 440 | _depositAToken(amountToDeposit, user); 441 | 442 | uint256 max = staticATokenLM.maxRedeem(address(user)); 443 | 444 | assertEq(max, staticATokenLM.balanceOf(user)); 445 | } 446 | 447 | function test_maxRedeem_partAvailable() public { 448 | uint128 amountToDeposit = 50 ether; 449 | _fundUser(amountToDeposit, user); 450 | 451 | _depositAToken(amountToDeposit, user); 452 | vm.stopPrank(); 453 | 454 | uint256 maxRedeemBefore = staticATokenLM.previewRedeem(staticATokenLM.maxRedeem(address(user))); 455 | uint256 underlyingBalanceBefore = IERC20Metadata(UNDERLYING).balanceOf(A_TOKEN); 456 | // create rich user 457 | address borrowUser = 0xAD69de0CE8aB50B729d3f798d7bC9ac7b4e79267; 458 | address usdc = AaveV3AvalancheAssets.USDC_UNDERLYING; 459 | vm.startPrank(borrowUser); 460 | deal(usdc, borrowUser, 100_000_000e6); 461 | AaveV3Avalanche.POOL.deposit(usdc, 100_000_000e6, borrowUser, 0); 462 | 463 | // borrow all available 464 | AaveV3Avalanche.POOL.borrow( 465 | UNDERLYING, 466 | underlyingBalanceBefore - (maxRedeemBefore / 2), 467 | 2, 468 | 0, 469 | borrowUser 470 | ); 471 | 472 | uint256 maxRedeemAfter = staticATokenLM.previewRedeem(staticATokenLM.maxRedeem(address(user))); 473 | assertApproxEqAbs(maxRedeemAfter, (maxRedeemBefore / 2), 1); 474 | } 475 | 476 | function test_maxRedeem_nonAvailable() public { 477 | uint128 amountToDeposit = 50 ether; 478 | _fundUser(amountToDeposit, user); 479 | 480 | _depositAToken(amountToDeposit, user); 481 | vm.stopPrank(); 482 | 483 | uint256 underlyingBalanceBefore = IERC20Metadata(UNDERLYING).balanceOf(A_TOKEN); 484 | // create rich user 485 | address borrowUser = 0xAD69de0CE8aB50B729d3f798d7bC9ac7b4e79267; 486 | address usdc = AaveV3AvalancheAssets.USDC_UNDERLYING; 487 | vm.startPrank(borrowUser); 488 | deal(usdc, borrowUser, 100_000_000e6); 489 | AaveV3Avalanche.POOL.deposit(usdc, 100_000_000e6, borrowUser, 0); 490 | 491 | // borrow all available 492 | AaveV3Avalanche.POOL.borrow(UNDERLYING, underlyingBalanceBefore, 2, 0, borrowUser); 493 | 494 | uint256 maxRedeemAfter = staticATokenLM.maxRedeem(address(user)); 495 | assertEq(maxRedeemAfter, 0); 496 | } 497 | 498 | function test_permit() public { 499 | address spender = address(4242); 500 | SigUtils.Permit memory permit = SigUtils.Permit({ 501 | owner: user, 502 | spender: spender, 503 | value: 1 ether, 504 | nonce: staticATokenLM.nonces(user), 505 | deadline: block.timestamp + 1 days 506 | }); 507 | 508 | bytes32 permitDigest = SigUtils.getTypedDataHash( 509 | permit, 510 | staticATokenLM.PERMIT_TYPEHASH(), 511 | staticATokenLM.DOMAIN_SEPARATOR() 512 | ); 513 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); 514 | 515 | staticATokenLM.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); 516 | 517 | assertEq(staticATokenLM.allowance(permit.owner, spender), permit.value); 518 | } 519 | 520 | function test_permit_expired() public { 521 | address spender = address(4242); 522 | SigUtils.Permit memory permit = SigUtils.Permit({ 523 | owner: user, 524 | spender: spender, 525 | value: 1 ether, 526 | nonce: staticATokenLM.nonces(user), 527 | deadline: block.timestamp - 1 days 528 | }); 529 | 530 | bytes32 permitDigest = SigUtils.getTypedDataHash( 531 | permit, 532 | staticATokenLM.PERMIT_TYPEHASH(), 533 | staticATokenLM.DOMAIN_SEPARATOR() 534 | ); 535 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); 536 | 537 | vm.expectRevert('PERMIT_DEADLINE_EXPIRED'); 538 | staticATokenLM.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); 539 | } 540 | 541 | function test_permit_invalidSigner() public { 542 | address spender = address(4242); 543 | SigUtils.Permit memory permit = SigUtils.Permit({ 544 | owner: address(424242), 545 | spender: spender, 546 | value: 1 ether, 547 | nonce: staticATokenLM.nonces(user), 548 | deadline: block.timestamp + 1 days 549 | }); 550 | 551 | bytes32 permitDigest = SigUtils.getTypedDataHash( 552 | permit, 553 | staticATokenLM.PERMIT_TYPEHASH(), 554 | staticATokenLM.DOMAIN_SEPARATOR() 555 | ); 556 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); 557 | 558 | vm.expectRevert('INVALID_SIGNER'); 559 | staticATokenLM.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); 560 | } 561 | 562 | /** 563 | * This test is a bit artificial and tests, what would happen if for some reason `_claimRewards` would no longer revert on insufficient funds. 564 | * Therefore we reduce the claimable amount for the staticAtoken itself. 565 | */ 566 | // function test_claimMoreThanAvailable() public { 567 | // uint128 amountToDeposit = 5 ether; 568 | // _fundUser(amountToDeposit, user); 569 | 570 | // _depositAToken(amountToDeposit, user); 571 | 572 | // _skipBlocks(60); 573 | 574 | // uint256 claimable = staticATokenLM.getClaimableRewards( 575 | // user, 576 | // REWARD_TOKEN() 577 | // ); 578 | 579 | // // transfer out funds 580 | // vm.stopPrank(); 581 | // uint256 emissionAdminBalance = IERC20(REWARD_TOKEN()).balanceOf( 582 | // EMISSION_ADMIN 583 | // ); 584 | // uint256 transferOut = emissionAdminBalance - (claimable / 2); 585 | // vm.startPrank(EMISSION_ADMIN); 586 | // IERC20(REWARD_TOKEN()).approve(address(1234), transferOut); 587 | // IERC20(REWARD_TOKEN()).transfer(address(1234), transferOut); 588 | // vm.stopPrank(); 589 | // vm.startPrank(user); 590 | // // claim 591 | // staticATokenLM.claimRewards(user, rewardTokens); 592 | // // assertEq(claimable, IERC20(REWARD_TOKEN()).balanceOf(user)); 593 | // // assertEq(IERC20(REWARD_TOKEN()).balanceOf(address(staticATokenLM)), 0); 594 | // // assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN()), 0); 595 | // } 596 | } 597 | -------------------------------------------------------------------------------- /tests/StaticATokenMetaTransactions.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.10; 3 | 4 | import 'forge-std/Test.sol'; 5 | import {AToken} from 'aave-v3-core/contracts/protocol/tokenization/AToken.sol'; 6 | import {IERC20WithPermit} from 'solidity-utils/contracts/oz-common/interfaces/IERC20WithPermit.sol'; 7 | import {TransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/TransparentProxyFactory.sol'; 8 | import {AaveV3Avalanche, AaveV3AvalancheAssets, IPool} from 'aave-address-book/AaveV3Avalanche.sol'; 9 | import {StaticATokenLM, IERC20, IERC20Metadata} from '../src/StaticATokenLM.sol'; 10 | import {IStaticATokenLM} from '../src/interfaces/IStaticATokenLM.sol'; 11 | import {SigUtils} from './SigUtils.sol'; 12 | import {BaseTest} from './TestBase.sol'; 13 | 14 | /** 15 | * Testing meta transactions with frax as WETH does not support permit 16 | */ 17 | contract StaticATokenMetaTransactions is BaseTest { 18 | address public constant override UNDERLYING = AaveV3AvalancheAssets.FRAX_UNDERLYING; 19 | address public constant override A_TOKEN = AaveV3AvalancheAssets.FRAX_A_TOKEN; 20 | 21 | IPool public override pool = IPool(AaveV3Avalanche.POOL); 22 | 23 | address[] rewardTokens; 24 | 25 | function REWARD_TOKEN() public returns (address) { 26 | return rewardTokens[0]; 27 | } 28 | 29 | function setUp() public override { 30 | vm.createSelectFork(vm.rpcUrl('avalanche'), 25016463); 31 | rewardTokens.push(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270); 32 | 33 | super.setUp(); 34 | } 35 | 36 | function test_validateDomainSeparator() public { 37 | address[] memory staticATokens = factory.getStaticATokens(); 38 | for (uint256 i = 0; i < staticATokens.length; i++) { 39 | bytes32 separator1 = StaticATokenLM(staticATokens[i]).DOMAIN_SEPARATOR(); 40 | for (uint256 j = 0; j < staticATokens.length; j++) { 41 | if (i != j) { 42 | bytes32 separator2 = StaticATokenLM(staticATokens[j]).DOMAIN_SEPARATOR(); 43 | assertNotEq(separator1, separator2, 'DOMAIN_SEPARATOR_MUST_BE_UNIQUE'); 44 | } 45 | } 46 | } 47 | } 48 | 49 | function test_metaDepositATokenUnderlyingNoPermit() public { 50 | uint128 amountToDeposit = 5 ether; 51 | deal(UNDERLYING, user, amountToDeposit); 52 | IERC20(UNDERLYING).approve(address(staticATokenLM), 1 ether); 53 | IStaticATokenLM.PermitParams memory permitParams; 54 | 55 | // generate combined permit 56 | SigUtils.DepositPermit memory depositPermit = SigUtils.DepositPermit({ 57 | owner: user, 58 | spender: spender, 59 | value: 1 ether, 60 | referralCode: 0, 61 | fromUnderlying: true, 62 | nonce: staticATokenLM.nonces(user), 63 | deadline: block.timestamp + 1 days, 64 | permit: permitParams 65 | }); 66 | bytes32 digest = SigUtils.getTypedDepositHash( 67 | depositPermit, 68 | staticATokenLM.METADEPOSIT_TYPEHASH(), 69 | staticATokenLM.DOMAIN_SEPARATOR() 70 | ); 71 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); 72 | 73 | IStaticATokenLM.SignatureParams memory sigParams = IStaticATokenLM.SignatureParams(v, r, s); 74 | 75 | uint256 previewDeposit = staticATokenLM.previewDeposit(depositPermit.value); 76 | staticATokenLM.metaDeposit( 77 | depositPermit.owner, 78 | depositPermit.spender, 79 | depositPermit.value, 80 | depositPermit.referralCode, 81 | depositPermit.fromUnderlying, 82 | depositPermit.deadline, 83 | permitParams, 84 | sigParams 85 | ); 86 | 87 | assertEq(staticATokenLM.balanceOf(depositPermit.spender), previewDeposit); 88 | } 89 | 90 | function test_metaDepositATokenUnderlying() public { 91 | uint128 amountToDeposit = 5 ether; 92 | deal(UNDERLYING, user, amountToDeposit); 93 | 94 | // permit for aToken deposit 95 | SigUtils.Permit memory permit = SigUtils.Permit({ 96 | owner: user, 97 | spender: address(staticATokenLM), 98 | value: 1 ether, 99 | nonce: IERC20WithPermit(UNDERLYING).nonces(user), 100 | deadline: block.timestamp + 1 days 101 | }); 102 | 103 | bytes32 permitDigest = SigUtils.getTypedDataHash( 104 | permit, 105 | staticATokenLM.PERMIT_TYPEHASH(), 106 | IERC20WithPermit(UNDERLYING).DOMAIN_SEPARATOR() 107 | ); 108 | 109 | (uint8 pV, bytes32 pR, bytes32 pS) = vm.sign(userPrivateKey, permitDigest); 110 | 111 | IStaticATokenLM.PermitParams memory permitParams = IStaticATokenLM.PermitParams( 112 | permit.owner, 113 | permit.spender, 114 | permit.value, 115 | permit.deadline, 116 | pV, 117 | pR, 118 | pS 119 | ); 120 | 121 | // generate combined permit 122 | SigUtils.DepositPermit memory depositPermit = SigUtils.DepositPermit({ 123 | owner: user, 124 | spender: spender, 125 | value: permit.value, 126 | referralCode: 0, 127 | fromUnderlying: true, 128 | nonce: staticATokenLM.nonces(user), 129 | deadline: permit.deadline, 130 | permit: permitParams 131 | }); 132 | (uint8 v, bytes32 r, bytes32 s) = vm.sign( 133 | userPrivateKey, 134 | SigUtils.getTypedDepositHash( 135 | depositPermit, 136 | staticATokenLM.METADEPOSIT_TYPEHASH(), 137 | staticATokenLM.DOMAIN_SEPARATOR() 138 | ) 139 | ); 140 | 141 | IStaticATokenLM.SignatureParams memory sigParams = IStaticATokenLM.SignatureParams(v, r, s); 142 | 143 | uint256 previewDeposit = staticATokenLM.previewDeposit(depositPermit.value); 144 | uint256 shares = staticATokenLM.metaDeposit( 145 | depositPermit.owner, 146 | depositPermit.spender, 147 | depositPermit.value, 148 | depositPermit.referralCode, 149 | depositPermit.fromUnderlying, 150 | depositPermit.deadline, 151 | permitParams, 152 | sigParams 153 | ); 154 | assertEq(shares, previewDeposit); 155 | assertEq(staticATokenLM.balanceOf(depositPermit.spender), previewDeposit); 156 | } 157 | 158 | function test_metaDepositAToken() public { 159 | uint128 amountToDeposit = 5 ether; 160 | _fundUser(amountToDeposit, user); 161 | _underlyingToAToken(amountToDeposit, user); 162 | 163 | // permit for aToken deposit 164 | SigUtils.Permit memory permit = SigUtils.Permit({ 165 | owner: user, 166 | spender: address(staticATokenLM), 167 | value: 1 ether, 168 | nonce: IERC20WithPermit(A_TOKEN).nonces(user), 169 | deadline: block.timestamp + 1 days 170 | }); 171 | 172 | bytes32 permitDigest = SigUtils.getTypedDataHash( 173 | permit, 174 | staticATokenLM.PERMIT_TYPEHASH(), 175 | IERC20WithPermit(A_TOKEN).DOMAIN_SEPARATOR() 176 | ); 177 | 178 | (uint8 pV, bytes32 pR, bytes32 pS) = vm.sign(userPrivateKey, permitDigest); 179 | 180 | IStaticATokenLM.PermitParams memory permitParams = IStaticATokenLM.PermitParams( 181 | permit.owner, 182 | permit.spender, 183 | permit.value, 184 | permit.deadline, 185 | pV, 186 | pR, 187 | pS 188 | ); 189 | 190 | // generate combined permit 191 | SigUtils.DepositPermit memory depositPermit = SigUtils.DepositPermit({ 192 | owner: user, 193 | spender: spender, 194 | value: permit.value, 195 | referralCode: 0, 196 | fromUnderlying: false, 197 | nonce: staticATokenLM.nonces(user), 198 | deadline: permit.deadline, 199 | permit: permitParams 200 | }); 201 | bytes32 digest = SigUtils.getTypedDepositHash( 202 | depositPermit, 203 | staticATokenLM.METADEPOSIT_TYPEHASH(), 204 | staticATokenLM.DOMAIN_SEPARATOR() 205 | ); 206 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); 207 | 208 | IStaticATokenLM.SignatureParams memory sigParams = IStaticATokenLM.SignatureParams(v, r, s); 209 | 210 | uint256 previewDeposit = staticATokenLM.previewDeposit(depositPermit.value); 211 | staticATokenLM.metaDeposit( 212 | depositPermit.owner, 213 | depositPermit.spender, 214 | depositPermit.value, 215 | depositPermit.referralCode, 216 | depositPermit.fromUnderlying, 217 | depositPermit.deadline, 218 | permitParams, 219 | sigParams 220 | ); 221 | 222 | assertEq(staticATokenLM.balanceOf(depositPermit.spender), previewDeposit); 223 | } 224 | 225 | function test_metaWithdraw() public { 226 | uint128 amountToDeposit = 5 ether; 227 | _fundUser(amountToDeposit, user); 228 | 229 | _depositAToken(amountToDeposit, user); 230 | 231 | SigUtils.WithdrawPermit memory permit = SigUtils.WithdrawPermit({ 232 | owner: user, 233 | spender: spender, 234 | staticAmount: 0, 235 | dynamicAmount: 1e18, 236 | toUnderlying: false, 237 | nonce: staticATokenLM.nonces(user), 238 | deadline: block.timestamp + 1 days 239 | }); 240 | bytes32 digest = SigUtils.getTypedWithdrawHash( 241 | permit, 242 | staticATokenLM.METAWITHDRAWAL_TYPEHASH(), 243 | staticATokenLM.DOMAIN_SEPARATOR() 244 | ); 245 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); 246 | 247 | IStaticATokenLM.SignatureParams memory sigParams = IStaticATokenLM.SignatureParams(v, r, s); 248 | 249 | staticATokenLM.metaWithdraw( 250 | permit.owner, 251 | permit.spender, 252 | permit.staticAmount, 253 | permit.dynamicAmount, 254 | permit.toUnderlying, 255 | permit.deadline, 256 | sigParams 257 | ); 258 | 259 | assertEq(IERC20(A_TOKEN).balanceOf(permit.spender), permit.dynamicAmount); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /tests/StaticATokenNoLM.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.10; 3 | 4 | import 'forge-std/Test.sol'; 5 | import {AaveV3Polygon, IPool} from 'aave-address-book/AaveV3Polygon.sol'; 6 | import {AToken} from 'aave-v3-core/contracts/protocol/tokenization/AToken.sol'; 7 | import {TransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/TransparentProxyFactory.sol'; 8 | import {StaticATokenLM, IERC20, IERC20Metadata} from '../src/StaticATokenLM.sol'; 9 | import {BaseTest} from './TestBase.sol'; 10 | 11 | /** 12 | * Testing the static token wrapper on a pool that never had LM enabled (polygon v3 pool at block 33718273) 13 | * This is a slightly different assumption than a pool that doesn't have LM enabled any more as incentivesController.rewardTokens() will have length=0 14 | */ 15 | contract StaticATokenNoLMTest is BaseTest { 16 | address public constant override UNDERLYING = 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619; 17 | address public constant override A_TOKEN = 0xe50fA9b3c56FfB159cB0FCA61F5c9D750e8128c8; 18 | 19 | IPool public override pool = IPool(AaveV3Polygon.POOL); 20 | 21 | address[] rewardTokens; 22 | 23 | function REWARD_TOKEN() public returns (address) { 24 | return rewardTokens[0]; 25 | } 26 | 27 | function setUp() public override { 28 | vm.createSelectFork(vm.rpcUrl('polygon'), 37747173); 29 | rewardTokens.push(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270); 30 | super.setUp(); 31 | } 32 | 33 | // test rewards 34 | function test_collectAndUpdateRewardsWithLMDisabled() public { 35 | uint128 amountToDeposit = 5 ether; 36 | _fundUser(amountToDeposit, user); 37 | 38 | _depositAToken(amountToDeposit, user); 39 | 40 | _skipBlocks(60); 41 | assertEq(IERC20(REWARD_TOKEN()).balanceOf(address(staticATokenLM)), 0); 42 | assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN()), 0); 43 | assertEq(staticATokenLM.collectAndUpdateRewards(REWARD_TOKEN()), 0); 44 | assertEq(IERC20(REWARD_TOKEN()).balanceOf(address(staticATokenLM)), 0); 45 | } 46 | 47 | function test_claimRewardsToSelfWithLMDisabled() public { 48 | uint128 amountToDeposit = 5 ether; 49 | _fundUser(amountToDeposit, user); 50 | 51 | _depositAToken(amountToDeposit, user); 52 | 53 | _skipBlocks(60); 54 | 55 | try staticATokenLM.getClaimableRewards(user, REWARD_TOKEN()) {} catch Error( 56 | string memory reason 57 | ) { 58 | require(keccak256(bytes(reason)) == keccak256(bytes('9'))); 59 | } 60 | 61 | try staticATokenLM.claimRewardsToSelf(rewardTokens) {} catch Error(string memory reason) { 62 | require(keccak256(bytes(reason)) == keccak256(bytes('9'))); 63 | } 64 | assertEq(IERC20(REWARD_TOKEN()).balanceOf(user), 0); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/TestBase.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.10; 3 | 4 | import 'forge-std/Test.sol'; 5 | import {IRewardsController} from 'aave-v3-periphery/contracts/rewards/interfaces/IRewardsController.sol'; 6 | import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; 7 | import {TransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/TransparentProxyFactory.sol'; 8 | import {AaveV3Avalanche, IPool, IPoolAddressesProvider} from 'aave-address-book/AaveV3Avalanche.sol'; 9 | import {StaticATokenFactory} from '../src/StaticATokenFactory.sol'; 10 | import {StaticATokenLM, IERC20, IERC20Metadata, ERC20} from '../src/StaticATokenLM.sol'; 11 | import {IStaticATokenLM} from '../src/interfaces/IStaticATokenLM.sol'; 12 | import {IAToken} from '../src/interfaces/IAToken.sol'; 13 | 14 | import {DeployATokenFactory} from '../scripts/Deploy.s.sol'; 15 | 16 | abstract contract BaseTest is Test { 17 | address constant OWNER = address(1234); 18 | address constant ADMIN = address(2345); 19 | 20 | address public user; 21 | address public user1; 22 | address internal spender; 23 | 24 | uint256 internal userPrivateKey; 25 | uint256 internal spenderPrivateKey; 26 | 27 | StaticATokenLM public staticATokenLM; 28 | address public proxyAdmin; 29 | StaticATokenFactory public factory; 30 | 31 | function UNDERLYING() external virtual returns (address); 32 | 33 | function A_TOKEN() external virtual returns (address); 34 | 35 | function pool() external virtual returns (IPool); 36 | 37 | function setUp() public virtual { 38 | userPrivateKey = 0xA11CE; 39 | spenderPrivateKey = 0xB0B0; 40 | user = address(vm.addr(userPrivateKey)); 41 | user1 = address(vm.addr(2)); 42 | spender = vm.addr(spenderPrivateKey); 43 | 44 | TransparentProxyFactory proxyFactory = new TransparentProxyFactory(); 45 | proxyAdmin = proxyFactory.createProxyAdmin(ADMIN); 46 | factory = DeployATokenFactory._deploy( 47 | proxyFactory, 48 | proxyAdmin, 49 | this.pool(), 50 | IRewardsController(IAToken(this.A_TOKEN()).getIncentivesController()) 51 | ); 52 | 53 | staticATokenLM = StaticATokenLM(factory.getStaticAToken(this.UNDERLYING())); 54 | vm.startPrank(user); 55 | } 56 | 57 | function _fundUser(uint256 amountToDeposit, address targetUser) internal { 58 | deal(this.UNDERLYING(), targetUser, amountToDeposit); 59 | } 60 | 61 | function _skipBlocks(uint128 blocks) internal { 62 | vm.roll(block.number + blocks); 63 | vm.warp(block.timestamp + blocks * 12); // assuming a block is around 12seconds 64 | } 65 | 66 | function _underlyingToAToken(uint256 amountToDeposit, address targetUser) internal { 67 | IERC20(this.UNDERLYING()).approve(address(this.pool()), amountToDeposit); 68 | this.pool().deposit(this.UNDERLYING(), amountToDeposit, targetUser, 0); 69 | } 70 | 71 | function _depositAToken(uint256 amountToDeposit, address targetUser) internal returns (uint256) { 72 | _underlyingToAToken(amountToDeposit, targetUser); 73 | IERC20(this.A_TOKEN()).approve(address(staticATokenLM), amountToDeposit); 74 | return staticATokenLM.deposit(amountToDeposit, targetUser, 10, false); 75 | } 76 | 77 | function testAdmin() public { 78 | vm.stopPrank(); 79 | vm.startPrank(proxyAdmin); 80 | assertEq(TransparentUpgradeableProxy(payable(address(staticATokenLM))).admin(), proxyAdmin); 81 | assertEq(TransparentUpgradeableProxy(payable(address(factory))).admin(), proxyAdmin); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Upgrade.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.16; 3 | 4 | import 'forge-std/Test.sol'; 5 | 6 | import {AaveV3Ethereum} from 'aave-address-book/AaveV3Ethereum.sol'; 7 | import {GovV3Helpers} from 'aave-helpers/GovV3Helpers.sol'; 8 | import {AaveGovernanceV2} from 'aave-address-book/AaveGovernanceV2.sol'; 9 | import {IRewardsController} from 'aave-v3-periphery/contracts/rewards/interfaces/IRewardsController.sol'; 10 | import {UpgradePayload} from '../src/UpgradePayload.sol'; 11 | import {StaticATokenFactory} from '../src/StaticATokenFactory.sol'; 12 | import {StaticATokenLM} from '../src/StaticATokenLM.sol'; 13 | import {ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/interfaces/ITransparentProxyFactory.sol'; 14 | import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; 15 | import {DeployUpgrade} from '../scripts/DeployUpgrade.s.sol'; 16 | 17 | abstract contract UpgradePayloadTest is Test { 18 | string public NETWORK; 19 | uint256 public immutable BLOCK_NUMBER; 20 | 21 | UpgradePayload internal payload; 22 | 23 | constructor(string memory network, uint256 blocknumber) { 24 | NETWORK = network; 25 | BLOCK_NUMBER = blocknumber; 26 | } 27 | 28 | function setUp() public { 29 | vm.createSelectFork(vm.rpcUrl(NETWORK), BLOCK_NUMBER); 30 | payload = _getPayload(); 31 | } 32 | 33 | function _getPayload() internal virtual returns (UpgradePayload); 34 | 35 | function test_upgrade() external { 36 | GovV3Helpers.executePayload(vm, address(payload)); 37 | 38 | address newImpl = payload.FACTORY().STATIC_A_TOKEN_IMPL(); 39 | 40 | // check factory is updated 41 | assertEq(newImpl, payload.NEW_TOKEN_IMPLEMENTATION()); 42 | // check all tokens are updated 43 | address[] memory tokens = payload.FACTORY().getStaticATokens(); 44 | vm.startPrank(address(payload.ADMIN())); 45 | for (uint256 i = 0; i < tokens.length; i++) { 46 | assertEq( 47 | TransparentUpgradeableProxy(payable(tokens[i])).implementation(), 48 | payload.NEW_TOKEN_IMPLEMENTATION() 49 | ); 50 | } 51 | } 52 | 53 | function test_validateDomainSeparator() public { 54 | GovV3Helpers.executePayload(vm, address(payload)); 55 | 56 | address[] memory staticATokens = payload.FACTORY().getStaticATokens(); 57 | for (uint256 i = 0; i < staticATokens.length; i++) { 58 | bytes32 separator1 = StaticATokenLM(staticATokens[i]).DOMAIN_SEPARATOR(); 59 | for (uint256 j = 0; j < staticATokens.length; j++) { 60 | if (i != j) { 61 | bytes32 separator2 = StaticATokenLM(staticATokens[j]).DOMAIN_SEPARATOR(); 62 | assertNotEq(separator1, separator2, 'DOMAIN_SEPARATOR_MUST_BE_UNIQUE'); 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | contract UpgradeMainnetTest is UpgradePayloadTest('mainnet', 19376575) { 70 | function _getPayload() internal virtual override returns (UpgradePayload) { 71 | return DeployUpgrade.deployMainnet(); 72 | } 73 | } 74 | 75 | contract UpgradePolygonTest is UpgradePayloadTest('polygon', 54337710) { 76 | function _getPayload() internal virtual override returns (UpgradePayload) { 77 | return DeployUpgrade.deployPolygon(); 78 | } 79 | } 80 | 81 | contract UpgradeAvalancheTest is UpgradePayloadTest('avalanche', 42590450) { 82 | function _getPayload() internal virtual override returns (UpgradePayload) { 83 | return DeployUpgrade.deployAvalanche(); 84 | } 85 | } 86 | 87 | contract UpgradeArbitrumTest is UpgradePayloadTest('arbitrum', 187970620) { 88 | function _getPayload() internal virtual override returns (UpgradePayload) { 89 | return DeployUpgrade.deployArbitrum(); 90 | } 91 | } 92 | 93 | contract UpgradeOptimismTest is UpgradePayloadTest('optimism', 117104603) { 94 | function _getPayload() internal virtual override returns (UpgradePayload) { 95 | return DeployUpgrade.deployOptimism(); 96 | } 97 | } 98 | 99 | contract UpgradeMetisTest is UpgradePayloadTest('metis', 14812943) { 100 | function _getPayload() internal virtual override returns (UpgradePayload) { 101 | return DeployUpgrade.deployMetis(); 102 | } 103 | } 104 | 105 | contract UpgradeBNBTest is UpgradePayloadTest('bnb', 36989356) { 106 | function _getPayload() internal virtual override returns (UpgradePayload) { 107 | return DeployUpgrade.deployBNB(); 108 | } 109 | } 110 | 111 | contract UpgradeScrollTest is UpgradePayloadTest('scroll', 3921934) { 112 | function _getPayload() internal virtual override returns (UpgradePayload) { 113 | return DeployUpgrade.deployScroll(); 114 | } 115 | } 116 | 117 | contract UpgradeBaseTest is UpgradePayloadTest('base', 11985792) { 118 | function _getPayload() internal virtual override returns (UpgradePayload) { 119 | return DeployUpgrade.deployBase(); 120 | } 121 | } 122 | 123 | contract UpgradeGnosisTest is UpgradePayloadTest('gnosis', 32991586) { 124 | function _getPayload() internal virtual override returns (UpgradePayload) { 125 | return DeployUpgrade.deployGnosis(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /wrapping.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bgd-labs/static-a-token-v3/101f5d977889254ca2d2711b9582b45f832d10a0/wrapping.jpg -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@solidity-parser/parser@^0.16.0": 6 | version "0.16.0" 7 | resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.0.tgz#1fb418c816ca1fc3a1e94b08bcfe623ec4e1add4" 8 | integrity sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q== 9 | dependencies: 10 | antlr4ts "^0.5.0-alpha.4" 11 | 12 | antlr4ts@^0.5.0-alpha.4: 13 | version "0.5.0-alpha.4" 14 | resolved "https://registry.yarnpkg.com/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz#71702865a87478ed0b40c0709f422cf14d51652a" 15 | integrity sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ== 16 | 17 | lru-cache@^6.0.0: 18 | version "6.0.0" 19 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" 20 | integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== 21 | dependencies: 22 | yallist "^4.0.0" 23 | 24 | prettier-plugin-solidity@^1.1.1: 25 | version "1.1.3" 26 | resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.3.tgz#9a35124f578404caf617634a8cab80862d726cba" 27 | integrity sha512-fQ9yucPi2sBbA2U2Xjh6m4isUTJ7S7QLc/XDDsktqqxYfTwdYKJ0EnnywXHwCGAaYbQNK+HIYPL1OemxuMsgeg== 28 | dependencies: 29 | "@solidity-parser/parser" "^0.16.0" 30 | semver "^7.3.8" 31 | solidity-comments-extractor "^0.0.7" 32 | 33 | prettier@^2.8.3: 34 | version "2.8.7" 35 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.7.tgz#bb79fc8729308549d28fe3a98fce73d2c0656450" 36 | integrity sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw== 37 | 38 | semver@^7.3.8: 39 | version "7.3.8" 40 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" 41 | integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== 42 | dependencies: 43 | lru-cache "^6.0.0" 44 | 45 | solidity-comments-extractor@^0.0.7: 46 | version "0.0.7" 47 | resolved "https://registry.yarnpkg.com/solidity-comments-extractor/-/solidity-comments-extractor-0.0.7.tgz#99d8f1361438f84019795d928b931f4e5c39ca19" 48 | integrity sha512-wciNMLg/Irp8OKGrh3S2tfvZiZ0NEyILfcRCXCD4mp7SgK/i9gzLfhY2hY7VMCQJ3kH9UB9BzNdibIVMchzyYw== 49 | 50 | yallist@^4.0.0: 51 | version "4.0.0" 52 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" 53 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 54 | --------------------------------------------------------------------------------