├── .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 |
--------------------------------------------------------------------------------