├── reports └── .empty ├── .solhintignore ├── certora ├── munged │ └── .gitignore ├── harness │ ├── DummyContract.sol │ ├── rewards │ │ ├── RewardToken0.sol │ │ └── RewardToken1.sol │ ├── assets │ │ ├── aTokenUnderlineMock.sol │ │ └── StakeTokenMock.sol │ ├── ERC20A.sol │ ├── ERC20B.sol │ ├── UmbrellaStakeTokenA.sol │ ├── UmbrellaStakeTokenB.sol │ ├── UmbrellaHarness.sol │ ├── UmbrellaStakeTokenHarness.sol │ ├── erc20 │ │ └── ERC20Impl.sol │ ├── DummyERC20Impl.sol │ └── RewardsControllerHarness.sol ├── applyHarness.patch ├── scripts │ ├── run-all-stakeToken.sh │ ├── run-all-umbrella.sh │ ├── run-all-rewards-invariants.sh │ └── run-all-rewards.sh ├── specs │ ├── rewards │ │ ├── sanity.spec │ │ └── mirrors.spec │ ├── umbrella │ │ ├── Pool.spec │ │ ├── invariants.spec │ │ └── setup.spec │ └── stakeToken │ │ ├── base.spec │ │ └── invariants.spec ├── Makefile └── conf │ ├── stakeToken │ ├── invariants.conf │ └── rules.conf │ ├── rewards │ ├── mirrors.conf │ ├── sanity.conf │ ├── single_reward.conf │ ├── double_reward.conf │ ├── single_reward-depth0.conf │ ├── invariants.conf │ └── single_reward-special_config.conf │ └── umbrella │ ├── invariants.conf │ └── Umbrella.conf ├── .prettierignore ├── audits ├── Certora │ ├── Umbrella.pdf │ ├── StakeToken.pdf │ ├── RewardsController.pdf │ └── UmbrellaBatchHelper.pdf ├── Ackee │ └── ackee-blockchain-aave-umbrella-report.pdf └── MixBytes │ └── Aave Umbrella Security Audit Report.pdf ├── assets ├── emission_curve_graph.jpg ├── umbrella_main_banner.jpg ├── umbrella.svg └── operating_conditions.md ├── .gitattributes ├── deploy.sh ├── tests ├── helpers │ ├── utils │ │ └── mocks │ │ │ └── MockERC20.sol │ ├── Rescuable.t.sol │ └── Pause.t.sol ├── umbrella │ ├── utils │ │ └── mocks │ │ │ ├── MockPoolAddressesProvider.sol │ │ │ ├── MockOracle.sol │ │ │ ├── MockAaveOracle.sol │ │ │ └── MockPool.sol │ └── RescuableACL.t.sol ├── rewards │ ├── utils │ │ └── mock │ │ │ ├── MockERC20_18_Decimals.sol │ │ │ └── MockERC20_6_Decimals.sol │ ├── RescuableACL.t.sol │ └── EmissonMath.t.sol ├── stakeToken │ ├── utils │ │ ├── mock │ │ │ ├── MockERC20Permit.sol │ │ │ └── MockRewardsController.sol │ │ └── StakeTestBase.t.sol │ ├── StakeTokenConfig.t.sol │ ├── Rescuable.t.sol │ ├── ERC4626a16z.t.sol │ ├── Slashing.t.sol │ ├── Invariants.t.sol │ ├── Pause.t.sol │ ├── ExchangeRate.t.sol │ ├── ERC20.t.sol │ └── PermitDeposit.t.sol ├── automation │ └── GelatoSlashingRobot.t.sol ├── stewards │ ├── Rescueable.t.sol │ └── utils │ │ └── DeficitOffsetClinicStewardBase.t.sol └── payloads │ └── Rescuable.t.sol ├── src └── contracts │ ├── stakeToken │ ├── interfaces │ │ ├── IUmbrellaStakeToken.sol │ │ ├── IStakeToken.sol │ │ └── IOracleToken.sol │ └── UmbrellaStakeToken.sol │ ├── helpers │ └── interfaces │ │ └── IUniversalToken.sol │ ├── payloads │ ├── EngineFlags.sol │ ├── configEngine │ │ └── IUmbrellaConfigEngine.sol │ ├── UmbrellaExtendedPayload.sol │ └── IUmbrellaEngineStructs.sol │ ├── automation │ ├── GelatoSlashingRobot.sol │ └── interfaces │ │ ├── ISlashingRobot.sol │ │ └── IAutomation.sol │ ├── rewards │ ├── interfaces │ │ └── IRewardsStructs.sol │ └── libraries │ │ └── InternalStructs.sol │ └── stewards │ ├── interfaces │ └── IDeficitOffsetClinicSteward.sol │ └── DeficitOffsetClinicSteward.sol ├── .solhint.json ├── .changeset ├── config.json └── README.md ├── .gitignore ├── .gitmodules ├── .github └── workflows │ ├── test.yml │ ├── comment.yml │ ├── release.yml │ └── certora.yml ├── .editorconfig ├── .prettierrc ├── remappings.txt ├── package.json ├── .env.example ├── Makefile ├── scripts └── deploy │ └── stages │ ├── 2_UmbrellaStakeToken.s.sol │ ├── 6_DeficitOffsetClinicSteward.s.sol │ ├── 1_RewardsController.s.sol │ ├── 5_UmbrellaConfigEngine.s.sol │ ├── 4_Helpers.s.sol │ └── 3_Umbrella.s.sol ├── foundry.toml ├── README.md └── LICENSE /reports/.empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /certora/munged/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | lib 3 | cache 4 | node_modules 5 | -------------------------------------------------------------------------------- /audits/Certora/Umbrella.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aave-dao/aave-umbrella/HEAD/audits/Certora/Umbrella.pdf -------------------------------------------------------------------------------- /audits/Certora/StakeToken.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aave-dao/aave-umbrella/HEAD/audits/Certora/StakeToken.pdf -------------------------------------------------------------------------------- /assets/emission_curve_graph.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aave-dao/aave-umbrella/HEAD/assets/emission_curve_graph.jpg -------------------------------------------------------------------------------- /assets/umbrella_main_banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aave-dao/aave-umbrella/HEAD/assets/umbrella_main_banner.jpg -------------------------------------------------------------------------------- /audits/Certora/RewardsController.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aave-dao/aave-umbrella/HEAD/audits/Certora/RewardsController.pdf -------------------------------------------------------------------------------- /audits/Certora/UmbrellaBatchHelper.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aave-dao/aave-umbrella/HEAD/audits/Certora/UmbrellaBatchHelper.pdf -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.spec linguist-language=Solidity 3 | *.conf linguist-detectable 4 | *.conf linguist-language=JSON5 -------------------------------------------------------------------------------- /audits/Ackee/ackee-blockchain-aave-umbrella-report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aave-dao/aave-umbrella/HEAD/audits/Ackee/ackee-blockchain-aave-umbrella-report.pdf -------------------------------------------------------------------------------- /audits/MixBytes/Aave Umbrella Security Audit Report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aave-dao/aave-umbrella/HEAD/audits/MixBytes/Aave Umbrella Security Audit Report.pdf -------------------------------------------------------------------------------- /certora/harness/DummyContract.sol: -------------------------------------------------------------------------------- 1 | contract DummyContract { 2 | function havoc_all_contracts_dummy() external {} 3 | function havoc_other_contracts() external {} 4 | } 5 | -------------------------------------------------------------------------------- /certora/harness/rewards/RewardToken0.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.10; 3 | import "../erc20/ERC20Impl.sol"; 4 | 5 | contract RewardToken0 is ERC20Impl {} 6 | -------------------------------------------------------------------------------- /certora/harness/rewards/RewardToken1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.10; 3 | import "../erc20/ERC20Impl.sol"; 4 | 5 | contract RewardToken1 is ERC20Impl {} 6 | -------------------------------------------------------------------------------- /certora/harness/assets/aTokenUnderlineMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity ^0.8.10; 3 | import "../erc20/ERC20Impl.sol"; 4 | 5 | contract aTokenUnderlineMock is ERC20Impl {} 6 | -------------------------------------------------------------------------------- /certora/applyHarness.patch: -------------------------------------------------------------------------------- 1 | diff -ruN .gitignore .gitignore 2 | --- .gitignore 1970-01-01 02:00:00.000000000 +0200 3 | +++ .gitignore 2025-01-08 15:45:07.906323479 +0200 4 | @@ -0,0 +1,2 @@ 5 | +* 6 | +!.gitignore 7 | -------------------------------------------------------------------------------- /certora/harness/ERC20A.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity >=0.8.20; 3 | 4 | import { ERC20 } from 'openzeppelin-contracts/contracts/token/ERC20/ERC20.sol'; 5 | 6 | contract ERC20A is ERC20 { 7 | constructor() ERC20("ERC20A","ERC20A") {} 8 | } -------------------------------------------------------------------------------- /certora/harness/ERC20B.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity >=0.8.20; 3 | 4 | import { ERC20 } from 'openzeppelin-contracts/contracts/token/ERC20/ERC20.sol'; 5 | 6 | contract ERC20B is ERC20 { 7 | constructor() ERC20("ERC20B","ERC20B") {} 8 | } -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | # To load the variables in the .env file 2 | source .env 3 | 4 | # To deploy and verify our contract 5 | forge script script/Ghost.s.sol:Deploy --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY -vvvv 6 | -------------------------------------------------------------------------------- /tests/helpers/utils/mocks/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {ERC20} from 'openzeppelin-contracts/contracts/token/ERC20/ERC20.sol'; 5 | 6 | contract MockERC20 is ERC20 { 7 | constructor() ERC20('test', 't') {} 8 | } 9 | -------------------------------------------------------------------------------- /src/contracts/stakeToken/interfaces/IUmbrellaStakeToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {IStakeToken} from './IStakeToken.sol'; 5 | import {IOracleToken} from './IOracleToken.sol'; 6 | 7 | interface IUmbrellaStakeToken is IStakeToken, IOracleToken {} 8 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["error", "^0.8.0"], 5 | "func-visibility": ["warn", { "ignoreConstructors": true }], 6 | "compiler-fixed": false, 7 | "quotes": ["error", "single"], 8 | "indent": [2] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /src/contracts/stakeToken/interfaces/IStakeToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {IERC20Permit} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sol'; 5 | 6 | import {IERC4626StakeToken} from './IERC4626StakeToken.sol'; 7 | 8 | interface IStakeToken is IERC4626StakeToken, IERC20Permit {} 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build and cache 2 | cache/ 3 | out/ 4 | 5 | # general 6 | .env 7 | 8 | # editors 9 | .idea 10 | .vscode 11 | 12 | # Vs code spelling extension 13 | cspell.json 14 | 15 | # well, looks strange to ignore package-lock, but we have only prettier and it's temporary 16 | yarn.lock 17 | node_modules 18 | 19 | # ignore foundry deploy artifacts 20 | broadcast/ 21 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/erc4626-tests"] 5 | path = lib/erc4626-tests 6 | url = https://github.com/a16z/erc4626-tests 7 | [submodule "lib/aave-v3-origin"] 8 | path = lib/aave-v3-origin 9 | url = https://github.com/bgd-labs/aave-v3-origin 10 | branch = v3.3.0 11 | -------------------------------------------------------------------------------- /certora/harness/UmbrellaStakeTokenA.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {UmbrellaStakeTokenHarness, IRewardsController} from './UmbrellaStakeTokenHarness.sol'; 5 | 6 | contract UmbrellaStakeTokenA is UmbrellaStakeTokenHarness { 7 | constructor(IRewardsController rewardsController) UmbrellaStakeTokenHarness (rewardsController) {} 8 | } 9 | -------------------------------------------------------------------------------- /certora/harness/UmbrellaStakeTokenB.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {UmbrellaStakeTokenHarness, IRewardsController} from './UmbrellaStakeTokenHarness.sol'; 5 | 6 | contract UmbrellaStakeTokenB is UmbrellaStakeTokenHarness { 7 | constructor(IRewardsController rewardsController) UmbrellaStakeTokenHarness (rewardsController) {} 8 | } 9 | -------------------------------------------------------------------------------- /tests/umbrella/utils/mocks/MockPoolAddressesProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | contract MockPoolAddressesProvider { 5 | address private immutable _ORACLE; 6 | 7 | constructor(address oracle) { 8 | _ORACLE = oracle; 9 | } 10 | 11 | function getPriceOracle() external view returns (address) { 12 | return _ORACLE; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /certora/scripts/run-all-stakeToken.sh: -------------------------------------------------------------------------------- 1 | #CMN="--compilation_steps_only" 2 | 3 | 4 | echo 5 | echo "1: invariants.conf" 6 | certoraRun $CMN certora/conf/stakeToken/invariants.conf \ 7 | --msg "1. stakeToken::invariants.conf" 8 | 9 | echo 10 | echo "2: rules.conf" 11 | certoraRun $CMN certora/conf/stakeToken/rules.conf \ 12 | --msg "2. stakeToken::rules.conf" 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/umbrella/utils/mocks/MockOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | contract MockOracle { 5 | int256 _price; 6 | 7 | constructor(int256 price) { 8 | _price = price; 9 | } 10 | 11 | function setPrice(int256 price) public { 12 | _price = price; 13 | } 14 | 15 | function latestAnswer() public view returns (int256) { 16 | return _price; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | pull_request: 9 | push: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | 17 | jobs: 18 | test: 19 | uses: bgd-labs/github-workflows/.github/workflows/foundry-test.yml@main 20 | -------------------------------------------------------------------------------- /.github/workflows/comment.yml: -------------------------------------------------------------------------------- 1 | name: PR Comment 2 | 3 | on: 4 | workflow_run: 5 | workflows: [Test] 6 | types: 7 | - completed 8 | 9 | permissions: 10 | actions: read 11 | issues: write 12 | checks: read 13 | statuses: read 14 | pull-requests: write 15 | 16 | jobs: 17 | comment: 18 | uses: bgd-labs/github-workflows/.github/workflows/comment.yml@main 19 | secrets: 20 | READ_ONLY_PAT: ${{ secrets.READ_ONLY_PAT }} 21 | -------------------------------------------------------------------------------- /tests/umbrella/utils/mocks/MockAaveOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | contract MockAaveOracle { 5 | mapping(address reserve => uint256 price) _prices; 6 | 7 | function setAssetPrice(address reserve, uint256 price) external { 8 | _prices[reserve] = price; 9 | } 10 | 11 | function getAssetPrice(address reserve) external view returns (uint256) { 12 | return _prices[reserve]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [{Makefile,**.mk}] 16 | # Use tabs for indentation (Makefiles require tabs) 17 | indent_style = tab 18 | -------------------------------------------------------------------------------- /certora/specs/rewards/sanity.spec: -------------------------------------------------------------------------------- 1 | import "base.spec"; 2 | import "invariant.spec"; 3 | 4 | rule sanity_claimAllRewards() { 5 | address asset; address receiver; 6 | 7 | double_RewardToken_setup(asset); 8 | 9 | env e; 10 | claimAllRewards(e, asset, receiver); 11 | satisfy true; 12 | } 13 | 14 | 15 | rule sanity(method f) filtered {f -> f.contract==currentContract} 16 | { 17 | env e; 18 | calldataarg arg; 19 | f(e, arg); 20 | satisfy true; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /.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 | "files": "*.ts", 15 | "options": { 16 | "printWidth": 100, 17 | "tabWidth": 2, 18 | "useTabs": false, 19 | "singleQuote": true, 20 | "bracketSpacing": false 21 | } 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /tests/rewards/utils/mock/MockERC20_18_Decimals.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {ERC20} from 'openzeppelin-contracts/contracts/token/ERC20/ERC20.sol'; 5 | 6 | contract MockERC20_18_Decimals is ERC20 { 7 | constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} 8 | 9 | function mint(address to, uint value) external { 10 | _mint(to, value); 11 | } 12 | 13 | function burn(address from, uint value) external { 14 | _burn(from, value); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/rewards/utils/mock/MockERC20_6_Decimals.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {ERC20} from 'openzeppelin-contracts/contracts/token/ERC20/ERC20.sol'; 5 | 6 | contract MockERC20_6_Decimals is ERC20 { 7 | constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} 8 | 9 | function mint(address to, uint value) external { 10 | _mint(to, value); 11 | } 12 | 13 | function burn(address from, uint value) external { 14 | _burn(from, value); 15 | } 16 | 17 | function decimals() public pure override returns (uint8) { 18 | return 6; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /certora/harness/UmbrellaHarness.sol: -------------------------------------------------------------------------------- 1 | 2 | import {Umbrella} from 'src/contracts/umbrella/Umbrella.sol'; 3 | import {IPool, DataTypes} from 'aave-v3-origin/contracts/interfaces/IPool.sol'; 4 | import {ReserveConfiguration} from 'aave-v3-origin/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; 5 | 6 | 7 | contract UmbrellaHarness is Umbrella { 8 | using ReserveConfiguration for DataTypes.ReserveConfigurationMap; 9 | 10 | constructor() Umbrella() {} 11 | 12 | function get_is_virtual_active(address reserve) external view returns (bool) { 13 | return POOL().getConfiguration(reserve).getIsVirtualAccActive(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /certora/harness/assets/StakeTokenMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.20; 2 | 3 | /* Location of the erc4626: 4 | /home/nissan/Dropbox/certora/aave/1-UMBRELLA/WORK/lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC4626.sol 5 | */ 6 | import {ERC4626} from "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC4626.sol"; 7 | import {ERC20, IERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; 8 | 9 | 10 | contract StakeTokenMock is ERC4626 { 11 | constructor(IERC20 asset_, string memory name_, string memory symbol_) ERC20("ERC4626Mock", "E4626M") ERC4626 (asset_) {} 12 | } 13 | -------------------------------------------------------------------------------- /src/contracts/helpers/interfaces/IUniversalToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {IStataTokenV2} from 'aave-v3-origin/contracts/extensions/stata-token/interfaces/IStataTokenV2.sol'; 5 | 6 | /** 7 | * @title IUniversalToken 8 | * @notice IUniversalToken is renamed interface of IStataTokenV2, because it includes the interface of a regular IERC20 token and IStataTokenV2. 9 | * This is necessary to avoid confusion in names inside `UmbrellaBatchHelper`, since it allows both `StataTokenV2`, `ERC20Permit` and `ERC20` calls (like transfer, approve, etc). 10 | * @author BGD labs 11 | */ 12 | interface IUniversalToken is IStataTokenV2 { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /tests/stakeToken/utils/mock/MockERC20Permit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {ERC20} from 'openzeppelin-contracts/contracts/token/ERC20/ERC20.sol'; 5 | import {ERC20Permit} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol'; 6 | 7 | contract MockERC20Permit is ERC20, ERC20Permit { 8 | constructor( 9 | string memory name_, 10 | string memory symbol_ 11 | ) ERC20(name_, symbol_) ERC20Permit(name_) {} 12 | 13 | function mint(address to, uint value) external { 14 | _mint(to, value); 15 | } 16 | 17 | function burn(address from, uint value) external { 18 | _burn(from, value); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | forge-std/=lib/forge-std/src/ 2 | solidity-utils/=lib/aave-v3-origin/lib/solidity-utils/src/ 3 | @openzeppelin/contracts-upgradeable/=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/contracts/ 4 | @openzeppelin/contracts/=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/ 5 | openzeppelin-contracts-upgradeable/=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/ 6 | openzeppelin-contracts/=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/ 7 | aave-v3-origin-tests/=lib/aave-v3-origin/tests/ 8 | aave-v3-origin/=lib/aave-v3-origin/src/ 9 | aave-address-book/=lib/aave-helpers/lib/aave-address-book/src/ 10 | aave-helpers/=lib/aave-helpers/src/ 11 | erc4626-tests/=lib/erc4626-tests/ 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | jobs: 15 | release: 16 | name: Release 17 | runs-on: ubuntu-latest 18 | if: github.repository == 'aave-dao/aave-umbrella' 19 | steps: 20 | - name: Checkout Repo 21 | uses: actions/checkout@v4 22 | 23 | - uses: bgd-labs/github-workflows/.github/actions/setup-node@main 24 | 25 | - name: Create Release Pull Request or Publish to npm 26 | id: changesets 27 | uses: changesets/action@v1.4.10 28 | with: 29 | publish: npm run release 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} 33 | -------------------------------------------------------------------------------- /certora/Makefile: -------------------------------------------------------------------------------- 1 | default: help 2 | 3 | PATCH = applyHarness.patch 4 | CONTRACTS_DIR = ../src 5 | MUNGED_DIR = munged 6 | 7 | help: 8 | @echo "usage:" 9 | @echo " make clean: remove all generated files (those ignored by git)" 10 | @echo " make $(MUNGED_DIR): create $(MUNGED_DIR) directory by applying the patch file to $(CONTRACTS_DIR)" 11 | @echo " make record: record a new patch file capturing the differences between $(CONTRACTS_DIR) and $(MUNGED_DIR)" 12 | 13 | munged: $(wildcard $(CONTRACTS_DIR)/*.sol) $(PATCH) 14 | rm -rf $@ 15 | mkdir $@ 16 | cp -r ../lib $@ 17 | cp -r ../src $@ 18 | patch -p0 -d $@ < $(PATCH) 19 | 20 | record: 21 | mkdir tmp 22 | cp -r ../lib tmp 23 | cp -r ../src tmp 24 | diff -ruN tmp $(MUNGED_DIR) | sed 's+tmp/++g' | sed 's+$(MUNGED_DIR)/++g' > $(PATCH) 25 | rm -rf tmp 26 | 27 | clean: 28 | git clean -fdX 29 | touch $(PATCH) 30 | 31 | -------------------------------------------------------------------------------- /src/contracts/payloads/EngineFlags.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | library EngineFlags { 5 | /** 6 | * @dev Magic value to be used as flag to keep unchanged any current configuration. 7 | * Strongly assumes that the value `type(uint256).max - 42` will never be used, which seems reasonable 8 | * 9 | * For now could be used for: 10 | * - `unstakeWindow (inside `UnstakeConfig`) 11 | * - `cooldown` (inside `UnstakeConfig`) 12 | * - `targetLiquidity` (inside `ConfigureStakeAndRewardsConfig`) 13 | * - `rewardConfigs[i].maxEmissionPerSecond` (inside `ConfigureStakeAndRewardsConfig` and `ConfigureRewardsConfig`) 14 | * - `rewardConfigs[i].distributionEnd` (inside `ConfigureStakeAndRewardsConfig` and `ConfigureRewardsConfig`) 15 | */ 16 | uint256 internal constant KEEP_CURRENT = type(uint256).max - 42; 17 | } 18 | -------------------------------------------------------------------------------- /tests/automation/GelatoSlashingRobot.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | import './SlashingRobot.t.sol'; 5 | import {GelatoSlashingRobot} from '../../src/contracts/automation/GelatoSlashingRobot.sol'; 6 | 7 | contract GelatoSlashingRobot_Test is SlashingRobot_Test { 8 | 9 | function setUp() public virtual override { 10 | super.setUp(); 11 | robot = SlashingRobot(address(new GelatoSlashingRobot(address(umbrella), ROBOT_GUARDIAN))); 12 | } 13 | 14 | function _checkAndPerformAutomation() internal virtual override returns (bool) { 15 | (bool shouldRunKeeper, bytes memory encodedPerformData) = robot.checkUpkeep(''); 16 | if (shouldRunKeeper) { 17 | (bool status, ) = address(robot).call(encodedPerformData); 18 | assertTrue(status, 'Perform Upkeep Failed'); 19 | } 20 | return shouldRunKeeper; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aave-dao/aave-umbrella", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "lint": "prettier ./", 6 | "lint:fix": "npm run lint -- --write", 7 | "changeset": "changeset", 8 | "version-packages": "changeset version", 9 | "release": "changeset publish" 10 | }, 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/aave-dao/aave-umbrella.git" 17 | }, 18 | "files": [ 19 | "src" 20 | ], 21 | "keywords": [], 22 | "author": "BGD Labs for Aave", 23 | "license": "BUSL1.1", 24 | "bugs": { 25 | "url": "https://github.com/aave-dao/aave-umbrella/issues" 26 | }, 27 | "homepage": "https://github.com/aave-dao/aave-umbrella#readme", 28 | "devDependencies": { 29 | "@changesets/cli": "^2.28.1", 30 | "prettier": "2.8.7", 31 | "prettier-plugin-solidity": "1.1.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /certora/scripts/run-all-umbrella.sh: -------------------------------------------------------------------------------- 1 | #CMN="--compilation_steps_only" 2 | 3 | 4 | echo 5 | echo "1: invariant.conf" 6 | certoraRun $CMN certora/conf/umbrella/invariants.conf \ 7 | --msg "1. umbrella::invariants.conf" 8 | 9 | echo 10 | echo "2: Umbrella.conf: slashing_cant_DOS_other_functions" 11 | certoraRun $CMN certora/conf/umbrella/Umbrella.conf --rule slashing_cant_DOS_other_functions \ 12 | --msg "2. umbrella::Umbrella.conf::slashing_cant_DOS_other_functions" 13 | 14 | echo 15 | echo "3: Umbrella.conf: slashing_cant_DOS__coverDeficitOffset" 16 | certoraRun $CMN certora/conf/umbrella/Umbrella.conf --rule slashing_cant_DOS__coverDeficitOffset \ 17 | --msg "3. umbrella::Umbrella.conf::slashing_cant_DOS__coverDeficitOffset" 18 | 19 | echo 20 | echo "4: Umbrella.conf: other rules" 21 | certoraRun $CMN certora/conf/umbrella/Umbrella.conf \ 22 | --exclude_rule slashing_cant_DOS_other_functions slashing_cant_DOS__coverDeficitOffset \ 23 | --msg "4. umbrella::Umbrella.conf: other rules" 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /certora/conf/stakeToken/invariants.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "certora/harness/UmbrellaStakeTokenHarness.sol", 4 | "certora/harness/DummyERC20Impl.sol", 5 | ], 6 | "packages": [ 7 | "openzeppelin-contracts=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", 8 | "openzeppelin-contracts-upgradeable=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", 9 | "solidity-utils=lib/aave-v3-origin/lib/solidity-utils/src", 10 | "@openzeppelin/contracts/=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", 11 | "aave-v3-origin/=lib/aave-v3-origin/src", 12 | ], 13 | // "build_cache": true, 14 | "loop_iter": "3", 15 | "optimistic_loop": true, 16 | "optimistic_fallback": true, 17 | "process": "emv", 18 | "rule_sanity": "basic", 19 | "solc": "solc8.27", 20 | "verify": "UmbrellaStakeTokenHarness:certora/specs/stakeToken/invariants.spec", 21 | "msg": "invariants.conf all" 22 | } -------------------------------------------------------------------------------- /.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://eth.llamarpc.com 10 | RPC_AVALANCHE=https://api.avax.network/ext/bc/C/rpc 11 | RPC_OPTIMISM=https://optimism.llamarpc.com 12 | RPC_POLYGON=https://polygon.llamarpc.com 13 | RPC_ARBITRUM=https://arbitrum.llamarpc.com 14 | RPC_FANTOM=https://rpc.ftm.tools 15 | RPC_HARMONY=https://api.harmony.one 16 | RPC_METIS=https://andromeda.metis.io/?owner=1088 17 | RPC_BASE=https://base.llamarpc.com 18 | RPC_ZKEVM=https://zkevm-rpc.com 19 | RPC_GNOSIS=https://rpc.ankr.com/gnosis 20 | RPC_BNB=https://binance.llamarpc.com 21 | RPC_CELO=https://forno.celo.org 22 | 23 | # Etherscan api keys for verification & download utils 24 | ETHERSCAN_API_KEY_MAINNET= 25 | ETHERSCAN_API_KEY_POLYGON= 26 | ETHERSCAN_API_KEY_AVALANCHE= 27 | ETHERSCAN_API_KEY_FANTOM= 28 | ETHERSCAN_API_KEY_OPTIMISM= 29 | ETHERSCAN_API_KEY_ARBITRUM= 30 | ETHERSCAN_API_KEY_BASE= 31 | ETHERSCAN_API_KEY_ZKEVM= 32 | ETHERSCAN_API_KEY_GNOSIS= 33 | ETHERSCAN_API_KEY_BNB= 34 | ETHERSCAN_API_KEY_CELO= 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/contracts/stakeToken/interfaces/IOracleToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | interface IOracleToken { 5 | /** 6 | * @notice Returns the current asset price of the `UmbrellaStakeToken`. 7 | * @dev The price is calculated as `underlyingPrice * exchangeRate`. 8 | * 9 | * This function is not functional immediately after the creation of an `UmbrellaStakeToken`, 10 | * but after the creation of a `SlashingConfig` for this token within `Umbrella`. 11 | * The function will remain operational even after the removal of `SlashingConfig`, 12 | * as the `Umbrella` contract retains information about the last installed oracle. 13 | * 14 | * The function may result in a revert if the asset to shares exchange rate leads to overflow. 15 | * 16 | * This function is intended solely for off-chain calculations and is not a critical component of `Umbrella`. 17 | * It should not be relied upon by other systems as a primary source of price information. 18 | * 19 | * @return Current asset price 20 | */ 21 | function latestAnswer() external view returns (int256); 22 | } 23 | -------------------------------------------------------------------------------- /certora/conf/stakeToken/rules.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "certora/harness/UmbrellaStakeTokenHarness.sol", 4 | "certora/harness/DummyERC20Impl.sol", 5 | ], 6 | "packages": [ 7 | "openzeppelin-contracts=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", 8 | "openzeppelin-contracts-upgradeable=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", 9 | "solidity-utils=lib/aave-v3-origin/lib/solidity-utils/src", 10 | "@openzeppelin/contracts/=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", 11 | "aave-v3-origin/=lib/aave-v3-origin/src", 12 | ], 13 | // "build_cache": true, 14 | "loop_iter": "3", 15 | "optimistic_loop": true, 16 | "optimistic_fallback": true, 17 | "process": "emv", 18 | "rule_sanity": "basic", 19 | // "prover_args": ["-depth 0"], 20 | "smt_timeout": "6000", 21 | "solc": "solc8.27", 22 | "verify": "UmbrellaStakeTokenHarness:certora/specs/stakeToken/rules.spec", 23 | "msg": "rules.conf all" 24 | } -------------------------------------------------------------------------------- /tests/stakeToken/StakeTokenConfig.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {StakeTestBase} from './utils/StakeTestBase.t.sol'; 5 | 6 | contract StakeTokenConfigTests is StakeTestBase { 7 | function test_setCooldown(uint32 cooldown) public { 8 | vm.startPrank(admin); 9 | 10 | stakeToken.setCooldown(cooldown); 11 | 12 | assertEq(stakeToken.getCooldown(), cooldown); 13 | } 14 | 15 | function test_setUnstakeWindow(uint32 unstakeWindow) public { 16 | vm.startPrank(admin); 17 | 18 | stakeToken.setUnstakeWindow(unstakeWindow); 19 | 20 | assertEq(stakeToken.getUnstakeWindow(), unstakeWindow); 21 | } 22 | 23 | function test_decimals() public view { 24 | assertEq(stakeToken.decimals(), 18 + _decimalsOffset()); 25 | } 26 | 27 | function test_transferOwnership(address anyone) public { 28 | vm.assume(anyone != address(0)); 29 | 30 | vm.startPrank(admin); 31 | 32 | stakeToken.transferOwnership(anyone); 33 | 34 | assertEq(stakeToken.owner(), anyone); 35 | } 36 | 37 | function test_renounceOwnership() public { 38 | vm.startPrank(admin); 39 | 40 | stakeToken.renounceOwnership(); 41 | 42 | assertEq(stakeToken.owner(), address(0)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/contracts/automation/GelatoSlashingRobot.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | import {SlashingRobot, IAutomation} from './SlashingRobot.sol'; 5 | 6 | /** 7 | * @title GelatoSlashingRobot 8 | * @author BGD Labs 9 | * @notice Contract to perform automated slashing on umbrella 10 | * The difference from `SlashingRobot` is that on `checkUpkeep`, we return 11 | * the reserve to slash encoded with the function selector 12 | */ 13 | contract GelatoSlashingRobot is SlashingRobot { 14 | /** 15 | * @param umbrella Address of the `umbrella` contract 16 | * @param robotGuardian Address of the robot guardian 17 | */ 18 | constructor(address umbrella, address robotGuardian) SlashingRobot(umbrella, robotGuardian) {} 19 | 20 | /** 21 | * @inheritdoc IAutomation 22 | * @dev run off-chain, checks if reserves should be slashed 23 | * @dev the returned bytes is specific to gelato and is encoded with the function selector. 24 | */ 25 | function checkUpkeep(bytes memory) public view virtual override returns (bool, bytes memory) { 26 | (bool upkeepNeeded, bytes memory encodedReservesToSlash) = super.checkUpkeep(''); 27 | 28 | return (upkeepNeeded, abi.encodeCall(this.performUpkeep, encodedReservesToSlash)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /certora/conf/rewards/mirrors.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "certora/harness/RewardsControllerHarness.sol", 4 | "certora/harness/rewards/RewardToken0.sol", 5 | "certora/harness/rewards/RewardToken1.sol", 6 | "certora/harness/assets/StakeTokenMock.sol", 7 | "certora/harness/assets/aTokenUnderlineMock.sol", 8 | ], 9 | "link": [ 10 | "StakeTokenMock:_asset=aTokenUnderlineMock", 11 | ], 12 | "packages": [ 13 | "openzeppelin-contracts=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", 14 | "openzeppelin-contracts-upgradeable=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", 15 | "solidity-utils=lib/aave-v3-origin/lib/solidity-utils/src", 16 | "@openzeppelin/contracts/=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", 17 | ], 18 | // "build_cache": true, 19 | "loop_iter": "1", 20 | "optimistic_loop": true, 21 | "optimistic_fallback": true, 22 | "optimistic_hashing": true, 23 | "process": "emv", 24 | "solc": "solc8.27", 25 | "verify": "RewardsControllerHarness:certora/specs/rewards/mirrors.spec", 26 | "prover_args": ["-depth 0"], 27 | "smt_timeout": "2000", 28 | "rule_sanity": "basic", 29 | "multi_assert_check" : true, 30 | "msg": "Umbrella-Rewards::mirrors " 31 | } 32 | 33 | -------------------------------------------------------------------------------- /certora/conf/rewards/sanity.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "certora/harness/RewardsControllerHarness.sol", 4 | "certora/harness/rewards/RewardToken0.sol", 5 | "certora/harness/rewards/RewardToken1.sol", 6 | "certora/harness/assets/StakeTokenMock.sol", 7 | "certora/harness/assets/aTokenUnderlineMock.sol", 8 | ], 9 | "link": [ 10 | "StakeTokenMock:_asset=aTokenUnderlineMock", 11 | ], 12 | "packages": [ 13 | "openzeppelin-contracts=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", 14 | "openzeppelin-contracts-upgradeable=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", 15 | "solidity-utils=lib/aave-v3-origin/lib/solidity-utils/src", 16 | "@openzeppelin/contracts/=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", 17 | ], 18 | // "coverage_info": "basic", 19 | // "build_cache": true, 20 | "loop_iter": "2", 21 | "optimistic_loop": true, 22 | "optimistic_fallback": true, 23 | "process": "emv", 24 | "solc": "solc8.27", 25 | "verify": "RewardsControllerHarness:certora/specs/rewards/sanity.spec", 26 | "prover_args": ["-copyLoopUnroll 16 -depth 0"], 27 | "smt_timeout": "2000", 28 | "rule_sanity": "basic", 29 | "multi_assert_check" : true, 30 | "msg": "Umbrella-Rewards::sanity " 31 | } -------------------------------------------------------------------------------- /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 | test :; forge test -vvv 11 | 12 | # Deploy 13 | deploy-ledger :; FOUNDRY_PROFILE=${chain} forge script -vvvvv $(if $(filter zksync,${chain}),--zksync) ${contract} --rpc-url ${chain} $(if ${dry},--sender 0x25F2226B597E8F9514B3F68F00f494cF4f286491 -vvvv, --ledger --mnemonic-indexes ${MNEMONIC_INDEX} --sender ${LEDGER_SENDER} --verify -vvvv --slow --broadcast) 14 | deploy-pk :; FOUNDRY_PROFILE=${chain} forge script $(if $(filter zksync,${chain}),--zksync) ${contract} --rpc-url ${chain} $(if ${dry},--sender 0x25F2226B597E8F9514B3F68F00f494cF4f286491 -vvvv, --private-key ${PRIVATE_KEY} --verify -vvvv --slow --broadcast) 15 | 16 | # Utilities 17 | download :; cast etherscan-source --chain ${chain} -d src/etherscan/${chain}_${address} ${address} 18 | git-diff : 19 | @mkdir -p diffs 20 | @npx prettier ${before} ${after} --write 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 | 23 | coverage-summary :; forge coverage --fuzz-runs 1 --report summary --no-match-coverage "(scripts|tests|deployments|mocks)" 24 | coverage-lcov :; forge coverage --fuzz-runs 1 --report lcov --no-match-coverage "(scripts|tests|deployments|mocks)" 25 | -------------------------------------------------------------------------------- /certora/harness/UmbrellaStakeTokenHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {UmbrellaStakeToken} from 'src/contracts/stakeToken/UmbrellaStakeToken.sol'; 5 | import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; 6 | import {ECDSA} from 'openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol'; 7 | import {IRewardsController} from 'src/contracts/rewards/interfaces/IRewardsController.sol'; 8 | //import {IRewardsController} from 'src/contracts/stakeToken/interfaces/IRewardsController.sol'; 9 | 10 | contract UmbrellaStakeTokenHarness is UmbrellaStakeToken { 11 | constructor(IRewardsController rewardsController) UmbrellaStakeToken (rewardsController) {} 12 | 13 | // Returns amount of the cooldown initiated by the user. 14 | function cooldownAmount(address user) public view returns (uint192) { 15 | return getStakerCooldown(user).amount; 16 | } 17 | 18 | // Returns timestamp of the end-of-cooldown-period initiated by the user. 19 | function cooldownEndOfCooldown(address user) public view returns (uint32) { 20 | return getStakerCooldown(user).endOfCooldown; 21 | } 22 | 23 | function cooldownWithdrawalWindow(address user) public view returns (uint32) { 24 | return getStakerCooldown(user).withdrawalWindow; 25 | } 26 | 27 | function get_maxSlashable() external view returns (uint256) { 28 | return _getMaxSlashableAssets(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /certora/conf/rewards/single_reward.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "certora/harness/RewardsControllerHarness.sol", 4 | "certora/harness/rewards/RewardToken0.sol", 5 | "certora/harness/rewards/RewardToken1.sol", 6 | "certora/harness/assets/StakeTokenMock.sol", 7 | "certora/harness/assets/aTokenUnderlineMock.sol", 8 | ], 9 | "link": [ 10 | "StakeTokenMock:_asset=aTokenUnderlineMock", 11 | ], 12 | "packages": [ 13 | "openzeppelin-contracts=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", 14 | "openzeppelin-contracts-upgradeable=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", 15 | "solidity-utils=lib/aave-v3-origin/lib/solidity-utils/src", 16 | "@openzeppelin/contracts/=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", 17 | ], 18 | // "build_cache": true, 19 | "loop_iter": "2", 20 | "optimistic_loop": true, 21 | "optimistic_fallback": true, 22 | "optimistic_hashing": true, 23 | "process": "emv", 24 | "solc": "solc8.27", 25 | "verify": "RewardsControllerHarness:certora/specs/rewards/single_reward.spec", 26 | "prover_args": ["-treeViewLiveStats false -destructiveOptimizations twostage"], 27 | "smt_timeout": "6000", 28 | "rule_sanity": "basic", 29 | // "multi_assert_check" : true, 30 | "msg": "Umbrella-Rewards::single_reward.conf" 31 | } -------------------------------------------------------------------------------- /certora/conf/rewards/double_reward.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "certora/harness/RewardsControllerHarness.sol", 4 | "certora/harness/rewards/RewardToken0.sol", 5 | "certora/harness/rewards/RewardToken1.sol", 6 | "certora/harness/assets/StakeTokenMock.sol", 7 | "certora/harness/assets/aTokenUnderlineMock.sol", 8 | ], 9 | "link": [ 10 | "StakeTokenMock:_asset=aTokenUnderlineMock", 11 | ], 12 | "packages": [ 13 | "openzeppelin-contracts=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", 14 | "openzeppelin-contracts-upgradeable=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", 15 | "solidity-utils=lib/aave-v3-origin/lib/solidity-utils/src", 16 | "@openzeppelin/contracts/=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", 17 | ], 18 | // "build_cache": true, 19 | "loop_iter": "2", 20 | "optimistic_loop": true, 21 | "optimistic_fallback": true, 22 | "optimistic_hashing": true, 23 | "process": "emv", 24 | "solc": "solc8.27", 25 | "verify": "RewardsControllerHarness:certora/specs/rewards/double_reward.spec", 26 | "prover_args": ["-depth 0 -treeViewLiveStats false -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on"], 27 | "smt_timeout": "6000", 28 | "rule_sanity": "basic", 29 | // "multi_assert_check" : true, 30 | "msg": "Umbrella-Rewards::double_reward.conf" 31 | } -------------------------------------------------------------------------------- /certora/conf/rewards/single_reward-depth0.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "certora/harness/RewardsControllerHarness.sol", 4 | "certora/harness/rewards/RewardToken0.sol", 5 | "certora/harness/rewards/RewardToken1.sol", 6 | "certora/harness/assets/StakeTokenMock.sol", 7 | "certora/harness/assets/aTokenUnderlineMock.sol", 8 | ], 9 | "link": [ 10 | "StakeTokenMock:_asset=aTokenUnderlineMock", 11 | ], 12 | "packages": [ 13 | "openzeppelin-contracts=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", 14 | "openzeppelin-contracts-upgradeable=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", 15 | "solidity-utils=lib/aave-v3-origin/lib/solidity-utils/src", 16 | "@openzeppelin/contracts/=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", 17 | ], 18 | // "build_cache": true, 19 | "loop_iter": "2", 20 | "optimistic_loop": true, 21 | "optimistic_fallback": true, 22 | "optimistic_hashing": true, 23 | "process": "emv", 24 | "solc": "solc8.27", 25 | "verify": "RewardsControllerHarness:certora/specs/rewards/single_reward.spec", 26 | // "prover_args": ["-depth 0 -treeViewLiveStats false -destructiveOptimizations twostage"], 27 | "prover_args": ["-depth 0 -treeViewLiveStats false -destructiveOptimizations disable"], 28 | "smt_timeout": "6000", 29 | "rule_sanity": "basic", 30 | // "multi_assert_check" : true, 31 | "msg": "Umbrella-Rewards::single_reward.conf" 32 | } -------------------------------------------------------------------------------- /certora/conf/rewards/invariants.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "certora/harness/RewardsControllerHarness.sol", 4 | "certora/harness/rewards/RewardToken0.sol", 5 | "certora/harness/rewards/RewardToken1.sol", 6 | "certora/harness/assets/StakeTokenMock.sol", 7 | "certora/harness/assets/aTokenUnderlineMock.sol", 8 | ], 9 | "link": [ 10 | "StakeTokenMock:_asset=aTokenUnderlineMock", 11 | ], 12 | "packages": [ 13 | "openzeppelin-contracts=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", 14 | "openzeppelin-contracts-upgradeable=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", 15 | "solidity-utils=lib/aave-v3-origin/lib/solidity-utils/src", 16 | "@openzeppelin/contracts/=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", 17 | ], 18 | // "build_cache": true, 19 | "loop_iter": "3", 20 | "optimistic_loop": true, 21 | "optimistic_fallback": true, 22 | "optimistic_hashing": true, 23 | "process": "emv", 24 | "solc": "solc8.27", 25 | "verify": "RewardsControllerHarness:certora/specs/rewards/invariants.spec", 26 | "prover_args": ["-depth 0 -treeViewLiveStats false -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on"], 27 | // "prover_args": ["-treeViewLiveStats false -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on"], 28 | "smt_timeout": "8000", 29 | "rule_sanity": "basic", 30 | "multi_assert_check" : true, 31 | "msg": "Umbrella-Rewards::invariants " 32 | } -------------------------------------------------------------------------------- /tests/stakeToken/utils/mock/MockRewardsController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {IERC20Metadata} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; 5 | 6 | import {IRewardsStructs} from '../../../../src/contracts/rewards/interfaces/IRewardsStructs.sol'; 7 | import {IERC20} from 'forge-std/interfaces/IERC20.sol'; 8 | 9 | contract MockRewardsController is IRewardsStructs { 10 | mapping(address => bool) isTokenRegistered; 11 | 12 | address public lastUser; 13 | uint256 public lastUserBalance; 14 | 15 | uint256 public lastTotalSupply; 16 | uint256 public lastTotalAssets; 17 | 18 | error AssetNotInitialized(address asset); 19 | 20 | function handleAction( 21 | uint256 totalSupply, 22 | uint256 totalAssets, 23 | address user, 24 | uint256 userBalance 25 | ) external { 26 | lastUser = user; 27 | lastUserBalance = userBalance; 28 | 29 | lastTotalSupply = totalSupply; 30 | lastTotalAssets = totalAssets; 31 | } 32 | 33 | function registerToken(address stakeToken) external { 34 | isTokenRegistered[stakeToken] = true; 35 | } 36 | 37 | function getAssetData( 38 | address stakeToken 39 | ) external view returns (IRewardsStructs.AssetDataExternal memory) { 40 | if (isTokenRegistered[stakeToken]) { 41 | return 42 | IRewardsStructs.AssetDataExternal({ 43 | targetLiquidity: 1_000 * IERC20Metadata(stakeToken).decimals(), 44 | lastUpdateTimestamp: block.timestamp 45 | }); 46 | } 47 | 48 | return IRewardsStructs.AssetDataExternal({targetLiquidity: 0, lastUpdateTimestamp: 0}); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /scripts/deploy/stages/2_UmbrellaStakeToken.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | import {Create2Utils} from 'solidity-utils/contracts/utils/ScriptUtils.sol'; 5 | 6 | import {UmbrellaStakeToken} from '../../../src/contracts/stakeToken/UmbrellaStakeToken.sol'; 7 | import {IRewardsController} from '../../../src/contracts/rewards/interfaces/IRewardsController.sol'; 8 | 9 | import {RewardsControllerScripts} from './1_RewardsController.s.sol'; 10 | 11 | library UmbrellaStakeTokenScripts { 12 | error ProxyNotExist(); 13 | 14 | function deployUmbrellaStakeTokenImpl( 15 | address transparentProxyFactory, 16 | address executor 17 | ) internal returns (address) { 18 | address rewardsController = RewardsControllerScripts.predictRewardsControllerProxy( 19 | transparentProxyFactory, 20 | executor 21 | ); 22 | 23 | require(rewardsController.code.length != 0, ProxyNotExist()); 24 | 25 | return 26 | Create2Utils.create2Deploy( 27 | 'v1', 28 | type(UmbrellaStakeToken).creationCode, 29 | abi.encode(IRewardsController(rewardsController)) 30 | ); 31 | } 32 | 33 | function predictUmbrellaStakeTokenImpl( 34 | address transparentProxyFactory, 35 | address executor 36 | ) internal view returns (address) { 37 | address rewardsController = RewardsControllerScripts.predictRewardsControllerProxy( 38 | transparentProxyFactory, 39 | executor 40 | ); 41 | 42 | return 43 | Create2Utils.computeCreate2Address( 44 | 'v1', 45 | type(UmbrellaStakeToken).creationCode, 46 | abi.encode(IRewardsController(rewardsController)) 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /certora/conf/umbrella/invariants.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "certora/harness/UmbrellaHarness.sol", 4 | "certora/harness/UmbrellaStakeTokenA.sol", 5 | "certora/harness/ERC20A.sol", 6 | "certora/harness/ERC20B.sol", 7 | "src/contracts/rewards/RewardsController.sol", 8 | ], 9 | "packages": [ 10 | "solidity-utils=lib/aave-v3-origin/lib/solidity-utils/src", 11 | "@openzeppelin/contracts-upgradeable=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/contracts", 12 | "@openzeppelin/contracts=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", 13 | "openzeppelin-contracts-upgradeable=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", 14 | "openzeppelin-contracts=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", 15 | "aave-v3-origin-tests=lib/aave-v3-origin/tests", 16 | "aave-v3-origin=lib/aave-v3-origin/src", 17 | ], 18 | "auto_dispatcher": true, 19 | // "build_cache": true, 20 | "prover_args":[ 21 | "-copyLoopUnroll 8", 22 | ], 23 | "optimistic_loop": true, 24 | "loop_iter": "2", 25 | "optimistic_fallback": true, 26 | "optimistic_hashing": true, 27 | "hashing_length_bound": "320", 28 | "process": "emv", 29 | "solc": "solc8.27", 30 | "solc_evm_version": "shanghai", 31 | "verify": "UmbrellaHarness:certora/specs/umbrella/invariants.spec", 32 | "parametric_contracts":["UmbrellaHarness"], 33 | "smt_timeout": "1000", 34 | "rule_sanity": "basic", 35 | "multi_assert_check" : false, 36 | "msg": "Umbrella::invariants" 37 | } 38 | -------------------------------------------------------------------------------- /tests/stewards/Rescueable.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {IRescuable} from 'solidity-utils/contracts/utils/interfaces/IRescuable.sol'; 5 | 6 | import {DeficitOffsetClinicStewardBase} from './utils/DeficitOffsetClinicStewardBase.t.sol'; 7 | 8 | contract RescuableACLTest is DeficitOffsetClinicStewardBase { 9 | function test_rescue() public { 10 | deal(address(underlying6Decimals), address(clinicSteward), 1 ether); 11 | 12 | vm.startPrank(defaultAdmin); 13 | 14 | clinicSteward.emergencyTokenTransfer(address(underlying6Decimals), collector, 0.5 ether); 15 | 16 | assertEq(underlying6Decimals.balanceOf(address(clinicSteward)), 0.5 ether); 17 | assertEq(underlying6Decimals.balanceOf(collector), 0.5 ether); 18 | } 19 | 20 | function test_rescueEther() public { 21 | deal(address(clinicSteward), 1 ether); 22 | 23 | vm.startPrank(defaultAdmin); 24 | 25 | clinicSteward.emergencyEtherTransfer(collector, 0.5 ether); 26 | 27 | assertEq(collector.balance, 0.5 ether); 28 | assertEq(address(clinicSteward).balance, 0.5 ether); 29 | } 30 | 31 | function test_rescueFromNotAdmin(address anyone) public { 32 | vm.assume(anyone != defaultAdmin); 33 | 34 | deal(address(underlying6Decimals), address(clinicSteward), 1 ether); 35 | deal(address(clinicSteward), 1 ether); 36 | 37 | vm.startPrank(anyone); 38 | 39 | vm.expectRevert(abi.encodeWithSelector(IRescuable.OnlyRescueGuardian.selector)); 40 | clinicSteward.emergencyTokenTransfer(address(underlying6Decimals), collector, 1 ether); 41 | 42 | vm.expectRevert(abi.encodeWithSelector(IRescuable.OnlyRescueGuardian.selector)); 43 | clinicSteward.emergencyEtherTransfer(collector, 1 ether); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/contracts/stakeToken/UmbrellaStakeToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; 5 | 6 | import {IRewardsController} from '../rewards/interfaces/IRewardsController.sol'; 7 | import {IUmbrellaConfiguration} from '../umbrella/interfaces/IUmbrellaConfiguration.sol'; 8 | 9 | import {IOracleToken} from './interfaces/IOracleToken.sol'; 10 | import {StakeToken} from './StakeToken.sol'; 11 | 12 | contract UmbrellaStakeToken is StakeToken, IOracleToken { 13 | constructor(IRewardsController rewardsController) StakeToken(rewardsController) { 14 | _disableInitializers(); 15 | } 16 | 17 | function initialize( 18 | IERC20 stakedToken, 19 | string calldata name, 20 | string calldata symbol, 21 | address owner, 22 | uint256 cooldown_, 23 | uint256 unstakeWindow_ 24 | ) external override initializer { 25 | __ERC20_init(name, symbol); 26 | __ERC20Permit_init(name); 27 | 28 | __Pausable_init(); 29 | 30 | __Ownable_init(owner); 31 | 32 | __ERC4626StakeTokenUpgradeable_init(stakedToken, cooldown_, unstakeWindow_); 33 | } 34 | 35 | /// @inheritdoc IOracleToken 36 | function latestAnswer() external view returns (int256) { 37 | // The `underlyingPrice` is obtained from an oracle located in `Umbrella`, 38 | // and the `StakeToken`'s `Owner` is always `Umbrella`, ensuring the call is routed through it. 39 | uint256 underlyingPrice = uint256( 40 | IUmbrellaConfiguration(owner()).latestUnderlyingAnswer(address(this)) 41 | ); 42 | 43 | // price of `StakeToken` should be always less or equal than price of underlying 44 | return int256((underlyingPrice * 1e18) / convertToShares(1e18)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /certora/conf/rewards/single_reward-special_config.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "certora/harness/RewardsControllerHarness.sol", 4 | "certora/harness/rewards/RewardToken0.sol", 5 | "certora/harness/rewards/RewardToken1.sol", 6 | "certora/harness/assets/StakeTokenMock.sol", 7 | "certora/harness/assets/aTokenUnderlineMock.sol", 8 | ], 9 | "link": [ 10 | "StakeTokenMock:_asset=aTokenUnderlineMock", 11 | ], 12 | "packages": [ 13 | "openzeppelin-contracts=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", 14 | "openzeppelin-contracts-upgradeable=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", 15 | "solidity-utils=lib/aave-v3-origin/lib/solidity-utils/src", 16 | "@openzeppelin/contracts/=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", 17 | ], 18 | // "build_cache": true, 19 | "loop_iter": "1", 20 | "optimistic_loop": true, 21 | "optimistic_fallback": true, 22 | "optimistic_hashing": true, 23 | "process": "emv", 24 | "solc": "solc8.27", 25 | "verify": "RewardsControllerHarness:certora/specs/rewards/single_reward.spec", 26 | "prover_args": [ 27 | " -destructiveOptimizations twostage \ 28 | -backendStrategy singleRace \ 29 | -smt_useLIA false -smt_useNIA true \ 30 | -depth 0 \ 31 | -s [z3:def{randomSeed=1},z3:def{randomSeed=2},z3:def{randomSeed=3},z3:def{randomSeed=4},z3:def{randomSeed=5},z3:def{randomSeed=6},z3:def{randomSeed=7},z3:def{randomSeed=8},z3:def{randomSeed=9},z3:def{randomSeed=10}]" 32 | ], 33 | "smt_timeout": "6000", 34 | "rule_sanity": "basic", 35 | "msg": "Umbrella-Rewards::single_reward-special_config.conf" 36 | } -------------------------------------------------------------------------------- /certora/harness/erc20/ERC20Impl.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity ^0.8.10; 3 | 4 | contract ERC20Impl { 5 | uint256 t; 6 | mapping (address => uint256) b; 7 | mapping (address => mapping (address => uint256)) a; 8 | 9 | string public name; 10 | string public symbol; 11 | uint public decimals; 12 | 13 | function myAddress() public returns (address) { 14 | return address(this); 15 | } 16 | 17 | function add(uint a, uint b) internal pure returns (uint256) { 18 | uint c = a +b; 19 | require (c >= a); 20 | return c; 21 | } 22 | function sub(uint a, uint b) internal pure returns (uint256) { 23 | require (a>=b); 24 | return a-b; 25 | } 26 | 27 | function totalSupply() external view returns (uint256) { 28 | return t; 29 | } 30 | function balanceOf(address account) external view returns (uint256) { 31 | return b[account]; 32 | } 33 | function transfer(address recipient, uint256 amount) external returns (bool) { 34 | b[msg.sender] = sub(b[msg.sender], amount); 35 | b[recipient] = add(b[recipient], amount); 36 | return true; 37 | } 38 | function allowance(address owner, address spender) external view returns (uint256) { 39 | return a[owner][spender]; 40 | } 41 | function approve(address spender, uint256 amount) external returns (bool) { 42 | a[msg.sender][spender] = amount; 43 | return true; 44 | } 45 | 46 | function transferFrom( 47 | address sender, 48 | address recipient, 49 | uint256 amount 50 | ) external returns (bool) { 51 | b[sender] = sub(b[sender], amount); 52 | b[recipient] = add(b[recipient], amount); 53 | a[sender][msg.sender] = sub(a[sender][msg.sender], amount); 54 | return true; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /scripts/deploy/stages/6_DeficitOffsetClinicSteward.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | import {IPool} from 'aave-v3-origin/contracts/interfaces/IPool.sol'; 5 | 6 | import {Create2Utils} from 'solidity-utils/contracts/utils/ScriptUtils.sol'; 7 | 8 | import {DeficitOffsetClinicSteward} from '../../../src/contracts/stewards/DeficitOffsetClinicSteward.sol'; 9 | 10 | import {UmbrellaScripts} from './3_Umbrella.s.sol'; 11 | 12 | library DeficitOffsetClinicStewardScripts { 13 | error ProxyNotExist(); 14 | 15 | function deployDeficitOffsetClinicSteward( 16 | address transparentProxyFactory, 17 | IPool pool, 18 | address executor, 19 | address collector, 20 | address financialComittee 21 | ) internal returns (address) { 22 | address umbrella = UmbrellaScripts.predictUmbrellaProxy( 23 | transparentProxyFactory, 24 | pool, 25 | executor, 26 | collector 27 | ); 28 | 29 | require(umbrella.code.length != 0, ProxyNotExist()); 30 | 31 | return 32 | Create2Utils.create2Deploy( 33 | 'v1', 34 | type(DeficitOffsetClinicSteward).creationCode, 35 | abi.encode(umbrella, collector, executor, financialComittee) 36 | ); 37 | } 38 | 39 | function predictDeficitOffsetClinicSteward( 40 | address transparentProxyFactory, 41 | IPool pool, 42 | address executor, 43 | address collector, 44 | address financialComittee 45 | ) internal view returns (address) { 46 | address umbrella = UmbrellaScripts.predictUmbrellaProxy( 47 | transparentProxyFactory, 48 | pool, 49 | executor, 50 | collector 51 | ); 52 | 53 | return 54 | Create2Utils.computeCreate2Address( 55 | 'v1', 56 | type(DeficitOffsetClinicSteward).creationCode, 57 | abi.encode(umbrella, collector, executor, financialComittee) 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /certora/specs/rewards/mirrors.spec: -------------------------------------------------------------------------------- 1 | import "base.spec"; 2 | 3 | 4 | 5 | rule mirror_asset_rewardsInfo_len__correct__rule(method f) 6 | { 7 | address asset; 8 | assert mirror_asset_rewardsInfo_len[asset] == get_rewardsInfo_length(asset); 9 | } 10 | 11 | 12 | rule mirror_targetLiquidity__correctness { 13 | address asset; 14 | assert mirror_targetLiquidity[asset] == get_targetLiquidity(asset); 15 | } 16 | 17 | 18 | rule mirror_asset_index_2_addr__correct__rule(method f) 19 | { 20 | address asset; uint256 i; 21 | assert mirror_asset_index_2_addr[asset][i] == get_addr(asset,i); 22 | } 23 | 24 | 25 | rule mirror_asset_distributionEnd__map__correctness(method f) 26 | { 27 | address asset; address reward; 28 | assert mirror_asset_distributionEnd__map[asset][reward] == get_distributionEnd__map(asset,reward); 29 | } 30 | 31 | rule mirror_asset_reward_2_rewardIndex__correctness(method f) 32 | { 33 | address asset; address reward; 34 | assert mirror_asset_reward_2_rewardIndex[asset][reward] == get_rewardIndex(asset,reward); 35 | } 36 | 37 | rule mirror_asset_distributionEnd__arr__correctness(method f) 38 | { 39 | address asset; uint ind; 40 | assert mirror_asset_distributionEnd__arr[asset][ind] == get_distributionEnd__arr(asset,ind); 41 | } 42 | 43 | rule mirror_asset_reward_user_2_accrued__correctness(method f) 44 | { 45 | address asset; address reward; address user; 46 | assert mirror_asset_reward_user_2_accrued[asset][reward][user] == get_accrued(asset,reward,user); 47 | } 48 | 49 | rule mirror_asset_reward_user_2_userIndex__correctness(method f) 50 | { 51 | address asset; address reward; address user; 52 | assert mirror_asset_reward_user_2_userIndex[asset][reward][user] == get_userIndex(asset,reward,user); 53 | } 54 | 55 | 56 | rule mirror_authorizedClaimers__correctness(method f) 57 | { 58 | address user; address claimer; 59 | assert mirror_authorizedClaimers[user][claimer] == isClaimerAuthorized(user,claimer); 60 | } 61 | -------------------------------------------------------------------------------- /scripts/deploy/stages/1_RewardsController.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | import {TransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/TransparentProxyFactory.sol'; 5 | import {Create2Utils} from 'solidity-utils/contracts/utils/ScriptUtils.sol'; 6 | 7 | import {RewardsController} from '../../../src/contracts/rewards/RewardsController.sol'; 8 | 9 | library RewardsControllerScripts { 10 | error ImplementationNotExist(); 11 | 12 | function deployRewardsControllerImpl() internal returns (address) { 13 | return Create2Utils.create2Deploy('v1', type(RewardsController).creationCode); 14 | } 15 | 16 | function deployRewardsControllerProxy( 17 | address transparentProxyFactory, 18 | address executor 19 | ) internal returns (address) { 20 | address rewardsControllerImpl = predictRewardsControllerImpl(); 21 | require(rewardsControllerImpl.code.length != 0, ImplementationNotExist()); 22 | 23 | return 24 | TransparentProxyFactory(transparentProxyFactory).createDeterministic( 25 | rewardsControllerImpl, 26 | executor, // proxyOwner 27 | abi.encodeWithSelector(RewardsController.initialize.selector, executor), 28 | 'v1' 29 | ); 30 | } 31 | 32 | function predictRewardsControllerImpl() internal pure returns (address) { 33 | return Create2Utils.computeCreate2Address('v1', type(RewardsController).creationCode); 34 | } 35 | 36 | function predictRewardsControllerProxy( 37 | address transparentProxyFactory, 38 | address executor 39 | ) internal view returns (address) { 40 | address rewardsControllerImpl = predictRewardsControllerImpl(); 41 | 42 | return 43 | TransparentProxyFactory(transparentProxyFactory).predictCreateDeterministic( 44 | rewardsControllerImpl, 45 | executor, // proxyOwner 46 | abi.encodeWithSelector(RewardsController.initialize.selector, executor), 47 | 'v1' 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /certora/conf/umbrella/Umbrella.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "certora/harness/UmbrellaHarness.sol", 4 | "certora/harness/UmbrellaStakeTokenA.sol", 5 | "certora/harness/ERC20A.sol", 6 | "certora/harness/ERC20B.sol", 7 | "src/contracts/rewards/RewardsController.sol", 8 | ], 9 | "packages": [ 10 | "solidity-utils=lib/aave-v3-origin/lib/solidity-utils/src", 11 | "@openzeppelin/contracts-upgradeable=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/contracts", 12 | "@openzeppelin/contracts=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts", 13 | "openzeppelin-contracts-upgradeable=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable", 14 | "openzeppelin-contracts=lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts", 15 | "aave-v3-origin-tests=lib/aave-v3-origin/tests", 16 | "aave-v3-origin=lib/aave-v3-origin/src", 17 | ], 18 | "auto_dispatcher": true, 19 | // "build_cache": true, 20 | "prover_args": ["-copyLoopUnroll 8", 21 | "-solvers [z3:def{randomSeed=110},z3:def{randomSeed=111},z3:def{randomSeed=112},z3:def{randomSeed=113},z3:def{randomSeed=114},z3:def{randomSeed=115},z3:def{randomSeed=116},z3:def{randomSeed=117},z3:def{randomSeed=118},z3:def{randomSeed=119}]" 22 | ], 23 | "optimistic_loop": true, 24 | "loop_iter": "2", 25 | "optimistic_fallback": true, 26 | "optimistic_hashing": true, 27 | "hashing_length_bound": "320", 28 | "process": "emv", 29 | "solc": "solc8.27", 30 | "solc_evm_version": "shanghai", 31 | "verify": "UmbrellaHarness:certora/specs/umbrella/Umbrella.spec", 32 | "parametric_contracts":["UmbrellaHarness"], 33 | "smt_timeout": "6000", 34 | "rule_sanity": "basic", 35 | "multi_assert_check" : false, 36 | "msg": "Umbrella::rules" 37 | } 38 | -------------------------------------------------------------------------------- /certora/specs/umbrella/Pool.spec: -------------------------------------------------------------------------------- 1 | // Mock of AAVE-v3 pool for the Umbrella contract 2 | methods { 3 | function _.eliminateReserveDeficit(address asset, uint256 amount) external => 4 | eliminateReserveDeficitCVL(asset,amount) expect void; 5 | function _.getReserveDeficit(address asset) external => getReserveDeficitCVL(asset) expect uint256; 6 | function _.getPriceOracle() external => PER_CALLEE_CONSTANT; 7 | function _.getAssetPrice(address asset) external with (env e) => assetPriceCVL(/*calledContract,*/ asset, e.block.timestamp) expect uint256; 8 | function _.ADDRESSES_PROVIDER() external => addressProvider() expect address; 9 | function _.getReserveAToken(address asset) external => ATokenOfReserve(asset) expect address; 10 | /// @dev: Which calls might change the configuration map? Is it really immutable in this scope? 11 | function _.getConfiguration(address asset) external => configurationMap(asset) expect uint256; 12 | } 13 | 14 | ghost ATokenOfReserve(address /* asset */) returns address; 15 | 16 | ghost assetPriceCVL(address /* asset */, uint256 /* timestamp */) returns uint256; 17 | 18 | ghost configurationMap(address /* asset */) returns uint256; 19 | persistent ghost addressProvider() returns address; 20 | 21 | /*persistent*/ ghost mapping(address /* asset */ => uint256 /* deficit */) _reservesDeficit; 22 | 23 | function getReserveDeficitCVL(address asset) returns uint256 {return _reservesDeficit[asset];} 24 | 25 | 26 | ghost address eliminateReserveDeficit__asset; 27 | ghost uint256 eliminateReserveDeficit__amount; 28 | function eliminateReserveDeficitCVL(address asset, uint256 amount) { 29 | // In order to check the parameters passed to eliminateReserveDeficit(), we record them here: 30 | eliminateReserveDeficit__asset = asset; 31 | eliminateReserveDeficit__amount = amount; 32 | 33 | uint256 balanceWriteOff = _reservesDeficit[asset] < amount ? _reservesDeficit[asset] : amount; 34 | _reservesDeficit[asset] = assert_uint256(_reservesDeficit[asset] - balanceWriteOff); 35 | } 36 | -------------------------------------------------------------------------------- /tests/rewards/RescuableACL.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; 5 | 6 | import {IRescuable} from 'solidity-utils/contracts/utils/interfaces/IRescuable.sol'; 7 | 8 | import {RewardsControllerBaseTest} from './utils/RewardsControllerBase.t.sol'; 9 | 10 | contract ReceiveEther { 11 | event Received(uint256 amount); 12 | 13 | receive() external payable { 14 | emit Received(msg.value); 15 | } 16 | } 17 | 18 | contract RescuableACLTest is RewardsControllerBaseTest { 19 | function test_rescue() public { 20 | _dealUnderlying(address(unusedReward), address(rewardsController), 1 ether); 21 | 22 | vm.startPrank(defaultAdmin); 23 | 24 | rewardsController.emergencyTokenTransfer(address(unusedReward), someone, 1 ether); 25 | 26 | assertEq(unusedReward.balanceOf(address(rewardsController)), 0); 27 | assertEq(unusedReward.balanceOf(someone), 1 ether); 28 | } 29 | 30 | function test_rescueEther() public { 31 | deal(address(rewardsController), 1 ether); 32 | 33 | address sendToMe = address(new ReceiveEther()); 34 | 35 | vm.stopPrank(); 36 | vm.startPrank(defaultAdmin); 37 | 38 | rewardsController.emergencyEtherTransfer(sendToMe, 1 ether); 39 | 40 | assertEq(sendToMe.balance, 1 ether); 41 | } 42 | 43 | function test_rescueFromNotAdmin(address anyone) public { 44 | vm.assume(anyone != defaultAdmin && anyone != proxyAdminContract); 45 | 46 | address sendToMe = address(new ReceiveEther()); 47 | 48 | _dealUnderlying(address(unusedReward), address(rewardsController), 1 ether); 49 | deal(address(rewardsController), 1 ether); 50 | 51 | vm.startPrank(anyone); 52 | 53 | vm.expectRevert(abi.encodeWithSelector(IRescuable.OnlyRescueGuardian.selector)); 54 | rewardsController.emergencyTokenTransfer(address(unusedReward), someone, 1 ether); 55 | 56 | vm.expectRevert(abi.encodeWithSelector(IRescuable.OnlyRescueGuardian.selector)); 57 | rewardsController.emergencyEtherTransfer(sendToMe, 1 ether); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/contracts/automation/interfaces/ISlashingRobot.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {IAutomation} from './IAutomation.sol'; 5 | 6 | /** 7 | * @title ISlashingRobot 8 | * @author BGD Labs 9 | **/ 10 | interface ISlashingRobot is IAutomation { 11 | /// @notice Attempted to `performUpkeep`, that hadn't slash anything. 12 | error NoSlashesPerformed(); 13 | 14 | /** 15 | * @notice Emitted when `performUpkeep` is called and the `reserve` is slashed. 16 | * @param reserve Address of the slashed `reserve` 17 | * @param amount Amount of the deficit covered 18 | */ 19 | event ReserveSlashed(address indexed reserve, uint256 amount); 20 | 21 | /** 22 | * @notice Emitted when owner sets the disable flag for a `reserve`. 23 | * @param reserve Address of the `reserve` for which `disable` flag is set 24 | */ 25 | event ReserveDisabled(address indexed reserve, bool disable); 26 | 27 | /** 28 | * @notice Method to get the maximum slash size. 29 | * Max check size is used to limit checking/slashing if `reserve` can be slashed or not 30 | * so as to avoid exceeding max gas limit on the automation infra. 31 | * @return max Max check size 32 | */ 33 | function MAX_CHECK_SIZE() external view returns (uint256); 34 | 35 | /** 36 | * @notice Method to get the address of the aave `umbrella` contract. 37 | * @return address Address of the aave `umbrella` contract 38 | */ 39 | function UMBRELLA() external view returns (address); 40 | 41 | /** 42 | * @notice Method to check if automation is disabled for the `reserve` or not. 43 | * @param reserve Address of the `reserve` to check 44 | * @return bool Flag if automation is disabled or not for this `reserve` 45 | **/ 46 | function isDisabled(address reserve) external view returns (bool); 47 | 48 | /** 49 | * @notice Method called by `owner` to disable/enable automation on the specific reserve. 50 | * @param reserve Address for which we need to disable/enable automation 51 | * @param disabled True if automation should be disabled, false otherwise 52 | */ 53 | function disable(address reserve, bool disabled) external; 54 | } 55 | -------------------------------------------------------------------------------- /certora/scripts/run-all-rewards-invariants.sh: -------------------------------------------------------------------------------- 1 | #CMN="--compilation_steps_only" 2 | #CMN="--server staging" 3 | 4 | 5 | 6 | echo 7 | echo "1: invariants.conf" 8 | certoraRun $CMN certora/conf/rewards/invariants.conf --rule distributionEnd_NEQ_0 \ 9 | --msg "invariant 1. distributionEnd_NEQ_0" 10 | 11 | echo 12 | echo "2: invariants.conf" 13 | certoraRun $CMN certora/conf/rewards/invariants.conf --rule all_rewars_are_different \ 14 | --msg "invariant 2. all_rewars_are_different" 15 | 16 | echo 17 | echo "3: invariants.conf" 18 | certoraRun $CMN certora/conf/rewards/invariants.conf --rule same_distributionEnd_values \ 19 | --msg "invariant 3. same_distributionEnd_values" 20 | 21 | echo 22 | echo "4: invariants.conf" 23 | certoraRun $CMN certora/conf/rewards/invariants.conf --rule lastUpdateTimestamp_LEQ_current_time \ 24 | --msg "invariant 4. lastUpdateTimestamp_LEQ_current_time" 25 | 26 | echo 27 | echo "5: invariants.conf" 28 | certoraRun $CMN certora/conf/rewards/invariants.conf --rule accrued_is_0_for_non_existing_reward \ 29 | --msg "invariant 5.accrued_is_0_for_non_existing_reward" 30 | 31 | echo 32 | echo "6: invariants.conf" 33 | certoraRun $CMN certora/conf/rewards/invariants.conf --rule userIndex_is_0_for_non_existing_reward \ 34 | --msg "invariant 6.userIndex_is_0_for_non_existing_reward" 35 | 36 | echo 37 | echo "7: invariants.conf" 38 | certoraRun $CMN certora/conf/rewards/invariants.conf --rule distributionEnd_is_0_for_non_existing_reward \ 39 | --msg "invariant 7.distributionEnd_is_0_for_non_existing_reward" 40 | 41 | echo 42 | echo "8: invariants.conf" 43 | certoraRun $CMN certora/conf/rewards/invariants.conf --rule rewardIndex_is_0_for_non_existing_reward \ 44 | --msg "invariant 8.rewardIndex_is_0_for_non_existing_reward" 45 | 46 | echo 47 | echo "9: invariants.conf" 48 | certoraRun $CMN certora/conf/rewards/invariants.conf --rule userIndex_LEQ_rewardIndex \ 49 | --msg "invariant 9.userIndex_LEQ_rewardIndex" 50 | 51 | echo 52 | echo "10: invariants.conf" 53 | certoraRun $CMN certora/conf/rewards/invariants.conf --rule targetLiquidity_NEQ_0 \ 54 | --msg "invariant 10.targetLiquidity_NEQ_0" 55 | 56 | -------------------------------------------------------------------------------- /tests/payloads/Rescuable.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; 5 | 6 | import {IRescuable} from 'solidity-utils/contracts/utils/interfaces/IRescuable.sol'; 7 | import {Ownable} from 'openzeppelin-contracts/contracts/access/Ownable.sol'; 8 | 9 | import {UmbrellaPayloadSetup} from './utils/UmbrellaPayloadSetup.t.sol'; 10 | 11 | contract ReceiveEther { 12 | event Received(uint256 amount); 13 | 14 | receive() external payable { 15 | emit Received(msg.value); 16 | } 17 | } 18 | 19 | contract RescuableTests is UmbrellaPayloadSetup { 20 | function test_rescue() public { 21 | deal(address(underlying_1), umbrellaConfigEngine, 1 ether); 22 | 23 | vm.startPrank(rescueGuardian); 24 | 25 | IRescuable(umbrellaConfigEngine).emergencyTokenTransfer(address(underlying_1), user, 1 ether); 26 | 27 | assertEq(underlying_1.balanceOf(umbrellaConfigEngine), 0); 28 | assertEq(underlying_1.balanceOf(user), 1 ether); 29 | } 30 | 31 | function test_rescueEther() public { 32 | deal(umbrellaConfigEngine, 1 ether); 33 | 34 | address sendToMe = address(new ReceiveEther()); 35 | 36 | vm.stopPrank(); 37 | vm.startPrank(rescueGuardian); 38 | 39 | IRescuable(umbrellaConfigEngine).emergencyEtherTransfer(sendToMe, 1 ether); 40 | 41 | assertEq(umbrellaConfigEngine.balance, 0); 42 | assertEq(sendToMe.balance, 1 ether); 43 | } 44 | 45 | function test_rescueFromNotAdmin(address anyone) public { 46 | vm.assume(anyone != rescueGuardian); 47 | 48 | deal(address(underlying_1), umbrellaConfigEngine, 1 ether); 49 | deal(umbrellaConfigEngine, 1 ether); 50 | 51 | address sendToMe = address(new ReceiveEther()); 52 | 53 | vm.stopPrank(); 54 | vm.startPrank(anyone); 55 | 56 | vm.expectRevert(abi.encodeWithSelector(IRescuable.OnlyRescueGuardian.selector)); 57 | IRescuable(umbrellaConfigEngine).emergencyTokenTransfer(address(underlying_1), user, 1 ether); 58 | 59 | vm.expectRevert(abi.encodeWithSelector(IRescuable.OnlyRescueGuardian.selector)); 60 | IRescuable(umbrellaConfigEngine).emergencyEtherTransfer(sendToMe, 1 ether); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /certora/scripts/run-all-rewards.sh: -------------------------------------------------------------------------------- 1 | #CMN="--compilation_steps_only" 2 | #CMN="--server staging" 3 | 4 | 5 | echo 6 | echo "1: mirrors.conf" 7 | certoraRun $CMN certora/conf/rewards/mirrors.conf \ 8 | --msg "1. rewards/mirrors.conf" 9 | 10 | #echo 11 | #echo "2: invariants.conf" 12 | #certoraRun $CMN certora/conf/rewards/invariants.conf \ 13 | # --msg "2. rewards/invariants.conf" 14 | 15 | echo 16 | echo "3: double_reward.conf" 17 | certoraRun $CMN certora/conf/rewards/double_reward.conf \ 18 | --msg "3. rewards/double_reward.conf" 19 | 20 | 21 | echo 22 | echo "4: single_reward.conf" 23 | certoraRun $CMN certora/conf/rewards/single_reward.conf \ 24 | --exclude_rule bob_cant_DOS_alice_to_claim bob_cant_DOS_alice_to_claim__claimSelectedRewards bob_cant_DOS_alice_to_claim__claimAllRewards bob_cant_affect_the_claimed_amount_of_alice \ 25 | --msg "4. rewards/single_reward.conf: not excluded rules" 26 | 27 | echo 28 | echo "5: single_reward-depth0.conf DOS1" 29 | certoraRun $CMN certora/conf/rewards/single_reward-depth0.conf \ 30 | --rule bob_cant_DOS_alice_to_claim \ 31 | --msg "5. rewards/single_reward-depth0.conf: bob_cant_DOS_alice_to_claim" 32 | 33 | 34 | echo 35 | echo "6: single_reward-depth0.conf DOS2" 36 | certoraRun $CMN certora/conf/rewards/single_reward-depth0.conf \ 37 | --rule bob_cant_DOS_alice_to_claim__claimAllRewards \ 38 | --msg "6. rewards/single_reward-depth0.conf: bob_cant_DOS_alice_to_claim__claimAllRewards" 39 | 40 | echo 41 | echo "7: single_reward-depth0.conf DOS3" 42 | certoraRun $CMN certora/conf/rewards/single_reward-depth0.conf \ 43 | --rule bob_cant_DOS_alice_to_claim__claimSelectedRewards \ 44 | --msg "7. rewards/single_reward-depth0.conf: bob_cant_DOS_alice_to_claim__claimSelectedRewards" 45 | 46 | echo 47 | echo "8: single_reward-special_config.conf bob_cant_affect_the_claimed_amount_of_alice" 48 | certoraRun $CMN certora/conf/rewards/single_reward-special_config.conf \ 49 | --rule bob_cant_affect_the_claimed_amount_of_alice \ 50 | --msg "8. rewards/single_rewad-special_config.conf: bob_cant_affect_the_claimed_amount_of_alice" 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /scripts/deploy/stages/5_UmbrellaConfigEngine.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | import {IPool} from 'aave-v3-origin/contracts/interfaces/IPool.sol'; 5 | 6 | import {Create2Utils} from 'solidity-utils/contracts/utils/ScriptUtils.sol'; 7 | 8 | import {UmbrellaConfigEngine} from '../../../src/contracts/payloads/configEngine/UmbrellaConfigEngine.sol'; 9 | 10 | import {RewardsControllerScripts} from './1_RewardsController.s.sol'; 11 | import {UmbrellaScripts} from './3_Umbrella.s.sol'; 12 | 13 | library UmbrellaConfigEngineScripts { 14 | error ProxyNotExist(); 15 | 16 | function deployUmbrellaConfigEngine( 17 | address transparentProxyFactory, 18 | IPool pool, 19 | address executor, 20 | address collector 21 | ) internal returns (address) { 22 | address rewardsController = RewardsControllerScripts.predictRewardsControllerProxy( 23 | transparentProxyFactory, 24 | executor 25 | ); 26 | 27 | require(rewardsController.code.length != 0, ProxyNotExist()); 28 | 29 | address umbrella = UmbrellaScripts.predictUmbrellaProxy( 30 | transparentProxyFactory, 31 | pool, 32 | executor, 33 | collector 34 | ); 35 | 36 | require(umbrella.code.length != 0, ProxyNotExist()); 37 | 38 | return 39 | Create2Utils.create2Deploy( 40 | 'v1', 41 | type(UmbrellaConfigEngine).creationCode, 42 | abi.encode(rewardsController, umbrella, executor) 43 | ); 44 | } 45 | 46 | function predictUmbrellaConfigEngine( 47 | address transparentProxyFactory, 48 | IPool pool, 49 | address executor, 50 | address collector 51 | ) internal view returns (address) { 52 | address rewardsController = RewardsControllerScripts.predictRewardsControllerProxy( 53 | transparentProxyFactory, 54 | executor 55 | ); 56 | 57 | address umbrella = UmbrellaScripts.predictUmbrellaProxy( 58 | transparentProxyFactory, 59 | pool, 60 | executor, 61 | collector 62 | ); 63 | 64 | return 65 | Create2Utils.computeCreate2Address( 66 | 'v1', 67 | type(UmbrellaConfigEngine).creationCode, 68 | abi.encode(rewardsController, umbrella, executor) 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | test = 'tests' 4 | script = 'scripts' 5 | out = 'out' 6 | libs = ['lib'] 7 | remappings = [] 8 | fs_permissions = [{ access = "write", path = "./reports" }] 9 | via_ir = false 10 | optimizer = true 11 | optimizer_runs = 200 12 | solc_version = '0.8.27' 13 | evm_version = 'shanghai' 14 | bytecode_hash = 'none' 15 | ffi = true 16 | 17 | 18 | [fuzz] 19 | runs = 1024 20 | max_test_rejects = 1000000 21 | 22 | [rpc_endpoints] 23 | mainnet = "${RPC_MAINNET}" 24 | polygon = "${RPC_POLYGON}" 25 | polygon_amoy = "${RPC_POLYGON_AMOY}" 26 | avalanche = "${RPC_AVALANCHE}" 27 | avalanche_fuji = "${RPC_AVALANCHE_FUJI}" 28 | arbitrum = "${RPC_ARBITRUM}" 29 | arbitrum_sepolia = "${RPC_ARBITRUM_SEPOLIA}" 30 | fantom = "${RPC_FANTOM}" 31 | fantom_testnet = "${RPC_FANTOM_TESTNET}" 32 | optimism = "${RPC_OPTIMISM}" 33 | optimism_sepolia = "${RPC_OPTIMISM_SEPOLIA}" 34 | harmony = "${RPC_HARMONY}" 35 | sepolia = "${RPC_SEPOLIA}" 36 | scroll = "${RPC_SCROLL}" 37 | scroll_sepolia = "${RPC_SCROLL_SEPOLIA}" 38 | metis = "${RPC_METIS}" 39 | base = "${RPC_BASE}" 40 | base_sepolia = "${RPC_BASE_SEPOLIA}" 41 | bnb = "${RPC_BNB}" 42 | gnosis = "${RPC_GNOSIS}" 43 | zkEVM = "${RPC_ZKEVM}" 44 | celo = "${RPC_CELO}" 45 | zksync = "${RPC_ZKSYNC}" 46 | 47 | [etherscan] 48 | mainnet = { key = "${ETHERSCAN_API_KEY_MAINNET}", chain = 1 } 49 | sepolia = { key = "${ETHERSCAN_API_KEY_MAINNET}" } 50 | optimism = { key = "${ETHERSCAN_API_KEY_OPTIMISM}", chain = 10 } 51 | avalanche = { key = "${ETHERSCAN_API_KEY_AVALANCHE}", chain = 43114 } 52 | avalanche_fuji = { key = "${ETHERSCAN_API_KEY_AVALANCHE}", chain = 43113, url = 'https://api-testnet.snowscan.xyz/api' } 53 | polygon = { key = "${ETHERSCAN_API_KEY_POLYGON}", chain = 137 } 54 | arbitrum = { key = "${ETHERSCAN_API_KEY_ARBITRUM}", chain = 42161 } 55 | fantom = { key = "${ETHERSCAN_API_KEY_FANTOM}", chain = 250 } 56 | metis = { key = "any", chain = 1088, url = 'https://andromeda-explorer.metis.io/' } 57 | base = { key = "${ETHERSCAN_API_KEY_BASE}", chain = 8453 } 58 | zkevm = { key = "${ETHERSCAN_API_KEY_ZKEVM}", chain = 1101 } 59 | gnosis = { key = "${ETHERSCAN_API_KEY_GNOSIS}", chain = 100 } 60 | bnb = { key = "${ETHERSCAN_API_KEY_BNB}", chain = 56 } 61 | 62 | # See more config options https://github.com/gakonst/foundry/tree/master/config 63 | -------------------------------------------------------------------------------- /tests/stakeToken/Rescuable.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; 5 | 6 | import {IRescuable} from 'solidity-utils/contracts/utils/interfaces/IRescuable.sol'; 7 | import {StakeTestBase} from './utils/StakeTestBase.t.sol'; 8 | 9 | contract ReceiveEther { 10 | event Received(uint256 amount); 11 | 12 | receive() external payable { 13 | emit Received(msg.value); 14 | } 15 | } 16 | 17 | contract RescuableTests is StakeTestBase { 18 | function test_checkWhoCanRescue() public view { 19 | assertEq(stakeToken.whoCanRescue(), admin); 20 | } 21 | 22 | function test_rescue() public { 23 | // will use the same token, but imagine if it's not underlying) 24 | _dealUnderlying(1 ether, someone); 25 | 26 | vm.startPrank(someone); 27 | 28 | IERC20(underlying).transfer(address(stakeToken), 1 ether); 29 | 30 | vm.stopPrank(); 31 | vm.startPrank(admin); 32 | 33 | stakeToken.emergencyTokenTransfer(address(underlying), someone, 1 ether); 34 | 35 | assertEq(underlying.balanceOf(address(stakeToken)), 0); 36 | assertEq(underlying.balanceOf(someone), 1 ether); 37 | } 38 | 39 | function test_rescueEther() public { 40 | deal(address(stakeToken), 1 ether); 41 | 42 | address sendToMe = address(new ReceiveEther()); 43 | 44 | vm.stopPrank(); 45 | vm.startPrank(admin); 46 | 47 | stakeToken.emergencyEtherTransfer(sendToMe, 1 ether); 48 | 49 | assertEq(sendToMe.balance, 1 ether); 50 | } 51 | 52 | function test_rescueFromNotAdmin(address anyone) public { 53 | vm.assume(anyone != admin && anyone != proxyAdminContract); 54 | _dealUnderlying(1 ether, someone); 55 | 56 | vm.startPrank(someone); 57 | 58 | IERC20(underlying).transfer(address(stakeToken), 1 ether); 59 | 60 | address sendToMe = address(new ReceiveEther()); 61 | 62 | vm.stopPrank(); 63 | vm.startPrank(anyone); 64 | 65 | vm.expectRevert(abi.encodeWithSelector(IRescuable.OnlyRescueGuardian.selector)); 66 | stakeToken.emergencyTokenTransfer(address(underlying), someone, 1 ether); 67 | 68 | vm.expectRevert(abi.encodeWithSelector(IRescuable.OnlyRescueGuardian.selector)); 69 | stakeToken.emergencyEtherTransfer(sendToMe, 1 ether); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /certora/specs/umbrella/invariants.spec: -------------------------------------------------------------------------------- 1 | import "setup.spec"; 2 | 3 | 4 | 5 | 6 | //================================================================================================ 7 | // Rule: pending_deficit_cant_exceed_real_deficit 8 | // Description: Any operation of currentContract cant make the pending-deficit bigger than the real-deficit 9 | // Status: PASS 10 | //================================================================================================ 11 | invariant pending_deficit_cant_exceed_real_deficit(address reserve) 12 | getReserveDeficitCVL(reserve) >= getPendingDeficit(reserve) 13 | filtered {f -> f.contract == currentContract} 14 | 15 | 16 | 17 | 18 | 19 | ghost mathint sumOfBalances_erc20A { 20 | init_state axiom sumOfBalances_erc20A == 0; 21 | } 22 | hook Sstore ERC20A._balances[KEY address account] uint256 balance (uint256 balance_old) { 23 | sumOfBalances_erc20A = sumOfBalances_erc20A + balance - balance_old; 24 | } 25 | hook Sload uint256 balance ERC20A._balances[KEY address account] { 26 | require balance <= sumOfBalances_erc20A; 27 | } 28 | 29 | // ==================================================================== 30 | // Invariant: inv_sumOfBalances_eq_totalSupply__erc20A 31 | // Description: The total supply equals the sum of all users' balances. 32 | // Status: PASS 33 | // ==================================================================== 34 | invariant inv_sumOfBalances_eq_totalSupply__erc20A() 35 | sumOfBalances_erc20A == erc20A.totalSupply(); 36 | 37 | ghost mathint sumOfBalances_erc20B { 38 | init_state axiom sumOfBalances_erc20B == 0; 39 | } 40 | hook Sstore ERC20B._balances[KEY address account] uint256 balance (uint256 balance_old) { 41 | sumOfBalances_erc20B = sumOfBalances_erc20B + balance - balance_old; 42 | } 43 | hook Sload uint256 balance ERC20B._balances[KEY address account] { 44 | require balance <= sumOfBalances_erc20B; 45 | } 46 | 47 | // ==================================================================== 48 | // Invariant: inv_sumOfBalances_eq_totalSupply__erc20B 49 | // Description: The total supply equals the sum of all users' balances. 50 | // Status: PASS 51 | // ==================================================================== 52 | invariant inv_sumOfBalances_eq_totalSupply__erc20B() 53 | sumOfBalances_erc20B == erc20B.totalSupply(); 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/contracts/automation/interfaces/IAutomation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IAutomation { 5 | /** 6 | * @notice method that is simulated by the keepers to see if any work actually 7 | * needs to be performed. This method does does not actually need to be 8 | * executable, and since it is only ever simulated it can consume lots of gas. 9 | * @dev To ensure that it is never called, you may want to add the 10 | * cannotExecute modifier from KeeperBase to your implementation of this 11 | * method. 12 | * @param checkData specified in the upkeep registration so it is always the 13 | * same for a registered upkeep. This can easily be broken down into specific 14 | * arguments using `abi.decode`, so multiple upkeeps can be registered on the 15 | * same contract and easily differentiated by the contract. 16 | * @return upkeepNeeded boolean to indicate whether the keeper should call 17 | * performUpkeep or not. 18 | * @return performData bytes that the keeper should call performUpkeep with, if 19 | * upkeep is needed. If you would like to encode data to decode later, try 20 | * `abi.encode`. 21 | */ 22 | function checkUpkeep(bytes calldata checkData) external returns (bool upkeepNeeded, bytes memory performData); 23 | 24 | /** 25 | * @notice method that is actually executed by the keepers, via the registry. 26 | * The data returned by the checkUpkeep simulation will be passed into 27 | * this method to actually be executed. 28 | * @dev The input to this method should not be trusted, and the caller of the 29 | * method should not even be restricted to any single registry. Anyone should 30 | * be able call it, and the input should be validated, there is no guarantee 31 | * that the data passed in is the performData returned from checkUpkeep. This 32 | * could happen due to malicious keepers, racing keepers, or simply a state 33 | * change while the performUpkeep transaction is waiting for confirmation. 34 | * Always validate the data passed in. 35 | * @param performData is the data which was passed back from the checkData 36 | * simulation. If it is encoded, it can easily be decoded into other types by 37 | * calling `abi.decode`. This data should not be trusted, and should be 38 | * validated against the contract's current state. 39 | */ 40 | function performUpkeep(bytes calldata performData) external; 41 | } -------------------------------------------------------------------------------- /src/contracts/rewards/interfaces/IRewardsStructs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @title IRewardsStructs interface 6 | * @notice An interface containing structures that can be used externally. 7 | * @author BGD labs 8 | */ 9 | interface IRewardsStructs { 10 | struct RewardSetupConfig { 11 | /// @notice Reward address 12 | address reward; 13 | /// @notice Address, from which this reward will be transferred (should give approval to this address) 14 | address rewardPayer; 15 | /// @notice Maximum possible emission rate of rewards per second 16 | uint256 maxEmissionPerSecond; 17 | /// @notice End of the rewards distribution 18 | uint256 distributionEnd; 19 | } 20 | 21 | struct AssetDataExternal { 22 | /// @notice Liquidity value at which there will be maximum emission per second (expected amount of asset to be deposited into `StakeToken`) 23 | uint256 targetLiquidity; 24 | /// @notice Timestamp of the last update 25 | uint256 lastUpdateTimestamp; 26 | } 27 | 28 | struct RewardDataExternal { 29 | /// @notice Reward address 30 | address addr; 31 | /// @notice Liquidity index of the reward set during the last update 32 | uint256 index; 33 | /// @notice Maximum possible emission rate of rewards per second 34 | uint256 maxEmissionPerSecond; 35 | /// @notice End of the reward distribution 36 | uint256 distributionEnd; 37 | } 38 | 39 | struct EmissionData { 40 | /// @notice Liquidity value at which there will be maximum emission per second applied 41 | uint256 targetLiquidity; 42 | /// @notice Liquidity value after which emission per second will be flat 43 | uint256 targetLiquidityExcess; 44 | /// @notice Maximum possible emission rate of rewards per second (can be with or without scaling to 18 decimals, depending on usage in code) 45 | uint256 maxEmission; 46 | /// @notice Flat emission value per second (can be with or without scaling, depending on usage in code) 47 | uint256 flatEmission; 48 | } 49 | 50 | struct UserDataExternal { 51 | /// @notice Liquidity index of the user reward set during the last update 52 | uint256 index; 53 | /// @notice Amount of accrued rewards that the user earned at the time of his last index update (pending to claim) 54 | uint256 accrued; 55 | } 56 | 57 | struct SignatureParams { 58 | uint8 v; 59 | bytes32 r; 60 | bytes32 s; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/helpers/Rescuable.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; 5 | 6 | import {IRescuable} from 'solidity-utils/contracts/utils/interfaces/IRescuable.sol'; 7 | import {UmbrellaBatchHelperTestBase} from './utils/UmbrellaBatchHelperBase.t.sol'; 8 | 9 | contract ReceiveEther { 10 | event Received(uint256 amount); 11 | 12 | receive() external payable { 13 | emit Received(msg.value); 14 | } 15 | } 16 | 17 | contract RescuableTests is UmbrellaBatchHelperTestBase { 18 | address someone = address(0xdead); 19 | 20 | function test_checkWhoCanRescue() public view { 21 | assertEq(umbrellaBatchHelper.whoCanRescue(), defaultAdmin); 22 | } 23 | 24 | function test_rescue() public { 25 | // will use the same token, but imagine if it's not underlying) 26 | deal(underlying, someone, 1 ether); 27 | 28 | vm.startPrank(someone); 29 | 30 | IERC20(underlying).transfer(address(umbrellaBatchHelper), 1 ether); 31 | 32 | vm.stopPrank(); 33 | vm.startPrank(defaultAdmin); 34 | 35 | umbrellaBatchHelper.emergencyTokenTransfer(address(underlying), someone, 1 ether); 36 | 37 | assertEq(IERC20(underlying).balanceOf(address(stakeToken)), 0); 38 | assertEq(IERC20(underlying).balanceOf(someone), 1 ether); 39 | } 40 | 41 | function test_rescueEther() public { 42 | deal(address(umbrellaBatchHelper), 1 ether); 43 | 44 | address sendToMe = address(new ReceiveEther()); 45 | 46 | vm.stopPrank(); 47 | vm.startPrank(defaultAdmin); 48 | 49 | umbrellaBatchHelper.emergencyEtherTransfer(sendToMe, 1 ether); 50 | 51 | assertEq(sendToMe.balance, 1 ether); 52 | } 53 | 54 | function test_rescueFromNotAdmin(address anyone) public { 55 | vm.assume(anyone != defaultAdmin); 56 | 57 | deal(underlying, someone, 1 ether); 58 | deal(address(umbrellaBatchHelper), 1 ether); 59 | 60 | vm.startPrank(someone); 61 | 62 | IERC20(underlying).transfer(address(umbrellaBatchHelper), 1 ether); 63 | 64 | address sendToMe = address(new ReceiveEther()); 65 | 66 | vm.stopPrank(); 67 | vm.startPrank(anyone); 68 | 69 | vm.expectRevert(abi.encodeWithSelector(IRescuable.OnlyRescueGuardian.selector)); 70 | umbrellaBatchHelper.emergencyTokenTransfer(address(underlying), someone, 1 ether); 71 | 72 | vm.expectRevert(abi.encodeWithSelector(IRescuable.OnlyRescueGuardian.selector)); 73 | umbrellaBatchHelper.emergencyEtherTransfer(sendToMe, 1 ether); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/stakeToken/ERC4626a16z.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | // modified 5 | import 'erc4626-tests/ERC4626.test.sol'; 6 | 7 | import {TransparentUpgradeableProxy} from 'openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol'; 8 | 9 | import {StakeToken} from '../../src/contracts/stakeToken/StakeToken.sol'; 10 | import {IRewardsController} from '../../src/contracts/rewards/interfaces/IRewardsController.sol'; 11 | 12 | import {MockRewardsController} from './utils/mock/MockRewardsController.sol'; 13 | import {MockERC20Permit} from './utils/mock/MockERC20Permit.sol'; 14 | 15 | interface ISlashable { 16 | function slash(address to, uint256 amount) external; 17 | } 18 | 19 | contract ERC4626StdTest is ERC4626Test { 20 | function setUp() public override { 21 | _underlying_ = address(new MockERC20Permit('Mock ERC20', 'MERC20')); 22 | 23 | address mockRewardsController = address(new MockRewardsController()); 24 | StakeToken stakeTokenImpl = new StakeToken(IRewardsController(mockRewardsController)); 25 | 26 | _vault_ = address( 27 | new TransparentUpgradeableProxy( 28 | address(stakeTokenImpl), 29 | address(0x2000), 30 | abi.encodeWithSelector( 31 | StakeToken.initialize.selector, 32 | address(_underlying_), 33 | 'Mock ERC4626', 34 | 'MERC4626', 35 | address(0x3000), 36 | 15 days, 37 | 2 days 38 | ) 39 | ) 40 | ); 41 | 42 | _delta_ = 0; 43 | _vaultMayBeEmpty = false; 44 | _unlimitedAmount = false; 45 | } 46 | 47 | function whoCanSlash() public pure returns (address) { 48 | return address(0x3000); 49 | } 50 | 51 | function MIN_ASSETS_REMAINING() public view returns (uint256) { 52 | return StakeToken(_vault_).MIN_ASSETS_REMAINING(); 53 | } 54 | 55 | function setUpYield(Init memory init) public override { 56 | if (init.yield >= 0) { 57 | // there's no way for direct gain opportunity in stakeToken 58 | } else { 59 | uint256 totalShares; 60 | 61 | for (uint i = 0; i < N; i++) { 62 | totalShares += init.share[i]; 63 | } 64 | 65 | vm.assume(init.yield > type(int).min); 66 | 67 | uint loss = uint(-1 * init.yield); 68 | 69 | vm.assume(loss + MIN_ASSETS_REMAINING() < totalShares); 70 | 71 | vm.startPrank(whoCanSlash()); 72 | try ISlashable(_vault_).slash(address(0xdead), loss) {} catch { 73 | vm.assume(false); 74 | } 75 | 76 | vm.stopPrank(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/contracts/payloads/configEngine/IUmbrellaConfigEngine.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {IUmbrellaEngineStructs as IStructs} from '../IUmbrellaEngineStructs.sol'; 5 | import {IUmbrellaStkManager as ISMStructs, IUmbrellaConfiguration as ICStructs} from '../IUmbrellaEngineStructs.sol'; 6 | 7 | /** 8 | * @title IUmbrellaConfigEngine interface 9 | * @notice Interface to define functions that can be called from `UmbrellaBasePayload` and `UmbrellaExtendedPayload`. 10 | * @author BGD labs 11 | */ 12 | interface IUmbrellaConfigEngine { 13 | /// @dev Attempted to set zero address as parameter. 14 | error ZeroAddress(); 15 | 16 | /// Umbrella 17 | ///////////////////////////////////////////////////////////////////////////////////////// 18 | 19 | function executeCreateTokens( 20 | ISMStructs.StakeTokenSetup[] memory configs 21 | ) external returns (address[] memory); 22 | 23 | function executeUpdateUnstakeConfigs(IStructs.UnstakeConfig[] memory configs) external; 24 | 25 | function executeChangeCooldowns(ISMStructs.CooldownConfig[] memory configs) external; 26 | 27 | function executeChangeUnstakeWindows(ISMStructs.UnstakeWindowConfig[] memory configs) external; 28 | 29 | function executeRemoveSlashingConfigs(ICStructs.SlashingConfigRemoval[] memory configs) external; 30 | 31 | function executeUpdateSlashingConfigs(ICStructs.SlashingConfigUpdate[] memory configs) external; 32 | 33 | function executeSetDeficitOffsets(IStructs.SetDeficitOffset[] memory configs) external; 34 | 35 | function executeCoverPendingDeficits(IStructs.CoverDeficit[] memory configs) external; 36 | 37 | function executeCoverDeficitOffsets(IStructs.CoverDeficit[] memory configs) external; 38 | 39 | function executeCoverReserveDeficits(IStructs.CoverDeficit[] memory configs) external; 40 | 41 | /// RewardsController 42 | ///////////////////////////////////////////////////////////////////////////////////////// 43 | 44 | function executeConfigureStakesAndRewards( 45 | IStructs.ConfigureStakeAndRewardsConfig[] memory configs 46 | ) external; 47 | 48 | function executeConfigureRewards(IStructs.ConfigureRewardsConfig[] memory configs) external; 49 | 50 | /// Functions for extended payloads 51 | ///////////////////////////////////////////////////////////////////////////////////////// 52 | 53 | function executeComplexRemovals(IStructs.TokenRemoval[] memory configs) external; 54 | 55 | function executeComplexCreations(IStructs.TokenSetup[] memory configs) external; 56 | 57 | ///////////////////////////////////////////////////////////////////////////////////////// 58 | } 59 | -------------------------------------------------------------------------------- /tests/stewards/utils/DeficitOffsetClinicStewardBase.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; 5 | 6 | import {IRewardsStructs} from '../../../src/contracts/rewards/interfaces/IRewardsStructs.sol'; 7 | import {IUmbrellaConfiguration} from '../../../src/contracts/umbrella/interfaces/IUmbrellaConfiguration.sol'; 8 | 9 | import {StakeToken} from '../../../src/contracts/stakeToken/StakeToken.sol'; 10 | import {DeficitOffsetClinicSteward} from '../../../src/contracts/stewards/DeficitOffsetClinicSteward.sol'; 11 | 12 | import {RescuableBase, IRescuableBase} from '../../../src/contracts/stewards/DeficitOffsetClinicSteward.sol'; 13 | import {RescuableACL, IRescuable} from '../../../src/contracts/stewards/DeficitOffsetClinicSteward.sol'; 14 | 15 | import {UmbrellaBaseTest} from '../../umbrella/utils/UmbrellaBase.t.sol'; 16 | import {MockOracle} from '../../umbrella/utils/mocks/MockOracle.sol'; 17 | 18 | abstract contract DeficitOffsetClinicStewardBase is UmbrellaBaseTest { 19 | DeficitOffsetClinicSteward clinicSteward; 20 | 21 | bytes32 public constant FINANCE_COMMITTEE_ROLE = keccak256('FINANCE_COMITTEE_ROLE'); 22 | address financeCommittee = vm.addr(0x0900); 23 | 24 | function setUp() public virtual override { 25 | super.setUp(); 26 | 27 | clinicSteward = new DeficitOffsetClinicSteward( 28 | address(umbrella), 29 | collector, 30 | defaultAdmin, 31 | financeCommittee 32 | ); 33 | 34 | IUmbrellaConfiguration.SlashingConfigUpdate[] 35 | memory stakeSetups = new IUmbrellaConfiguration.SlashingConfigUpdate[](2); 36 | 37 | stakeSetups[0] = IUmbrellaConfiguration.SlashingConfigUpdate({ 38 | reserve: address(underlying6Decimals), 39 | umbrellaStake: address(stakeWith6Decimals), 40 | liquidationFee: 0, 41 | umbrellaStakeUnderlyingOracle: _setUpOracles(address(underlying6Decimals)) 42 | }); 43 | stakeSetups[1] = IUmbrellaConfiguration.SlashingConfigUpdate({ 44 | reserve: address(underlying18Decimals), 45 | umbrellaStake: address(stakeWith18Decimals), 46 | liquidationFee: 0, 47 | umbrellaStakeUnderlyingOracle: _setUpOracles(address(underlying18Decimals)) 48 | }); 49 | 50 | vm.startPrank(defaultAdmin); 51 | 52 | umbrella.updateSlashingConfigs(stakeSetups); 53 | umbrella.grantRole(COVERAGE_MANAGER_ROLE, address(clinicSteward)); 54 | 55 | vm.stopPrank(); 56 | } 57 | 58 | function _setUpOracles(address reserve) internal returns (address oracle) { 59 | aaveOracle.setAssetPrice(reserve, 1e8); 60 | 61 | return address(new MockOracle(1e8)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /scripts/deploy/stages/4_Helpers.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | import {Create2Utils} from 'solidity-utils/contracts/utils/ScriptUtils.sol'; 5 | 6 | import {UmbrellaBatchHelper} from '../../../src/contracts/helpers/UmbrellaBatchHelper.sol'; 7 | import {DataAggregationHelper} from '../../../src/contracts/helpers/DataAggregationHelper.sol'; 8 | 9 | import {RewardsControllerScripts} from './1_RewardsController.s.sol'; 10 | 11 | library HelpersScripts { 12 | error ProxyNotExist(); 13 | 14 | function deployDataAggregationHelper( 15 | address transparentProxyFactory, 16 | address executor 17 | ) internal returns (address) { 18 | address rewardsController = RewardsControllerScripts.predictRewardsControllerProxy( 19 | transparentProxyFactory, 20 | executor 21 | ); 22 | 23 | require(rewardsController.code.length != 0, ProxyNotExist()); 24 | 25 | return 26 | Create2Utils.create2Deploy( 27 | 'v1', 28 | type(DataAggregationHelper).creationCode, 29 | abi.encode(rewardsController, executor) 30 | ); 31 | } 32 | 33 | function deployUmbrellaBatchHelper( 34 | address transparentProxyFactory, 35 | address executor 36 | ) internal returns (address) { 37 | address rewardsController = RewardsControllerScripts.predictRewardsControllerProxy( 38 | transparentProxyFactory, 39 | executor 40 | ); 41 | 42 | require(rewardsController.code.length != 0, ProxyNotExist()); 43 | 44 | return 45 | Create2Utils.create2Deploy( 46 | 'v1', 47 | type(UmbrellaBatchHelper).creationCode, 48 | abi.encode(rewardsController, executor) 49 | ); 50 | } 51 | 52 | function predictDataAggregationHelper( 53 | address transparentProxyFactory, 54 | address executor 55 | ) internal view returns (address) { 56 | address rewardsController = RewardsControllerScripts.predictRewardsControllerProxy( 57 | transparentProxyFactory, 58 | executor 59 | ); 60 | 61 | return 62 | Create2Utils.computeCreate2Address( 63 | 'v1', 64 | type(DataAggregationHelper).creationCode, 65 | abi.encode(rewardsController, executor) 66 | ); 67 | } 68 | 69 | function predictUmbrellaBatchHelper( 70 | address transparentProxyFactory, 71 | address executor 72 | ) internal view returns (address) { 73 | address rewardsController = RewardsControllerScripts.predictRewardsControllerProxy( 74 | transparentProxyFactory, 75 | executor 76 | ); 77 | 78 | return 79 | Create2Utils.computeCreate2Address( 80 | 'v1', 81 | type(UmbrellaBatchHelper).creationCode, 82 | abi.encode(rewardsController, executor) 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/contracts/rewards/libraries/InternalStructs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | /** 5 | * @title InternalStructs library 6 | * @notice Structs for internal usage only 7 | * @author BGD labs 8 | */ 9 | library InternalStructs { 10 | struct AssetData { 11 | /// @notice Map of reward token addresses with their configuration and user data 12 | mapping(address reward => RewardAndUserData) data; 13 | /// @notice An array of rewards and the corresponding endings of their distribution (duplicated for optimization) 14 | RewardAddrAndDistrEnd[] rewardsInfo; 15 | /// @notice The target liquidity value at which the `maxEmissionPerSecond` is applied. (Max value with current `EmissionMath` lib is 1e34/35). 16 | uint160 targetLiquidity; 17 | /// @notice Timestamp of the last update 18 | uint32 lastUpdateTimestamp; 19 | } 20 | 21 | struct RewardAndUserData { 22 | /// @notice Reward configuration and index 23 | RewardData rewardData; 24 | /// @notice Map of reward token addresses with their user data 25 | mapping(address user => UserData) userData; 26 | /// @notice Address from which reward will be transferred 27 | address rewardPayer; 28 | } 29 | 30 | // @dev All rewards will be calculated as they are all 18-decimals tokens with the help of this variable 31 | struct RewardData { 32 | /// @notice Liquidity index of the reward (with scaling to 18 decimals and SCALING_FACTOR applied) 33 | uint144 index; 34 | /// @notice Maximum possible emission rate of rewards per second (scaled to 18 decimals) 35 | uint72 maxEmissionPerSecondScaled; 36 | /// @notice End of the reward distribution (DUPLICATED for optimization) 37 | uint32 distributionEnd; 38 | /// @notice Difference between 18 and `reward.decimals()` 39 | uint8 decimalsScaling; 40 | } 41 | 42 | struct UserData { 43 | /// @notice Liquidity index of the reward for the user that was set as a result of the last user rewards update 44 | uint144 index; 45 | /// @notice Amount of accrued rewards that the user earned at the time of his last index update (pending to claim) 46 | uint112 accrued; 47 | } 48 | 49 | struct RewardAddrAndDistrEnd { 50 | /// @notice Reward address 51 | address addr; 52 | /// @notice The end of the reward distribution (DUPLICATED for optimization) 53 | uint32 distributionEnd; 54 | } 55 | 56 | struct ExtraParamsForIndex { 57 | /// @notice Liquidity value at which there will be maximum emission per second 58 | uint256 targetLiquidity; 59 | /// @notice Amount of assets remaining inside the `stakeToken` 60 | uint256 totalAssets; 61 | /// @notice Amount of `stakeToken` shares minted 62 | uint256 totalSupply; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /scripts/deploy/stages/3_Umbrella.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | import {IPool} from 'aave-v3-origin/contracts/interfaces/IPool.sol'; 5 | 6 | import {TransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/TransparentProxyFactory.sol'; 7 | import {Create2Utils} from 'solidity-utils/contracts/utils/ScriptUtils.sol'; 8 | 9 | import {Umbrella} from '../../../src/contracts/umbrella/Umbrella.sol'; 10 | 11 | import {UmbrellaStakeTokenScripts} from './2_UmbrellaStakeToken.s.sol'; 12 | 13 | library UmbrellaScripts { 14 | error ImplementationNotExist(); 15 | 16 | function deployUmbrellaImpl() internal returns (address) { 17 | return Create2Utils.create2Deploy('v1', type(Umbrella).creationCode); 18 | } 19 | 20 | function deployUmbrellaProxy( 21 | address transparentProxyFactory, 22 | IPool pool, 23 | address executor, 24 | address collector 25 | ) internal returns (address) { 26 | address umbrellaImpl = predictUmbrellaImpl(); 27 | address umbrellaStakeTokenImpl = UmbrellaStakeTokenScripts.predictUmbrellaStakeTokenImpl( 28 | transparentProxyFactory, 29 | executor 30 | ); 31 | 32 | require( 33 | umbrellaImpl.code.length != 0 && umbrellaStakeTokenImpl.code.length != 0, 34 | ImplementationNotExist() 35 | ); 36 | 37 | bytes memory data = abi.encodeWithSelector( 38 | Umbrella.initialize.selector, 39 | pool, 40 | executor, 41 | collector, 42 | umbrellaStakeTokenImpl, 43 | transparentProxyFactory 44 | ); 45 | 46 | return 47 | TransparentProxyFactory(transparentProxyFactory).createDeterministic( 48 | umbrellaImpl, 49 | executor, // proxyOwner 50 | data, 51 | 'v1' 52 | ); 53 | } 54 | 55 | function predictUmbrellaImpl() internal pure returns (address) { 56 | return Create2Utils.computeCreate2Address('v1', type(Umbrella).creationCode); 57 | } 58 | 59 | function predictUmbrellaProxy( 60 | address transparentProxyFactory, 61 | IPool pool, 62 | address executor, 63 | address collector 64 | ) internal view returns (address) { 65 | address umbrellaImpl = predictUmbrellaImpl(); 66 | address umbrellaStakeTokenImpl = UmbrellaStakeTokenScripts.predictUmbrellaStakeTokenImpl( 67 | transparentProxyFactory, 68 | executor 69 | ); 70 | 71 | bytes memory data = abi.encodeWithSelector( 72 | Umbrella.initialize.selector, 73 | pool, 74 | executor, 75 | collector, 76 | umbrellaStakeTokenImpl, 77 | transparentProxyFactory 78 | ); 79 | 80 | return 81 | TransparentProxyFactory(transparentProxyFactory).predictCreateDeterministic( 82 | umbrellaImpl, 83 | executor, // proxyOwner 84 | data, 85 | 'v1' 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aave Umbrella 2 | 3 | ![Aave Umbrella Banner](./assets/umbrella_main_banner.jpg) 4 | 5 |
6 | 7 | ## Umbrella core overview 8 | 9 | `Umbrella` is an upgraded version of the Aave Safety Module, based on a staking and slashing mechanism. The primary reserve coverage asset is an `aToken` wrapped in a [`waToken (StataTokenV2)`](https://github.com/bgd-labs/aave-v3-origin/blob/main/src/contracts/extensions/stata-token/README.md). However, assets not directly connected to the Aave ecosystem (such as various LP or staked/restaked tokens) may also be utilized. This system employs a series of contracts designed to semi-automatically address reserve deficits while providing additional rewards to `StakeToken` holders. 10 | 11 |
12 | 13 | ## Umbrella key components 14 | 15 | The `Umbrella` system comprises three main components: 16 | 17 | - [`Umbrella`](/src/contracts/umbrella/README.md): The central contract that orchestrates the system and serves as the entry point for addressing deficits through the slashing mechanism. It incorporates logic for estimating required funds to cover debt, managing system configurations, and creating new `StakeToken`s. The `Umbrella` contract is connected to each `Pool` across all networks to monitor reserve deficits and semi-automatically mitigate them when they occur. 18 | 19 | - [`StakeToken`](/src/contracts/stakeToken/README.md): An upgraded version of the `Aave Stake Token`, responsible for holding reserve assets. These assets can be slashed to cover deficits. Each `StakeToken` maintains an exchange rate tied to its underlying assets, which adjusts based on slashing events. The `StakeToken` is integrated with the reward system via a hook and the `RewardsController` contract. Additionally, it includes logic for fund withdrawals through a cooldown mechanism. For each market protected by the `Umbrella` system, one or more `StakeToken`s will be created. 20 | 21 | - [`RewardsController`](/src/contracts/rewards/README.md): A revised contract designed to allocate incentives to `StakeToken` holders. It facilitates the distribution of rewards and manages the claims process for these stakeholders. The contract supports the distribution of multiple types of rewards (up to eight in the current implementation) and operates as a single instance per network. 22 | 23 |
24 | 25 | ## Setup 26 | 27 | ```sh 28 | cp .env.example .env 29 | forge install 30 | ``` 31 | 32 |
33 | 34 | ## Test 35 | 36 | ```sh 37 | forge test 38 | ``` 39 | 40 |
41 | 42 | ## License 43 | 44 | Copyright © 2025, Aave DAO, represented by its governance smart contracts. 45 | 46 | The [BUSL1.1](./LICENSE) license of this repository allows for any usage of the software, if respecting the Additional Use Grant limitations, forbidding any use case damaging anyhow the Aave DAO's interests. 47 | Interfaces and other components required for integrations are explicitly MIT licensed. 48 | -------------------------------------------------------------------------------- /src/contracts/stewards/interfaces/IDeficitOffsetClinicSteward.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {IPool} from 'aave-v3-origin/contracts/interfaces/IPool.sol'; 5 | 6 | import {IUmbrella} from '../../umbrella/interfaces/IUmbrella.sol'; 7 | 8 | interface IDeficitOffsetClinicSteward { 9 | /** 10 | * @dev Attempted to set zero address. 11 | */ 12 | error ZeroAddress(); 13 | 14 | /** 15 | * @dev Attempted to cover zero deficit. 16 | */ 17 | error DeficitOffsetCannotBeCovered(); 18 | 19 | /** 20 | * @notice Pulls funds to resolve `deficitOffset` on the maximum possible amount. 21 | * @dev If current allowance or treasury balance is less than the `deficitOffsetToCover` the function will revert. 22 | * @param reserve Reserve address 23 | * @return The amount of `deficitOffset` eliminated 24 | */ 25 | function coverDeficitOffset(address reserve) external returns (uint256); 26 | 27 | /** 28 | * @notice Returns the amount of allowance, that can be spent for `deficitOffset` coverage. 29 | * @param reserve Reserve address 30 | * @return The amount of allowance 31 | */ 32 | function getRemainingAllowance(address reserve) external view returns (uint256); 33 | 34 | /** 35 | * @notice Returns the amount of `deficitOffset` that can be covered. 36 | * @param reserve Reserve address 37 | * @return The amount of `deficitOffset` that can be covered 38 | */ 39 | function getDeficitOffsetToCover(address reserve) external view returns (uint256); 40 | 41 | /** 42 | * @notice Returns the amount of already slashed funds that have not yet been used for the deficit elimination. 43 | * @param reserve Address of the `reserve` 44 | * @return The amount of funds pending for deficit elimination 45 | */ 46 | function getPendingDeficit(address reserve) external view returns (uint256); 47 | 48 | /** 49 | * @notice Returns the amount of deficit that can't be slashed using `UmbrellaStakeToken` funds. 50 | * @param reserve Address of the `reserve` 51 | * @return The amount of the `deficitOffset` 52 | */ 53 | function getDeficitOffset(address reserve) external view returns (uint256); 54 | 55 | /** 56 | * @notice Returns the amount of actual reserve deficit at the moment. 57 | * @param reserve Address of the `reserve` 58 | * @return The amount of the `deficitOffset` 59 | */ 60 | function getReserveDeficit(address reserve) external view returns (uint256); 61 | 62 | /** 63 | * @notice Returns the `Umbrella` contract for which this steward instance is configured. 64 | * @return Umbrella address 65 | */ 66 | function UMBRELLA() external view returns (IUmbrella); 67 | 68 | /** 69 | * @notice Returns the Aave Collector from where the funds are pulled. 70 | * @return Treasury address 71 | */ 72 | function TREASURY() external view returns (address); 73 | 74 | /** 75 | * @notice Returns the Aave Pool for which `Umbrella` instance is configured. 76 | * @return Pool address 77 | */ 78 | function POOL() external view returns (IPool); 79 | } 80 | -------------------------------------------------------------------------------- /certora/harness/DummyERC20Impl.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.8.27; 3 | 4 | import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; 5 | 6 | contract DummyERC20Impl is IERC20 { 7 | 8 | 9 | address internal _owner; 10 | 11 | constructor(address owner_){ 12 | _owner = owner_; 13 | } 14 | 15 | modifier onlyOwner() 16 | { 17 | require (msg.sender == _owner, "only owner can access"); 18 | _; 19 | } 20 | 21 | function owner() public view returns (address) { 22 | return _owner; 23 | } 24 | 25 | uint256 t; 26 | mapping (address => uint256) b; 27 | mapping (address => mapping (address => uint256)) a; 28 | 29 | string public name; 30 | string public symbol; 31 | uint public decimals; 32 | 33 | function myAddress() public returns (address) { 34 | return address(this); 35 | } 36 | 37 | function add(uint a, uint b) internal pure returns (uint256) { 38 | uint c = a +b; 39 | require (c >= a); 40 | return c; 41 | } 42 | function sub(uint a, uint b) internal pure returns (uint256) { 43 | require (a>=b); 44 | return a-b; 45 | } 46 | 47 | function totalSupply() public override view returns (uint256) { 48 | return t; 49 | } 50 | function balanceOf(address account) public view override returns (uint256) { 51 | return b[account]; 52 | } 53 | function transfer(address recipient, uint256 amount) external override returns (bool) { 54 | b[msg.sender] = sub(b[msg.sender], amount); 55 | b[recipient] = add(b[recipient], amount); 56 | return true; 57 | } 58 | function safeTransfer(address recipient, uint256 amount) external returns (bool) { 59 | b[msg.sender] = sub(b[msg.sender], amount); 60 | b[recipient] = add(b[recipient], amount); 61 | return true; 62 | } 63 | function allowance(address owner, address spender) external override view returns (uint256) { 64 | return a[owner][spender]; 65 | } 66 | function approve(address spender, uint256 amount) external override returns (bool) { 67 | a[msg.sender][spender] = amount; 68 | return true; 69 | } 70 | 71 | function transferFrom( 72 | address sender, 73 | address recipient, 74 | uint256 amount 75 | ) external override returns (bool) { 76 | b[sender] = sub(b[sender], amount); 77 | b[recipient] = add(b[recipient], amount); 78 | if (sender != recipient) 79 | a[sender][msg.sender] = sub(a[sender][msg.sender], amount); 80 | return true; 81 | } 82 | function safeTransferFrom( 83 | address sender, 84 | address recipient, 85 | uint256 amount 86 | ) external returns (bool) { 87 | b[sender] = sub(b[sender], amount); 88 | b[recipient] = add(b[recipient], amount); 89 | if (sender != recipient) 90 | a[sender][msg.sender] = sub(a[sender][msg.sender], amount); 91 | return true; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/stakeToken/Slashing.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {OwnableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol'; 5 | 6 | import {StakeToken} from '../../src/contracts/stakeToken/StakeToken.sol'; 7 | import {IERC4626StakeToken} from '../../src/contracts/stakeToken/interfaces/IERC4626StakeToken.sol'; 8 | 9 | import {StakeTestBase} from './utils/StakeTestBase.t.sol'; 10 | 11 | contract SlashingTests is StakeTestBase { 12 | function test_slashNotByAdmin(address anyone) external { 13 | vm.assume(anyone != admin && anyone != address(proxyAdminContract)); 14 | 15 | vm.startPrank(anyone); 16 | 17 | vm.expectRevert( 18 | abi.encodeWithSelector( 19 | OwnableUpgradeable.OwnableUnauthorizedAccount.selector, 20 | address(anyone) 21 | ) 22 | ); 23 | stakeToken.slash(user, type(uint256).max); 24 | } 25 | 26 | function test_slash_shouldRevertWithAmountZero() public { 27 | vm.startPrank(admin); 28 | 29 | vm.expectRevert(IERC4626StakeToken.ZeroAmountSlashing.selector); 30 | stakeToken.slash(user, 0); 31 | } 32 | 33 | function test_slash_shouldRevertWithFundsLteMinimum(uint256 amount) public { 34 | amount = bound(amount, 1, stakeToken.MIN_ASSETS_REMAINING()); 35 | 36 | _deposit(amount, user, user); 37 | 38 | vm.startPrank(admin); 39 | 40 | vm.expectRevert(IERC4626StakeToken.ZeroFundsAvailable.selector); 41 | stakeToken.slash(someone, type(uint256).max); 42 | } 43 | 44 | function test_slash(uint192 amountToStake, uint192 amountToSlash) public { 45 | amountToStake = uint192( 46 | bound(amountToStake, stakeToken.MIN_ASSETS_REMAINING() + 1, type(uint192).max) 47 | ); 48 | amountToSlash = uint192( 49 | bound(amountToSlash, 1, amountToStake - stakeToken.MIN_ASSETS_REMAINING()) 50 | ); 51 | 52 | _deposit(amountToStake, user, user); 53 | 54 | vm.startPrank(admin); 55 | 56 | stakeToken.slash(someone, amountToSlash); 57 | 58 | vm.stopPrank(); 59 | 60 | assertEq(underlying.balanceOf(someone), amountToSlash); 61 | assertEq(underlying.balanceOf(address(stakeToken)), amountToStake - amountToSlash); 62 | 63 | assertEq(stakeToken.convertToAssets(stakeToken.balanceOf(user)), amountToStake - amountToSlash); 64 | } 65 | 66 | function test_stakeAfterSlash(uint96 amountToStake, uint96 amountToSlash) public { 67 | amountToStake = uint96( 68 | bound(amountToStake, stakeToken.MIN_ASSETS_REMAINING() + 1, type(uint96).max) 69 | ); 70 | amountToSlash = uint96( 71 | bound(amountToSlash, 1, amountToStake - stakeToken.MIN_ASSETS_REMAINING()) 72 | ); 73 | 74 | _deposit(amountToStake, user, user); 75 | 76 | vm.startPrank(admin); 77 | 78 | stakeToken.slash(someone, amountToSlash); 79 | 80 | vm.stopPrank(); 81 | 82 | _deposit(amountToStake, user, user); 83 | 84 | assertEq(underlying.balanceOf(someone), amountToSlash); 85 | assertEq(underlying.balanceOf(address(stakeToken)), 2 * uint256(amountToStake) - amountToSlash); 86 | 87 | assertEq( 88 | stakeToken.convertToAssets(stakeToken.balanceOf(user)), 89 | 2 * uint256(amountToStake) - amountToSlash 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/umbrella/utils/mocks/MockPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | import {IPool, DataTypes} from 'aave-v3-origin/contracts/interfaces/IPool.sol'; 5 | import {ReserveConfiguration} from 'aave-v3-origin/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; 6 | 7 | import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; 8 | import {MockERC20Permit} from '../../../stakeToken/utils/mock/MockERC20Permit.sol'; 9 | import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; 10 | 11 | interface IMockPool { 12 | function getReserveDeficit(address reserve) external view returns (uint256); 13 | 14 | function eliminateReserveDeficit(address reserve, uint256 amount) external returns (uint256); 15 | 16 | function ADDRESSES_PROVIDER() external returns (address); 17 | } 18 | 19 | contract MockPool is IMockPool { 20 | using SafeERC20 for IERC20; 21 | using ReserveConfiguration for DataTypes.ReserveConfigurationMap; 22 | 23 | address private immutable _POOL_ADDRESSES_PROVIDER; 24 | 25 | mapping(address reserve => uint256 deficit) _deficit; 26 | mapping(address reserve => address aTokens) _aTokens; 27 | mapping(address reserve => DataTypes.ReserveConfigurationMap config) _configs; 28 | 29 | bool deactivateReserve; 30 | 31 | constructor(address mockPoolAddressesProvider) { 32 | _POOL_ADDRESSES_PROVIDER = mockPoolAddressesProvider; 33 | } 34 | 35 | function ADDRESSES_PROVIDER() external view returns (address) { 36 | return _POOL_ADDRESSES_PROVIDER; 37 | } 38 | 39 | function addReserveDeficit(address reserve, uint256 amount) external { 40 | _deficit[reserve] += amount; 41 | } 42 | 43 | function eliminateReserveDeficit(address reserve, uint256 amount) external returns (uint256) { 44 | if (_isVirtualAcc(reserve)) { 45 | MockERC20Permit(_aTokens[reserve]).burn(msg.sender, amount); 46 | } else { 47 | IERC20(reserve).safeTransferFrom(msg.sender, address(this), amount); 48 | } 49 | 50 | _deficit[reserve] -= amount; 51 | return amount; 52 | } 53 | 54 | function activateVirtualAcc(address reserve, bool flag) external { 55 | DataTypes.ReserveConfigurationMap memory config = DataTypes.ReserveConfigurationMap(0); 56 | 57 | config.setVirtualAccActive(flag); 58 | 59 | _configs[reserve] = config; 60 | } 61 | 62 | function setATokenForReserve(address reserve, address aToken) external { 63 | _aTokens[reserve] = aToken; 64 | } 65 | 66 | function getReserveDeficit(address reserve) external view returns (uint256) { 67 | return _deficit[reserve]; 68 | } 69 | 70 | function getConfiguration( 71 | address reserve 72 | ) external view returns (DataTypes.ReserveConfigurationMap memory) { 73 | uint256 add = deactivateReserve ? 0 : 1; 74 | return DataTypes.ReserveConfigurationMap(_configs[reserve].data + add); 75 | } 76 | 77 | function getReserveAToken(address reserve) external view returns (address) { 78 | return _aTokens[reserve]; 79 | } 80 | 81 | function _isVirtualAcc(address reserve) internal view returns (bool) { 82 | return _configs[reserve].getIsVirtualAccActive(); 83 | } 84 | 85 | function switchReserve() external { 86 | deactivateReserve = true; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/contracts/payloads/UmbrellaExtendedPayload.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {Address} from 'openzeppelin-contracts/contracts/utils/Address.sol'; 5 | 6 | import {UmbrellaBasePayload} from './UmbrellaBasePayload.sol'; 7 | import {IUmbrellaEngineStructs as IStructs} from './IUmbrellaEngineStructs.sol'; 8 | 9 | import {IUmbrellaConfigEngine as IEngine} from './configEngine/IUmbrellaConfigEngine.sol'; 10 | 11 | /** 12 | * @title UmbrellaExtendedPayload 13 | * @notice A contract for advanced automated configurations, including token creation, reward setup, and slashing configurations. 14 | * @dev Allows creating tokens from scratch, configuring rewards, and setting `SlashingConfig`s. 15 | * Also supports removing slashing configurations and stopping all associated reward distributions. 16 | * 17 | * ***IMPORTANT*** Payload inheriting this `UmbrellaExtendedPayload` MUST BE STATELESS always. 18 | * 19 | * At this moment extended payload in addition to the base covering: 20 | * - Complex token removals (deleting `SlashingConfig`s, stopping all reward distributions) 21 | * - Complex token creations (deploying of tokens, setting `SlashingConfig`s, initializing reward distributions and setting `deficitOffset`) 22 | * 23 | * @author BGD labs 24 | */ 25 | abstract contract UmbrellaExtendedPayload is UmbrellaBasePayload { 26 | using Address for address; 27 | 28 | constructor(address umbrellaConfigEngine) UmbrellaBasePayload(umbrellaConfigEngine) {} 29 | 30 | /// @dev Functions to be overridden on the child 31 | ///////////////////////////////////////////////////////////////////////////////////////// 32 | 33 | /** 34 | * @notice Performs the following operations: 35 | * - Removes `SlashingConfig` inside `Umbrella` and corresponding reserve 36 | * - Stops the distribution of all rewards set inside `RewardsController` 37 | * - Sets `cooldown` to 0 and `unstakeWindow` to maximum possible value 38 | */ 39 | function complexTokenRemovals() public view virtual returns (IStructs.TokenRemoval[] memory) {} 40 | 41 | /** 42 | * @notice Performs the following operations: 43 | * - Creates new `UmbrellaStakeToken`s 44 | * - Sets `SlashingConfig` for reserves and newly created tokens 45 | * - Sets the specified rewards and `targetLiquidity` for `UmbrellaStakeToken`s 46 | * - Increases `deficitOffset` if non-zero value is specified 47 | */ 48 | function complexTokenCreations() public view virtual returns (IStructs.TokenSetup[] memory) {} 49 | 50 | ///////////////////////////////////////////////////////////////////////////////////////// 51 | 52 | function _extendedExecute() internal override { 53 | IStructs.TokenRemoval[] memory complexTokenRemovalConfigs = complexTokenRemovals(); 54 | IStructs.TokenSetup[] memory complexTokenSetupConfigs = complexTokenCreations(); 55 | 56 | if (complexTokenRemovalConfigs.length != 0) { 57 | ENGINE.functionDelegateCall( 58 | abi.encodeWithSelector(IEngine.executeComplexRemovals.selector, complexTokenRemovalConfigs) 59 | ); 60 | } 61 | 62 | if (complexTokenSetupConfigs.length != 0) { 63 | ENGINE.functionDelegateCall( 64 | abi.encodeWithSelector(IEngine.executeComplexCreations.selector, complexTokenSetupConfigs) 65 | ); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/certora.yml: -------------------------------------------------------------------------------- 1 | name: certora 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | pull_request: 9 | branches: 10 | - certora 11 | - main 12 | push: 13 | branches: 14 | - main 15 | 16 | workflow_dispatch: 17 | 18 | jobs: 19 | verify: 20 | runs-on: ubuntu-latest 21 | if: 22 | github.event.pull_request.head.repo.full_name == github.repository || (github.event_name == 'push' && 23 | github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) 24 | permissions: 25 | contents: read 26 | statuses: write 27 | pull-requests: write 28 | steps: 29 | - uses: actions/checkout@v4 30 | with: 31 | submodules: recursive 32 | - uses: Certora/certora-run-action@v1 33 | with: 34 | cli-version: 7.29.3 35 | configurations: |- 36 | certora/conf/stakeToken/rules.conf 37 | certora/conf/stakeToken/invariants.conf 38 | certora/conf/rewards/mirrors.conf 39 | certora/conf/rewards/invariants.conf --rule distributionEnd_NEQ_0 40 | certora/conf/rewards/invariants.conf --rule all_rewars_are_different 41 | certora/conf/rewards/invariants.conf --rule same_distributionEnd_values 42 | certora/conf/rewards/invariants.conf --rule lastUpdateTimestamp_LEQ_current_time 43 | certora/conf/rewards/invariants.conf --rule accrued_is_0_for_non_existing_reward 44 | certora/conf/rewards/invariants.conf --rule userIndex_is_0_for_non_existing_reward 45 | certora/conf/rewards/invariants.conf --rule distributionEnd_is_0_for_non_existing_reward 46 | certora/conf/rewards/invariants.conf --rule rewardIndex_is_0_for_non_existing_reward 47 | certora/conf/rewards/invariants.conf --rule userIndex_LEQ_rewardIndex 48 | certora/conf/rewards/invariants.conf --rule targetLiquidity_NEQ_0 49 | certora/conf/rewards/double_reward.conf 50 | certora/conf/rewards/single_reward.conf --exclude_rule bob_cant_DOS_alice_to_claim bob_cant_DOS_alice_to_claim__claimSelectedRewards bob_cant_DOS_alice_to_claim__claimAllRewards bob_cant_affect_the_claimed_amount_of_alice 51 | certora/conf/rewards/single_reward-depth0.conf --rule bob_cant_DOS_alice_to_claim 52 | certora/conf/rewards/single_reward-depth0.conf --rule bob_cant_DOS_alice_to_claim__claimAllRewards 53 | certora/conf/rewards/single_reward-depth0.conf --rule bob_cant_DOS_alice_to_claim__claimSelectedRewards 54 | certora/conf/rewards/single_reward-special_config.conf --rule bob_cant_affect_the_claimed_amount_of_alice 55 | certora/conf/umbrella/invariants.conf 56 | certora/conf/umbrella/Umbrella.conf --rule slashing_cant_DOS_other_functions 57 | certora/conf/umbrella/Umbrella.conf --rule slashing_cant_DOS__coverDeficitOffset 58 | certora/conf/umbrella/Umbrella.conf --exclude_rule slashing_cant_DOS_other_functions slashing_cant_DOS__coverDeficitOffset 59 | solc-versions: 0.8.27 60 | comment-fail-only: false 61 | solc-remove-version-prefix: "0." 62 | job-name: "Certora Prover Run" 63 | certora-key: ${{ secrets.CERTORAKEY }} 64 | install-java: true 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | -------------------------------------------------------------------------------- /tests/stakeToken/Invariants.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {StakeTestBase} from './utils/StakeTestBase.t.sol'; 5 | 6 | contract InvariantTest is StakeTestBase { 7 | function test_exchangeRateAfterSlashingAlwaysIncreasing( 8 | uint192 amountToDeposit, 9 | uint192 amountToSlash 10 | ) external { 11 | amountToDeposit = uint96( 12 | bound(amountToDeposit, stakeToken.MIN_ASSETS_REMAINING() + 1, type(uint96).max) 13 | ); 14 | amountToSlash = uint96( 15 | bound(amountToSlash, 1, amountToDeposit - stakeToken.MIN_ASSETS_REMAINING()) 16 | ); 17 | 18 | _deposit(amountToDeposit, user, user); 19 | 20 | uint256 defaultExchangeRate = stakeToken.previewDeposit(1); 21 | 22 | vm.startPrank(admin); 23 | 24 | stakeToken.slash(someone, amountToSlash); 25 | 26 | uint256 newExchangeRate = stakeToken.previewDeposit(1); 27 | 28 | assertLe(defaultExchangeRate, newExchangeRate); 29 | } 30 | 31 | function test_dataShouldBeNotUpdatedDuringDeposit() external { 32 | _deposit(1 ether, user, user); 33 | 34 | assertEq(mockRewardsController.lastTotalAssets(), 0); 35 | assertEq(mockRewardsController.lastTotalSupply(), 0); 36 | 37 | uint256 totalAssets = stakeToken.totalAssets(); 38 | uint256 totalSupply = stakeToken.totalSupply(); 39 | 40 | assertNotEq(totalAssets, 0); 41 | assertNotEq(totalSupply, 0); 42 | 43 | _deposit(1 ether, user, user); 44 | 45 | assertEq(mockRewardsController.lastTotalAssets(), totalAssets); 46 | assertEq(mockRewardsController.lastTotalSupply(), totalSupply); 47 | 48 | assertNotEq(totalAssets, stakeToken.totalAssets()); 49 | assertNotEq(totalSupply, stakeToken.totalSupply()); 50 | 51 | assertEq(mockRewardsController.lastUser(), user); 52 | assertEq(mockRewardsController.lastUserBalance(), stakeToken.convertToShares(1 ether)); 53 | } 54 | 55 | function test_dataShouldBeNotUpdatedDuringWithdraw() external { 56 | _deposit(1 ether, user, user); 57 | 58 | uint256 newtotalAssets = stakeToken.totalAssets(); 59 | uint256 newtotalSupply = stakeToken.totalSupply(); 60 | 61 | vm.startPrank(user); 62 | stakeToken.cooldown(); 63 | 64 | skip(stakeToken.getCooldown()); 65 | 66 | stakeToken.withdraw(0.5 ether, user, user); 67 | 68 | assertEq(mockRewardsController.lastTotalAssets(), newtotalAssets); 69 | assertEq(mockRewardsController.lastTotalSupply(), newtotalSupply); 70 | 71 | assertEq(mockRewardsController.lastUser(), user); 72 | assertEq(mockRewardsController.lastUserBalance(), 1 ether); 73 | 74 | assertNotEq(newtotalAssets, stakeToken.totalAssets()); 75 | assertNotEq(newtotalSupply, stakeToken.totalSupply()); 76 | 77 | assertEq(mockRewardsController.lastUser(), user); 78 | assertEq(mockRewardsController.lastUserBalance(), stakeToken.convertToShares(1 ether)); 79 | } 80 | 81 | function test_dataShouldNotBeUpdatedDuringSlash() external { 82 | _deposit(1 ether, user, user); 83 | 84 | uint256 newtotalSupply = stakeToken.totalSupply(); 85 | uint256 newtotalAssets = stakeToken.totalAssets(); 86 | 87 | assertEq(0, mockRewardsController.lastTotalSupply()); 88 | assertEq(0, mockRewardsController.lastTotalAssets()); 89 | 90 | vm.startPrank(admin); 91 | stakeToken.slash(someone, 0.5 ether); 92 | 93 | assertEq(mockRewardsController.lastTotalSupply(), newtotalSupply); 94 | assertEq(mockRewardsController.lastTotalAssets(), newtotalAssets); 95 | 96 | assertEq(address(0), mockRewardsController.lastUser()); 97 | assertEq(0, mockRewardsController.lastUserBalance()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/contracts/payloads/IUmbrellaEngineStructs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {IUmbrellaStkManager} from '../umbrella/interfaces/IUmbrellaStkManager.sol'; 5 | import {IUmbrellaConfiguration} from '../umbrella/interfaces/IUmbrellaConfiguration.sol'; 6 | 7 | import {IRewardsStructs} from '../rewards/interfaces/IRewardsStructs.sol'; 8 | 9 | /** 10 | * @title IUmbrellaEngineStructs interface 11 | * @notice An interface containing structures that can be used externally. 12 | * @author BGD labs 13 | */ 14 | interface IUmbrellaEngineStructs { 15 | /// Umbrella 16 | ///////////////////////////////////////////////////////////////////////////////////////// 17 | 18 | struct UnstakeConfig { 19 | /// @notice Address of the `UmbrellaStakeToken` to be configured 20 | address umbrellaStake; 21 | /// @notice New duration of `cooldown` to be set (could be set as `KEEP_CURRENT`) 22 | uint256 newCooldown; 23 | /// @notice New duration of `unstakeWindow` to be set (could be set as `KEEP_CURRENT`) 24 | uint256 newUnstakeWindow; 25 | } 26 | 27 | struct SetDeficitOffset { 28 | /// @notice Reserve address 29 | address reserve; 30 | /// @notice New amount of `deficitOffset` to set for this reserve 31 | uint256 newDeficitOffset; 32 | } 33 | 34 | struct CoverDeficit { 35 | /// @notice Reserve address 36 | address reserve; 37 | /// @notice Amount of `aToken`s (or reserve) to be eliminated 38 | uint256 amount; 39 | /// @notice True - make `forceApprove` for required amount of tokens, false - skip 40 | bool approve; 41 | } 42 | 43 | /// RewardsController 44 | ///////////////////////////////////////////////////////////////////////////////////////// 45 | 46 | struct ConfigureStakeAndRewardsConfig { 47 | /// @notice Address of the `asset` to be configured/initialized 48 | address umbrellaStake; 49 | /// @notice Amount of liquidity where will be the maximum emission of rewards per second applied (could be set as KEEP_CURRENT) 50 | uint256 targetLiquidity; 51 | /// @notice Optional array of reward configs, can be empty 52 | IRewardsStructs.RewardSetupConfig[] rewardConfigs; 53 | } 54 | 55 | struct ConfigureRewardsConfig { 56 | /// @notice Address of the `asset` whose reward should be configured 57 | address umbrellaStake; 58 | /// @notice Array of structs with params to set 59 | IRewardsStructs.RewardSetupConfig[] rewardConfigs; 60 | } 61 | 62 | /// Structs for extended payloads 63 | ///////////////////////////////////////////////////////////////////////////////////////// 64 | 65 | struct TokenRemoval { 66 | /// @notice Address of the `UmbrellaStakeToken` which should be removed from the system 67 | address umbrellaStake; 68 | /// @notice Address that must transfer all rewards that have not been received by users 69 | address residualRewardPayer; 70 | } 71 | 72 | struct TokenSetup { 73 | /// @notice `UmbrellaStakeToken`s setup config 74 | IUmbrellaStkManager.StakeTokenSetup stakeSetup; 75 | /// @notice Amount of liquidity where will be the maximum emission of rewards per second applied 76 | uint256 targetLiquidity; 77 | /// @notice Optional array of reward configs, can be empty 78 | IRewardsStructs.RewardSetupConfig[] rewardConfigs; 79 | /// @notice Reserve address 80 | address reserve; 81 | /// @notice Percentage of funds slashed on top of the new deficit 82 | uint256 liquidationFee; 83 | /// @notice Oracle of `UmbrellaStakeToken`s underlying 84 | address umbrellaStakeUnderlyingOracle; 85 | /// @notice The value by which `deficitOffset` will be increased 86 | uint256 deficitOffsetIncrease; 87 | } 88 | 89 | ///////////////////////////////////////////////////////////////////////////////////////// 90 | } 91 | -------------------------------------------------------------------------------- /tests/stakeToken/Pause.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {OwnableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol'; 5 | import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol'; 6 | import {ERC4626Upgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC4626Upgradeable.sol'; 7 | 8 | import {StakeTestBase} from './utils/StakeTestBase.t.sol'; 9 | 10 | contract PauseTests is StakeTestBase { 11 | function test_setPauseByAdmin() external { 12 | assertEq(PausableUpgradeable(address(stakeToken)).paused(), false); 13 | 14 | vm.startPrank(admin); 15 | 16 | stakeToken.pause(); 17 | 18 | assertEq(PausableUpgradeable(address(stakeToken)).paused(), true); 19 | 20 | stakeToken.unpause(); 21 | 22 | assertEq(PausableUpgradeable(address(stakeToken)).paused(), false); 23 | } 24 | 25 | function test_setPauseNotByAdmin(address anyone) external { 26 | vm.assume(anyone != admin && anyone != address(proxyAdminContract)); 27 | 28 | assertEq(PausableUpgradeable(address(stakeToken)).paused(), false); 29 | 30 | vm.startPrank(anyone); 31 | 32 | vm.expectRevert( 33 | abi.encodeWithSelector( 34 | OwnableUpgradeable.OwnableUnauthorizedAccount.selector, 35 | address(anyone) 36 | ) 37 | ); 38 | stakeToken.pause(); 39 | } 40 | 41 | function test_shouldRevertWhenPauseIsActive() external { 42 | _deposit(1e18, user, user); 43 | _dealUnderlying(1e18, user); 44 | 45 | vm.startPrank(user); 46 | 47 | stakeToken.cooldown(); 48 | stakeToken.approve(someone, 1000); 49 | stakeToken.setCooldownOperator(someone, true); 50 | underlying.approve(address(stakeToken), 1e18); 51 | 52 | skip(stakeToken.getCooldown()); 53 | 54 | assertNotEq(stakeToken.maxDeposit(user), 0); 55 | assertNotEq(stakeToken.maxMint(user), 0); 56 | assertNotEq(stakeToken.maxWithdraw(user), 0); 57 | assertNotEq(stakeToken.maxRedeem(user), 0); 58 | assertNotEq(stakeToken.getMaxSlashableAssets(), 0); 59 | 60 | vm.stopPrank(); 61 | vm.startPrank(admin); 62 | 63 | stakeToken.pause(); 64 | 65 | vm.stopPrank(); 66 | vm.startPrank(someone); 67 | 68 | vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); 69 | stakeToken.cooldownOnBehalfOf(user); 70 | 71 | vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); 72 | stakeToken.transferFrom(user, someone, 1); 73 | 74 | vm.stopPrank(); 75 | vm.startPrank(user); 76 | 77 | vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); 78 | stakeToken.cooldown(); 79 | 80 | // error is another one, due to zero-liq checks before pause check 81 | vm.expectRevert( 82 | abi.encodeWithSelector(ERC4626Upgradeable.ERC4626ExceededMaxDeposit.selector, user, 1, 0) 83 | ); 84 | stakeToken.deposit(1, user); 85 | 86 | vm.expectRevert( 87 | abi.encodeWithSelector(ERC4626Upgradeable.ERC4626ExceededMaxMint.selector, user, 1, 0) 88 | ); 89 | stakeToken.mint(1, user); 90 | 91 | vm.expectRevert( 92 | abi.encodeWithSelector(ERC4626Upgradeable.ERC4626ExceededMaxRedeem.selector, user, 1, 0) 93 | ); 94 | stakeToken.redeem(1, user, user); 95 | 96 | vm.expectRevert( 97 | abi.encodeWithSelector(ERC4626Upgradeable.ERC4626ExceededMaxWithdraw.selector, user, 1, 0) 98 | ); 99 | stakeToken.withdraw(1, user, user); 100 | 101 | assertEq(stakeToken.maxDeposit(user), 0); 102 | assertEq(stakeToken.maxMint(user), 0); 103 | assertEq(stakeToken.maxWithdraw(user), 0); 104 | assertEq(stakeToken.maxRedeem(user), 0); 105 | assertEq(stakeToken.getMaxSlashableAssets(), 0); 106 | 107 | vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); 108 | stakeToken.transfer(someone, 1); 109 | 110 | vm.stopPrank(); 111 | vm.startPrank(admin); 112 | 113 | vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); 114 | stakeToken.slash(someone, 1); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /certora/specs/umbrella/setup.spec: -------------------------------------------------------------------------------- 1 | import "Pool.spec"; 2 | 3 | using UmbrellaStakeTokenA as StakeTokenA; 4 | 5 | using ERC20A as erc20A; 6 | using ERC20B as erc20B; 7 | 8 | methods { 9 | function StakeTokenA.eip712Domain() external 10 | returns(bytes1,string memory,string memory,uint256,address,bytes32,uint256[]) => NONDET DELETE; 11 | 12 | function StakeTokenA.name() external returns(string memory) => NONDET DELETE; 13 | 14 | function _.latestAnswer() external with (env e) => latestAnswerCVL(/*calledContract,*/e.block.timestamp) expect int256; 15 | 16 | /// This one created a new contract, which we ignore. 17 | function _.createDeterministic(address logic, address initialOwner, bytes data, bytes32 salt) external => 18 | determinsticAddress(logic, initialOwner, data, salt) expect address; 19 | 20 | /// This one just predicts the expected created contract's address. Should conform to the value of 'createDeterministic'. 21 | function _.predictCreateDeterministic(address logic, address initialOwner, bytes data, bytes32 salt) external => 22 | determinsticAddress(logic, initialOwner, data, salt) expect address; 23 | 24 | function Math.mulDiv(uint256 x, uint256 y, uint256 denominator) internal returns uint256 => mulDivDownCVL_pessim(x,y,denominator); 25 | 26 | function UmbrellaStkManager._getStakeNameAndSymbol(address,string calldata) internal returns (string memory, string memory) => randomStakeNameAndSymbol(); 27 | } 28 | 29 | // ==== envfree methods =========================================================== 30 | methods { 31 | function getDeficitOffset(address reserve) external returns (uint256) envfree; 32 | function getPendingDeficit(address reserve) external returns (uint256) envfree; 33 | function SLASHED_FUNDS_RECIPIENT() external returns (address) envfree; 34 | function get_is_virtual_active(address reserve) external returns (bool) envfree; 35 | function getReserveSlashingConfigs(address) external returns (IUmbrellaConfiguration.SlashingConfig[]) envfree; 36 | 37 | function erc20A.totalSupply() external returns (uint256) envfree; 38 | function erc20A.balanceOf(address account) external returns (uint256) envfree; 39 | function erc20A.allowance(address,address) external returns (uint256) envfree; 40 | function erc20B.totalSupply() external returns (uint256) envfree; 41 | function erc20B.balanceOf(address account) external returns (uint256) envfree; 42 | function erc20B.allowance(address,address) external returns (uint256) envfree; 43 | 44 | function StakeTokenA.asset() external returns (address) envfree; 45 | } 46 | 47 | 48 | ghost latestAnswerCVL(uint256 /*timestamp*/) returns int256; 49 | 50 | persistent ghost createDeterministicAddress(address,address,bytes32,bytes32) returns address 51 | { 52 | axiom forall address logic. forall address initialOwner. forall bytes32 dataHash. forall bytes32 salt. 53 | createDeterministicAddress(logic, initialOwner, dataHash, salt) != 0; 54 | /* 55 | /// Injectivity axiom, use only if necessary 56 | axiom forall address logic. forall address initialOwner. forall bytes32 dataHash. forall bytes32 salt. 57 | forall address logicA. forall address initialOwnerA. forall bytes32 dataHashA. forall bytes32 saltA. 58 | createDeterministicAddress(logic, initialOwner, dataHash, salt) == 59 | createDeterministicAddress(logicA, initialOwnerA, dataHashA, saltA) => 60 | logicA == logic && initialOwner == initialOwnerA && dataHash == dataHashA && salt == saltA; 61 | */ 62 | } 63 | 64 | function determinsticAddress(address logic, address initialOwner, bytes data, bytes32 salt) returns address { 65 | bytes32 dataHash = keccak256(data); 66 | return createDeterministicAddress(logic,initialOwner,dataHash,salt); 67 | } 68 | 69 | function mulDivDownCVL_pessim(uint256 x, uint256 y, uint256 z) returns uint256 { 70 | assert z !=0, "mulDivDown error: cannot divide by zero"; 71 | return require_uint256(x * y / z); 72 | } 73 | 74 | function randomStakeNameAndSymbol() returns (string, string) { 75 | string name; require name.length <= 32; 76 | string symbol; require symbol.length <= 32; 77 | return (name, symbol); 78 | } 79 | -------------------------------------------------------------------------------- /tests/stakeToken/ExchangeRate.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {SafeCast} from 'openzeppelin-contracts/contracts/utils/math/SafeCast.sol'; 5 | 6 | import {IRewardsController} from '../../src/contracts/rewards/interfaces/IRewardsController.sol'; 7 | 8 | import {StakeTestBase} from './utils/StakeTestBase.t.sol'; 9 | 10 | contract ExchangeRateTest is StakeTestBase { 11 | using SafeCast for uint256; 12 | 13 | function test_precisionLossWithSlash(uint192 assets, uint192 assetsToSlash) public { 14 | assets = uint192(bound(assets, stakeToken.MIN_ASSETS_REMAINING() + 1, type(uint192).max)); 15 | assetsToSlash = uint192(bound(assetsToSlash, 1, assets - stakeToken.MIN_ASSETS_REMAINING())); 16 | 17 | uint256 shares = stakeToken.previewDeposit(assets); 18 | 19 | _deposit(assets, user, user); 20 | 21 | vm.startPrank(admin); 22 | 23 | stakeToken.slash(someone, assetsToSlash); 24 | 25 | vm.stopPrank(); 26 | 27 | uint192 assetsAfterRedeem = stakeToken.previewRedeem(shares).toUint192(); 28 | 29 | assertLe(assetsAfterRedeem, assets); 30 | 31 | assertLe(getDiff(assetsAfterRedeem, assets - assetsToSlash), 1); 32 | } 33 | 34 | function test_precisionLossStartingWithAssets( 35 | uint192 assetsToStake, 36 | uint192 assetsToCheck 37 | ) public { 38 | assetsToCheck = uint192(bound(assetsToCheck, 1, type(uint192).max - 1)); 39 | assetsToStake = uint192(bound(assetsToStake, assetsToCheck + 1, type(uint192).max)); 40 | 41 | _deposit(assetsToStake, user, user); 42 | 43 | uint256 sharesFromDeposit = stakeToken.previewDeposit(assetsToCheck); 44 | uint256 assetsFromMint = stakeToken.previewMint(sharesFromDeposit); 45 | 46 | assertLe(getDiff(assetsToCheck, assetsFromMint), 1); 47 | 48 | uint256 sharesFromWithdrawal = stakeToken.previewWithdraw(assetsToCheck); 49 | uint256 assetsFromRedeem = stakeToken.previewRedeem(sharesFromWithdrawal); 50 | 51 | assertLe(getDiff(assetsToCheck, assetsFromRedeem), 1); 52 | } 53 | 54 | function test_precisionLossStartingWithShares( 55 | uint192 assetsToStake, 56 | uint224 sharesToCheck 57 | ) public { 58 | sharesToCheck = uint192(bound(sharesToCheck, sharesMultiplier(), type(uint192).max)); 59 | assetsToStake = uint192( 60 | bound(assetsToStake, stakeToken.convertToAssets(sharesToCheck), type(uint192).max) 61 | ); 62 | 63 | _deposit(assetsToStake, user, user); 64 | 65 | uint256 assetsFromMint = stakeToken.previewMint(sharesToCheck); 66 | uint256 sharesFromDeposit = stakeToken.previewDeposit(assetsFromMint); 67 | 68 | assertLe(getDiff(sharesToCheck, sharesFromDeposit), 1000); 69 | 70 | uint256 assetsFromRedeem = stakeToken.previewRedeem(sharesToCheck); 71 | uint256 sharesFromWithdrawal = stakeToken.previewWithdraw(assetsFromRedeem); 72 | 73 | assertLe(getDiff(sharesToCheck, sharesFromWithdrawal), 1000); 74 | } 75 | 76 | function test_precisionLossCombinedTest( 77 | uint96 assets, 78 | uint96 assetsToSlash, 79 | uint96 assetsToCheck 80 | ) public { 81 | assets = uint96(bound(assets, stakeToken.MIN_ASSETS_REMAINING() + 1, type(uint96).max)); 82 | assetsToSlash = uint96(bound(assetsToSlash, 1, assets - stakeToken.MIN_ASSETS_REMAINING())); 83 | assetsToCheck = uint96(bound(assetsToCheck, 1, type(uint96).max)); 84 | 85 | stakeToken.previewDeposit(assets); 86 | 87 | _deposit(assets, user, user); 88 | 89 | vm.startPrank(admin); 90 | 91 | stakeToken.slash(someone, assetsToSlash); 92 | 93 | vm.stopPrank(); 94 | 95 | uint256 sharesFromDeposit_1 = stakeToken.previewDeposit(assetsToCheck); 96 | uint256 assetsFromMint_1 = stakeToken.previewMint(sharesFromDeposit_1); 97 | 98 | assertEq(assetsToCheck - assetsFromMint_1, 0); 99 | 100 | uint256 sharesFromWithdrawal_1 = stakeToken.previewWithdraw(assetsToCheck); 101 | uint256 assetsFromRedeem_1 = stakeToken.previewRedeem(sharesFromWithdrawal_1); 102 | 103 | assertEq(assetsToCheck - assetsFromRedeem_1, 0); 104 | 105 | // check, cause they have different rounding, but same convertToShares with the same assets started 106 | assertLe(getDiff(sharesFromDeposit_1, sharesFromWithdrawal_1), 1); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /assets/umbrella.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /tests/stakeToken/utils/StakeTestBase.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import 'forge-std/Test.sol'; 5 | 6 | import {VmSafe} from 'forge-std/Vm.sol'; 7 | 8 | import {IERC20Metadata} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; 9 | 10 | import {TransparentUpgradeableProxy} from 'openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol'; 11 | 12 | import {StakeToken} from '../../../src/contracts/stakeToken/StakeToken.sol'; 13 | import {IRewardsController} from '../../../src/contracts/rewards/interfaces/IRewardsController.sol'; 14 | 15 | import {MockERC20Permit} from './mock/MockERC20Permit.sol'; 16 | import {MockRewardsController} from './mock/MockRewardsController.sol'; 17 | 18 | contract StakeTestBase is Test { 19 | address public admin = vm.addr(0x1000); 20 | 21 | uint256 public userPrivateKey = 0x3000; 22 | address public user = vm.addr(userPrivateKey); 23 | 24 | address public someone = vm.addr(0x4000); 25 | 26 | address proxyAdmin = vm.addr(0x5000); 27 | address proxyAdminContract; 28 | 29 | IERC20Metadata public underlying; 30 | StakeToken public stakeToken; 31 | 32 | MockRewardsController public mockRewardsController; 33 | 34 | function setUp() public virtual { 35 | _setupProtocol(); 36 | _setupStakeToken(address(underlying)); 37 | } 38 | 39 | function _setupStakeToken(address stakeTokenUnderlying) internal { 40 | StakeToken stakeTokenImpl = new StakeToken(IRewardsController(address(mockRewardsController))); 41 | stakeToken = StakeToken( 42 | address( 43 | new TransparentUpgradeableProxy( 44 | address(stakeTokenImpl), 45 | proxyAdmin, 46 | abi.encodeWithSelector( 47 | StakeToken.initialize.selector, 48 | address(stakeTokenUnderlying), 49 | 'Stake Test', 50 | 'stkTest', 51 | admin, 52 | 15 days, 53 | 2 days 54 | ) 55 | ) 56 | ) 57 | ); 58 | 59 | proxyAdminContract = _predictProxyAdminAddress(address(stakeToken)); 60 | } 61 | 62 | function _setupProtocol() internal { 63 | mockRewardsController = new MockRewardsController(); 64 | 65 | underlying = new MockERC20Permit('MockToken', 'MTK'); 66 | } 67 | 68 | function _dealUnderlying(uint256 amount, address actor) internal { 69 | deal(address(underlying), actor, amount); 70 | } 71 | 72 | function _deposit( 73 | uint256 amountOfAsset, 74 | address actor, 75 | address receiver 76 | ) internal returns (uint256) { 77 | _dealUnderlying(amountOfAsset, actor); 78 | 79 | vm.startPrank(actor); 80 | 81 | IERC20Metadata(stakeToken.asset()).approve(address(stakeToken), amountOfAsset); 82 | uint256 shares = stakeToken.deposit(amountOfAsset, receiver); 83 | 84 | vm.stopPrank(); 85 | 86 | return shares; 87 | } 88 | 89 | function _mint( 90 | uint256 amountOfShares, 91 | address actor, 92 | address receiver 93 | ) internal returns (uint256) { 94 | uint256 amountOfAssets = stakeToken.previewMint(amountOfShares); 95 | 96 | _dealUnderlying(amountOfAssets, actor); 97 | 98 | vm.startPrank(actor); 99 | 100 | IERC20Metadata(stakeToken.asset()).approve(address(stakeToken), amountOfAssets); 101 | uint256 assets = stakeToken.mint(amountOfShares, receiver); 102 | 103 | vm.stopPrank(); 104 | 105 | return assets; 106 | } 107 | 108 | function sharesMultiplier() internal pure returns (uint256) { 109 | return 10 ** _decimalsOffset(); 110 | } 111 | 112 | function _decimalsOffset() internal pure returns (uint256) { 113 | return 0; 114 | } 115 | 116 | function getDiff(uint256 a, uint256 b) internal pure returns (uint256) { 117 | return a > b ? a - b : b - a; 118 | } 119 | 120 | function _predictProxyAdminAddress(address proxy) internal pure virtual returns (address) { 121 | return 122 | address( 123 | uint160( 124 | uint256( 125 | keccak256( 126 | abi.encodePacked( 127 | bytes1(0xd6), // RLP prefix for a list with total length 22 128 | bytes1(0x94), // RLP prefix for an address (20 bytes) 129 | proxy, // 20-byte address 130 | uint8(1) // 1-byte nonce 131 | ) 132 | ) 133 | ) 134 | ) 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /certora/specs/stakeToken/base.spec: -------------------------------------------------------------------------------- 1 | using DummyERC20Impl as stake_token; 2 | 3 | methods { 4 | function stake_token.balanceOf(address) external returns (uint256) envfree; 5 | function stake_token.totalSupply() external returns (uint256) envfree; 6 | 7 | function balanceOf(address) external returns (uint256) envfree; 8 | function totalSupply() external returns (uint256) envfree; 9 | function totalAssets() external returns (uint256) envfree; 10 | function previewRedeem(uint256) external returns (uint256) envfree; 11 | function get_maxSlashable() external returns (uint256) envfree; 12 | function getCooldown() external returns (uint256) envfree; 13 | function getUnstakeWindow() external returns (uint256) envfree; 14 | function cooldownAmount(address) external returns (uint192) envfree; 15 | function cooldownEndOfCooldown(address) external returns (uint32) envfree; 16 | function cooldownWithdrawalWindow(address user) external returns (uint32) envfree; 17 | function paused() external returns (bool) envfree; 18 | function asset() external returns (address) envfree; 19 | function stake_token.balanceOf(address) external returns (uint256) envfree; 20 | function maxRescue(address erc20Token) external returns (uint256) envfree; 21 | function allowance(address owner, address spender) external returns (uint256) envfree; 22 | function owner() external returns (address) envfree; 23 | function isCooldownOperator(address user, address operator) external returns (bool) envfree; 24 | function previewRedeem(uint256 shares) external returns (uint256) envfree; 25 | function MIN_ASSETS_REMAINING() external returns (uint256) envfree; 26 | } 27 | 28 | methods { 29 | function _.handleAction(uint256 totalSupply, uint256 totalAssets, address user, uint256 userBalance) external 30 | => handleActionCVL(user) expect void; 31 | 32 | function _.permit(address, address, uint256, uint256, uint8, bytes32, bytes32) external => NONDET; 33 | function _.permit(address, address, uint256, uint256, uint8, bytes32, bytes32) internal => NONDET; 34 | 35 | // ============================================================================================== 36 | // NOTE: in our setup the contract DummyERC20Impl(stake_token) is the staked-asset. We want that the following 37 | // dispatchers will eventually call the functions of DummyERC20Impl (in case of non-resolved calls 38 | // of course). To ensure that, at the beginning of all the rules and invariants we do: 39 | // require(asset() == stake_token); 40 | // (asset() returns the staked-asset) 41 | // ============================================================================================== 42 | function _.transfer(address to, uint256 value) external => DISPATCHER(true); 43 | function _.transferFrom(address from, address to, uint256 value) external => DISPATCHER(true); 44 | function _.safeTransfer(address to, uint256 value) external => DISPATCHER(true); 45 | function _.safeTransferFrom(address from, address to, uint256 value) external => DISPATCHER(true); 46 | function _.balanceOf(address usr) external => DISPATCHER(true); 47 | function _.decimals() external => DISPATCHER(true); 48 | 49 | function _.mulDiv(uint256 x, uint256 y, uint256 denominator) internal => mulDiv_CVL(x,y,denominator) expect (uint256); 50 | function _.mulDiv(uint256 x, uint256 y, uint256 denominator) external => mulDiv_CVL(x,y,denominator) expect (uint256); 51 | } 52 | 53 | function mulDiv_CVL(mathint x, mathint y, mathint denominator) returns uint256 { 54 | uint256 ret = require_uint256 (x*y/denominator); 55 | return ret; 56 | } 57 | 58 | definition is_admin_func(method f) returns bool = 59 | f.selector == sig:slash(address,uint256).selector 60 | || f.selector == sig:pause().selector 61 | || f.selector == sig:unpause().selector 62 | || f.selector == sig:setUnstakeWindow(uint256).selector 63 | || f.selector == sig:setCooldown(uint256).selector 64 | ; 65 | 66 | function in_withdrawal_window(env e, address user) returns bool { 67 | return 68 | cooldownEndOfCooldown(user) <= e.block.timestamp && 69 | e.block.timestamp <= cooldownEndOfCooldown(user) + cooldownWithdrawalWindow(user); 70 | } 71 | 72 | ghost bool handleAction_was_called; 73 | ghost address handleAction_user1; 74 | ghost address handleAction_user2; 75 | function handleActionCVL(address user) { 76 | if (!handleAction_was_called) { 77 | handleAction_was_called = true; 78 | handleAction_user1 = user; 79 | } 80 | else { 81 | // we come here only if it's not the first call to handleAction(...) 82 | handleAction_user2 = user; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/umbrella/RescuableACL.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {IAccessControl} from 'openzeppelin-contracts/contracts/access/IAccessControl.sol'; 5 | import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; 6 | 7 | import {IRescuable} from 'solidity-utils/contracts/utils/interfaces/IRescuable.sol'; 8 | 9 | import {UmbrellaBaseTest} from './utils/UmbrellaBase.t.sol'; 10 | 11 | contract ReceiveEther { 12 | event Received(uint256 amount); 13 | 14 | receive() external payable { 15 | emit Received(msg.value); 16 | } 17 | } 18 | 19 | contract RescuableACLTest is UmbrellaBaseTest { 20 | function test_rescue() public { 21 | deal(address(underlying6Decimals), address(umbrella), 1 ether); 22 | 23 | vm.startPrank(defaultAdmin); 24 | 25 | umbrella.emergencyTokenTransfer(address(underlying6Decimals), someone, 1 ether); 26 | 27 | assertEq(underlying6Decimals.balanceOf(address(umbrella)), 0); 28 | assertEq(underlying6Decimals.balanceOf(someone), 1 ether); 29 | } 30 | 31 | function test_rescueFromStk() public { 32 | deal(address(underlying6Decimals), address(stakeWith6Decimals), 1 ether); 33 | 34 | vm.startPrank(defaultAdmin); 35 | 36 | umbrella.emergencyTokenTransferStk( 37 | address(stakeWith6Decimals), 38 | address(underlying6Decimals), 39 | someone, 40 | 1 ether 41 | ); 42 | 43 | assertEq(underlying6Decimals.balanceOf(address(umbrella)), 0); 44 | assertEq(underlying6Decimals.balanceOf(someone), 1 ether); 45 | } 46 | 47 | function test_rescueEther() public { 48 | deal(address(umbrella), 1 ether); 49 | 50 | address sendToMe = address(new ReceiveEther()); 51 | 52 | vm.stopPrank(); 53 | vm.startPrank(defaultAdmin); 54 | 55 | umbrella.emergencyEtherTransfer(sendToMe, 1 ether); 56 | 57 | assertEq(sendToMe.balance, 1 ether); 58 | } 59 | 60 | function test_rescueEtherStk() public { 61 | deal(address(stakeWith6Decimals), 1 ether); 62 | 63 | address sendToMe = address(new ReceiveEther()); 64 | 65 | vm.stopPrank(); 66 | vm.startPrank(defaultAdmin); 67 | 68 | umbrella.emergencyEtherTransferStk(address(stakeWith6Decimals), sendToMe, 1 ether); 69 | 70 | assertEq(sendToMe.balance, 1 ether); 71 | } 72 | 73 | function test_rescueFromNotAdmin(address anyone) public { 74 | vm.assume( 75 | anyone != defaultAdmin && anyone != transparentProxyFactory.getProxyAdmin(address(umbrella)) 76 | ); 77 | 78 | address sendToMe = address(new ReceiveEther()); 79 | 80 | deal(address(underlying6Decimals), address(umbrella), 1 ether); 81 | deal(address(umbrella), 1 ether); 82 | 83 | vm.startPrank(anyone); 84 | 85 | vm.expectRevert( 86 | abi.encodeWithSelector( 87 | IAccessControl.AccessControlUnauthorizedAccount.selector, 88 | anyone, 89 | RESCUE_GUARDIAN_ROLE 90 | ) 91 | ); 92 | umbrella.emergencyTokenTransfer(address(underlying6Decimals), someone, 1 ether); 93 | 94 | vm.expectRevert( 95 | abi.encodeWithSelector( 96 | IAccessControl.AccessControlUnauthorizedAccount.selector, 97 | anyone, 98 | RESCUE_GUARDIAN_ROLE 99 | ) 100 | ); 101 | umbrella.emergencyEtherTransfer(sendToMe, 1 ether); 102 | } 103 | 104 | function test_rescueFromNotAdminStk(address anyone) public { 105 | vm.assume( 106 | anyone != defaultAdmin && anyone != transparentProxyFactory.getProxyAdmin(address(umbrella)) 107 | ); 108 | 109 | address sendToMe = address(new ReceiveEther()); 110 | 111 | deal(address(underlying6Decimals), address(stakeWith6Decimals), 1 ether); 112 | deal(address(stakeWith6Decimals), 1 ether); 113 | 114 | vm.startPrank(anyone); 115 | 116 | vm.expectRevert( 117 | abi.encodeWithSelector( 118 | IAccessControl.AccessControlUnauthorizedAccount.selector, 119 | anyone, 120 | RESCUE_GUARDIAN_ROLE 121 | ) 122 | ); 123 | umbrella.emergencyTokenTransferStk( 124 | address(stakeWith6Decimals), 125 | address(underlying6Decimals), 126 | someone, 127 | 1 ether 128 | ); 129 | 130 | vm.expectRevert( 131 | abi.encodeWithSelector( 132 | IAccessControl.AccessControlUnauthorizedAccount.selector, 133 | anyone, 134 | RESCUE_GUARDIAN_ROLE 135 | ) 136 | ); 137 | umbrella.emergencyEtherTransferStk(address(stakeWith6Decimals), sendToMe, 1 ether); 138 | } 139 | 140 | function test_maxRescue(address token) public view { 141 | assertEq(umbrella.maxRescue(token), type(uint256).max); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tests/stakeToken/ERC20.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {IERC20Errors} from 'openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol'; 5 | 6 | import {StakeTestBase} from './utils/StakeTestBase.t.sol'; 7 | 8 | contract ERC20Tests is StakeTestBase { 9 | function test_name() external view { 10 | assertEq('Stake Test', stakeToken.name()); 11 | } 12 | 13 | function test_symbol() external view { 14 | assertEq('stkTest', stakeToken.symbol()); 15 | } 16 | 17 | // mint 18 | function test_mint(uint192 amount) public { 19 | vm.assume(amount > 0); 20 | 21 | _mint(amount, user, user); 22 | 23 | assertEq(stakeToken.totalAssets(), underlying.balanceOf(address(stakeToken))); 24 | assertEq(stakeToken.totalSupply(), stakeToken.balanceOf(user)); 25 | 26 | assertLe( 27 | getDiff(stakeToken.previewRedeem(amount), underlying.balanceOf(address(stakeToken))), 28 | 10 29 | ); 30 | } 31 | 32 | // burn 33 | function test_withdraw(uint192 amountStaked, uint192 amountWithdraw) public { 34 | vm.assume(amountStaked > amountWithdraw && amountWithdraw > 0); 35 | 36 | _deposit(amountStaked, user, user); 37 | 38 | vm.startPrank(user); 39 | 40 | stakeToken.cooldown(); 41 | skip(stakeToken.getCooldown()); 42 | 43 | stakeToken.withdraw(amountWithdraw, user, user); 44 | 45 | assertEq(stakeToken.totalAssets(), amountStaked - amountWithdraw); 46 | assertEq(underlying.balanceOf(user), amountWithdraw); 47 | 48 | assertEq(stakeToken.balanceOf(user), stakeToken.totalSupply()); 49 | assertEq(stakeToken.balanceOf(user), stakeToken.convertToShares(amountStaked - amountWithdraw)); 50 | } 51 | 52 | function test_approve(uint192 amount) public { 53 | assertTrue(stakeToken.approve(user, amount)); 54 | assertEq(stakeToken.allowance(address(this), user), amount); 55 | } 56 | 57 | function test_resetApproval(uint192 amount) public { 58 | assertTrue(stakeToken.approve(user, amount)); 59 | assertTrue(stakeToken.approve(user, 0)); 60 | assertEq(stakeToken.allowance(address(this), user), 0); 61 | } 62 | 63 | function test_transferWithoutCooldownInStake( 64 | uint192 amountStake, 65 | uint192 sharesTransfer 66 | ) external { 67 | vm.assume(amountStake > 0); 68 | vm.assume(sharesTransfer <= stakeToken.convertToShares(amountStake)); 69 | 70 | _deposit(amountStake, user, user); 71 | 72 | vm.startPrank(user); 73 | 74 | stakeToken.transfer(someone, sharesTransfer); 75 | 76 | assertEq(stakeToken.balanceOf(someone), sharesTransfer); 77 | assertEq(stakeToken.balanceOf(user), stakeToken.convertToShares(amountStake) - sharesTransfer); 78 | } 79 | 80 | function test_transferWithCooldownInStake(uint192 amountStake, uint192 sharesTransfer) external { 81 | vm.assume(amountStake > 0); 82 | vm.assume(sharesTransfer <= stakeToken.convertToShares(amountStake)); 83 | 84 | _deposit(amountStake, user, user); 85 | 86 | vm.startPrank(user); 87 | 88 | stakeToken.cooldown(); 89 | 90 | skip(1); 91 | 92 | stakeToken.transfer(someone, sharesTransfer); 93 | 94 | assertEq(stakeToken.balanceOf(someone), sharesTransfer); 95 | assertEq(stakeToken.balanceOf(user), stakeToken.convertToShares(amountStake) - sharesTransfer); 96 | } 97 | 98 | function test_transferFrom(uint192 amountStake, uint192 sharesTransfer) external { 99 | vm.assume(amountStake > 0); 100 | vm.assume(sharesTransfer <= stakeToken.convertToShares(amountStake)); 101 | 102 | uint256 sharesMinted = _deposit(amountStake, user, user); 103 | 104 | vm.startPrank(user); 105 | 106 | stakeToken.approve(someone, sharesTransfer); 107 | 108 | vm.stopPrank(); 109 | vm.startPrank(someone); 110 | 111 | assertTrue(stakeToken.transferFrom(user, someone, sharesTransfer)); 112 | 113 | vm.stopPrank(); 114 | 115 | assertEq(stakeToken.allowance(user, someone), 0); 116 | 117 | assertEq(stakeToken.balanceOf(user), sharesMinted - sharesTransfer); 118 | assertEq(stakeToken.balanceOf(someone), sharesTransfer); 119 | } 120 | 121 | function test_transferFromWithoutApprove(uint192 amountStake, uint192 sharesTransfer) external { 122 | vm.assume(amountStake > 0); 123 | vm.assume(0 < sharesTransfer && sharesTransfer <= stakeToken.convertToShares(amountStake)); 124 | 125 | _deposit(amountStake, user, user); 126 | 127 | vm.startPrank(someone); 128 | 129 | vm.expectRevert( 130 | abi.encodeWithSelector( 131 | IERC20Errors.ERC20InsufficientAllowance.selector, 132 | someone, 133 | 0, 134 | sharesTransfer 135 | ) 136 | ); 137 | stakeToken.transferFrom(user, someone, sharesTransfer); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /certora/harness/RewardsControllerHarness.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import {RewardsController} from 'src/contracts/rewards/RewardsController.sol'; 4 | import {InternalStructs} from 'src/contracts/rewards/libraries/InternalStructs.sol'; 5 | 6 | import {DummyContract} from './DummyContract.sol'; 7 | 8 | contract RewardsControllerHarness is RewardsController { 9 | DummyContract DUMMY; 10 | 11 | constructor() RewardsController() {} 12 | 13 | bytes32 private constant __RewardsDistributorStorageLocation = 14 | 0x21b0411c7d97c506a34525b56b49eed70b15d28e22527c4589674c84ba9a5200; 15 | 16 | bytes32 private constant __RewardsControllerStorageLocation = 17 | 0x7a5f91582c97dd0b2921808fbdbab73d3de091aefc8bf8607868e058abb2e300; 18 | 19 | 20 | // The "getStorage" functions of the contracts RewardsController and RewardsDistributor are 21 | // private, hence we make a copy of them. 22 | function __getRewardsDistributorStorage() private pure returns (RewardsDistributorStorage storage $) { 23 | assembly {$.slot := __RewardsDistributorStorageLocation} 24 | } 25 | 26 | function __getRewardsControllerStorage() internal pure returns (RewardsControllerStorage storage $) { 27 | assembly {$.slot := __RewardsControllerStorageLocation} 28 | } 29 | 30 | 31 | function get_rewardsInfo_length(address asset) external view returns (uint) { 32 | InternalStructs.AssetData storage assetData = __getRewardsControllerStorage().assetsData[asset]; 33 | return assetData.rewardsInfo.length; 34 | } 35 | 36 | function get_targetLiquidity(address asset) external view returns (uint160) { 37 | InternalStructs.AssetData storage assetData = __getRewardsControllerStorage().assetsData[asset]; 38 | return assetData.targetLiquidity; 39 | } 40 | 41 | //================================================ 42 | // RewardData struct 43 | //================================================ 44 | function get_rewardIndex(address asset, address reward) external view returns (uint144) { 45 | InternalStructs.AssetData storage assetData = __getRewardsControllerStorage().assetsData[asset]; 46 | return assetData.data[reward].rewardData.index; 47 | } 48 | 49 | function get_maxEmissionPerSecondScaled(address asset, address reward) external view returns (uint72) { 50 | InternalStructs.AssetData storage assetData = __getRewardsControllerStorage().assetsData[asset]; 51 | return assetData.data[reward].rewardData.maxEmissionPerSecondScaled; 52 | } 53 | 54 | function get_distributionEnd__map(address asset, address reward) external view returns (uint32) { 55 | InternalStructs.AssetData storage assetData = __getRewardsControllerStorage().assetsData[asset]; 56 | return assetData.data[reward].rewardData.distributionEnd; 57 | } 58 | 59 | function get_decimalsScaling(address asset, address reward) external view returns (uint8) { 60 | InternalStructs.AssetData storage assetData = __getRewardsControllerStorage().assetsData[asset]; 61 | return assetData.data[reward].rewardData.decimalsScaling; 62 | } 63 | 64 | //================================================ 65 | // UserData struct 66 | //================================================ 67 | function get_userIndex(address asset, address reward, address user) external view returns (uint144) { 68 | InternalStructs.AssetData storage assetData = __getRewardsControllerStorage().assetsData[asset]; 69 | return assetData.data[reward].userData[user].index ; 70 | } 71 | 72 | function get_accrued(address asset, address reward, address user) external view returns (uint112) { 73 | InternalStructs.AssetData storage assetData = __getRewardsControllerStorage().assetsData[asset]; 74 | return assetData.data[reward].userData[user].accrued ; 75 | } 76 | 77 | 78 | 79 | function get_distributionEnd__arr(address asset, uint ind) external view returns (uint32) { 80 | InternalStructs.AssetData storage assetData = __getRewardsControllerStorage().assetsData[asset]; 81 | return assetData.rewardsInfo[ind].distributionEnd; 82 | } 83 | 84 | // Return the reward's address 85 | function get_addr(address asset, uint ind) external view returns (address) { 86 | InternalStructs.AssetData storage assetData = __getRewardsControllerStorage().assetsData[asset]; 87 | return assetData.rewardsInfo[ind].addr; 88 | } 89 | 90 | function get_rewardPayer(address asset, address reward) external view returns (address) { 91 | InternalStructs.AssetData storage assetData = __getRewardsControllerStorage().assetsData[asset]; 92 | return assetData.data[reward].rewardPayer ; 93 | } 94 | 95 | function get_lastUpdateTimestamp(address asset) external view returns (uint32) { 96 | InternalStructs.AssetData storage assetData = __getRewardsControllerStorage().assetsData[asset]; 97 | return assetData.lastUpdateTimestamp; 98 | } 99 | 100 | 101 | 102 | function havoc_other_contracts() external {DUMMY.havoc_other_contracts();} 103 | function havoc_all_contracts() external {DUMMY.havoc_all_contracts_dummy();} 104 | } 105 | -------------------------------------------------------------------------------- /src/contracts/stewards/DeficitOffsetClinicSteward.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.27; 3 | 4 | import {IPool} from 'aave-v3-origin/contracts/interfaces/IPool.sol'; 5 | 6 | import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; 7 | import {SafeERC20} from 'openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol'; 8 | import {AccessControl} from 'openzeppelin-contracts/contracts/access/AccessControl.sol'; 9 | 10 | import {RescuableBase, IRescuableBase} from 'solidity-utils/contracts/utils/RescuableBase.sol'; 11 | import {RescuableACL, IRescuable} from 'solidity-utils/contracts/utils/RescuableACL.sol'; 12 | 13 | import {IUmbrella} from '../umbrella/interfaces/IUmbrella.sol'; 14 | 15 | import {IDeficitOffsetClinicSteward} from './interfaces/IDeficitOffsetClinicSteward.sol'; 16 | 17 | /** 18 | * @title DeficitOffsetClinicSteward 19 | * @author BGD Labs 20 | * @notice This contract covers reserve deficits until the total covered amount exceeds the `deficitOffset` threshold. 21 | * It is designed to prevent the accumulation of deficits in the pool and to eliminate the need for creating individual proposals for each coverage event. 22 | * 23 | * All funds used for coverage are sourced from the Aave Collector. 24 | * For this contract to work properly, it must have the `COVERAGE_MANAGER_ROLE` role on `Umbrella` and also have the appropriate allowance in the required tokens. 25 | * 26 | * Access control: 27 | * Deficit offset can be covered by `FINANCE_COMMITTEE_ROLE` 28 | * Resque funds can be executed by `DEFAULT_ADMIN_ROLE` 29 | */ 30 | contract DeficitOffsetClinicSteward is AccessControl, RescuableACL, IDeficitOffsetClinicSteward { 31 | using SafeERC20 for IERC20; 32 | 33 | bytes32 public constant FINANCE_COMMITTEE_ROLE = keccak256('FINANCE_COMITTEE_ROLE'); 34 | 35 | IUmbrella public immutable UMBRELLA; 36 | address public immutable TREASURY; 37 | IPool public immutable POOL; 38 | 39 | constructor(address umbrella, address treasury, address governance, address financeCommittee) { 40 | require( 41 | umbrella != address(0) && 42 | treasury != address(0) && 43 | governance != address(0) && 44 | financeCommittee != address(0), 45 | ZeroAddress() 46 | ); 47 | 48 | UMBRELLA = IUmbrella(umbrella); 49 | TREASURY = treasury; 50 | 51 | POOL = IPool(UMBRELLA.POOL()); 52 | 53 | _grantRole(DEFAULT_ADMIN_ROLE, governance); 54 | _grantRole(FINANCE_COMMITTEE_ROLE, financeCommittee); 55 | } 56 | 57 | function coverDeficitOffset( 58 | address reserve 59 | ) external onlyRole(FINANCE_COMMITTEE_ROLE) returns (uint256) { 60 | uint256 deficitOffsetToCover = getDeficitOffsetToCover(reserve); 61 | 62 | require(deficitOffsetToCover != 0, DeficitOffsetCannotBeCovered()); 63 | 64 | IERC20 tokenForCoverage = IERC20(UMBRELLA.tokenForDeficitCoverage(reserve)); 65 | tokenForCoverage.safeTransferFrom(TREASURY, address(this), deficitOffsetToCover); 66 | 67 | uint256 actualBalanceReceived = tokenForCoverage.balanceOf(address(this)); 68 | deficitOffsetToCover = actualBalanceReceived < deficitOffsetToCover 69 | ? actualBalanceReceived 70 | : deficitOffsetToCover; 71 | 72 | tokenForCoverage.forceApprove(address(UMBRELLA), deficitOffsetToCover); 73 | 74 | deficitOffsetToCover = UMBRELLA.coverDeficitOffset(reserve, deficitOffsetToCover); 75 | 76 | return deficitOffsetToCover; 77 | } 78 | 79 | function getRemainingAllowance(address reserve) external view returns (uint256) { 80 | IERC20 tokenForCoverage = IERC20(UMBRELLA.tokenForDeficitCoverage(reserve)); 81 | 82 | return tokenForCoverage.allowance(TREASURY, address(this)); 83 | } 84 | 85 | function getDeficitOffsetToCover(address reserve) public view returns (uint256) { 86 | uint256 pendingDeficit = getPendingDeficit(reserve); 87 | uint256 deficitOffset = getDeficitOffset(reserve); 88 | uint256 poolDeficit = getReserveDeficit(reserve); 89 | 90 | if (pendingDeficit + deficitOffset > poolDeficit) { 91 | // `deficitOffset` is manually increased and we can't cover it all, 92 | // because only existing reserve deficit can be covered 93 | return poolDeficit - pendingDeficit; 94 | } else { 95 | // we can cover all `deficitOffset` 96 | return deficitOffset; 97 | } 98 | } 99 | 100 | function getPendingDeficit(address reserve) public view returns (uint256) { 101 | return UMBRELLA.getPendingDeficit(reserve); 102 | } 103 | 104 | function getDeficitOffset(address reserve) public view returns (uint256) { 105 | return UMBRELLA.getDeficitOffset(reserve); 106 | } 107 | 108 | function getReserveDeficit(address reserve) public view returns (uint256) { 109 | return POOL.getReserveDeficit(reserve); 110 | } 111 | 112 | function maxRescue( 113 | address token 114 | ) public view override(IRescuableBase, RescuableBase) returns (uint256) { 115 | return IERC20(token).balanceOf(address(this)); 116 | } 117 | 118 | function _checkRescueGuardian() internal view override { 119 | require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), IRescuable.OnlyRescueGuardian()); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tests/rewards/EmissonMath.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; 5 | import {IAccessControl} from 'openzeppelin-contracts/contracts/access/IAccessControl.sol'; 6 | 7 | import {EmissionMath} from '../../src/contracts/rewards/libraries/EmissionMath.sol'; 8 | import {IRewardsController} from '../../src/contracts/rewards/interfaces/IRewardsController.sol'; 9 | import {RewardsControllerBaseTest, StakeToken, IRewardsDistributor, IRewardsStructs} from './utils/RewardsControllerBase.t.sol'; 10 | 11 | contract RewardsControllerTest is RewardsControllerBaseTest { 12 | function test_CurveSector_1( 13 | uint256 targetLiquidity, 14 | uint256 validMaxEmissionPerSecond, 15 | uint256 amountOfAssets 16 | ) public { 17 | targetLiquidity = bound(targetLiquidity, 1e18, 1e34); 18 | 19 | validMaxEmissionPerSecond = bound( 20 | validMaxEmissionPerSecond, 21 | targetLiquidity / 1e15 + 2, // 2 wei minimum 22 | 1_000 * 1e18 23 | ); 24 | 25 | amountOfAssets = bound(amountOfAssets, 1, targetLiquidity); 26 | 27 | IRewardsStructs.RewardSetupConfig[] memory rewards = new IRewardsStructs.RewardSetupConfig[](1); 28 | rewards[0] = IRewardsStructs.RewardSetupConfig({ 29 | reward: address(reward18Decimals), 30 | rewardPayer: address(rewardsAdmin), 31 | maxEmissionPerSecond: validMaxEmissionPerSecond, 32 | distributionEnd: (block.timestamp + 2 * 365 days) 33 | }); 34 | 35 | vm.startPrank(defaultAdmin); 36 | 37 | rewardsController.configureAssetWithRewards( 38 | address(stakeWith18Decimals), 39 | targetLiquidity, 40 | rewards 41 | ); 42 | 43 | _dealStakeToken(stakeWith18Decimals, user, amountOfAssets); 44 | 45 | uint256 currentEmission = rewardsController.calculateCurrentEmission( 46 | address(stakeWith18Decimals), 47 | address(reward18Decimals) 48 | ); 49 | 50 | assertGe(currentEmission, 0); 51 | assertGe(validMaxEmissionPerSecond, currentEmission); 52 | 53 | if (amountOfAssets > targetLiquidity) { 54 | assertGt(currentEmission, (3 * validMaxEmissionPerSecond) / 4); 55 | } 56 | } 57 | 58 | function test_CurveSector_2( 59 | uint256 targetLiquidity, 60 | uint256 validMaxEmissionPerSecond, 61 | uint256 amountOfAssets 62 | ) public { 63 | targetLiquidity = bound(targetLiquidity, 1e18, 1e34); 64 | 65 | validMaxEmissionPerSecond = bound( 66 | validMaxEmissionPerSecond, 67 | targetLiquidity / 1e15 + 2, // 2 wei minimum 68 | 1_000 * 1e18 69 | ); 70 | 71 | amountOfAssets = bound(amountOfAssets, targetLiquidity, (targetLiquidity * 12_000) / 10_000); 72 | 73 | IRewardsStructs.RewardSetupConfig[] memory rewards = new IRewardsStructs.RewardSetupConfig[](1); 74 | rewards[0] = IRewardsStructs.RewardSetupConfig({ 75 | reward: address(reward18Decimals), 76 | rewardPayer: address(rewardsAdmin), 77 | maxEmissionPerSecond: validMaxEmissionPerSecond, 78 | distributionEnd: (block.timestamp + 2 * 365 days) 79 | }); 80 | 81 | vm.startPrank(defaultAdmin); 82 | 83 | rewardsController.configureAssetWithRewards( 84 | address(stakeWith18Decimals), 85 | targetLiquidity, 86 | rewards 87 | ); 88 | 89 | _dealStakeToken(stakeWith18Decimals, user, amountOfAssets); 90 | 91 | uint256 currentEmission = rewardsController.calculateCurrentEmission( 92 | address(stakeWith18Decimals), 93 | address(reward18Decimals) 94 | ); 95 | 96 | assertGe(currentEmission, (8_000 * validMaxEmissionPerSecond) / 10_000); 97 | assertGe(validMaxEmissionPerSecond, currentEmission); 98 | } 99 | 100 | function test_CurveSector_3( 101 | uint256 targetLiquidity, 102 | uint256 validMaxEmissionPerSecond, 103 | uint256 amountOfAssets 104 | ) public { 105 | targetLiquidity = bound(targetLiquidity, 1e18, 1e34); 106 | 107 | validMaxEmissionPerSecond = bound( 108 | validMaxEmissionPerSecond, 109 | targetLiquidity / 1e15 + 2, // 2 wei minimum 110 | 1_000 * 1e18 111 | ); 112 | 113 | amountOfAssets = bound(amountOfAssets, (targetLiquidity * 12_000) / 10_000, 1e40); 114 | 115 | IRewardsStructs.RewardSetupConfig[] memory rewards = new IRewardsStructs.RewardSetupConfig[](1); 116 | rewards[0] = IRewardsStructs.RewardSetupConfig({ 117 | reward: address(reward18Decimals), 118 | rewardPayer: address(rewardsAdmin), 119 | maxEmissionPerSecond: validMaxEmissionPerSecond, 120 | distributionEnd: (block.timestamp + 2 * 365 days) 121 | }); 122 | 123 | vm.startPrank(defaultAdmin); 124 | 125 | rewardsController.configureAssetWithRewards( 126 | address(stakeWith18Decimals), 127 | targetLiquidity, 128 | rewards 129 | ); 130 | 131 | _dealStakeToken(stakeWith18Decimals, user, amountOfAssets); 132 | 133 | uint256 currentEmission = rewardsController.calculateCurrentEmission( 134 | address(stakeWith18Decimals), 135 | address(reward18Decimals) 136 | ); 137 | 138 | assertEq(currentEmission, (8_000 * validMaxEmissionPerSecond) / 10_000); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /certora/specs/stakeToken/invariants.spec: -------------------------------------------------------------------------------- 1 | import "base.spec"; 2 | 3 | 4 | /* ================================== 5 | 104 6 | 32 64 96| 7 | | | | | 8 | 0x123456789012345678901234567890 9 | =================================== */ 10 | 11 | ghost mathint sumOfBalances { 12 | init_state axiom sumOfBalances == 0; 13 | } 14 | hook Sstore UmbrellaStakeTokenHarness.(slot 0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00).(offset 0) 15 | [KEY address account] uint256 balance (uint256 balance_old) { 16 | sumOfBalances = sumOfBalances + balance - balance_old; 17 | } 18 | hook Sload uint256 balance UmbrellaStakeTokenHarness.(slot 0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00).(offset 0) 19 | [KEY address account] { 20 | require balance <= sumOfBalances; 21 | } 22 | 23 | // ==================================================================== 24 | // Invariant: inv_sumOfBalances_eq_totalSupply 25 | // Description: The total supply equals the sum of all users' balances. 26 | // Status: PASS 27 | // ==================================================================== 28 | invariant inv_sumOfBalances_eq_totalSupply() 29 | sumOfBalances == totalSupply(); 30 | 31 | 32 | 33 | ghost mathint sumOfBalances_stake_token { 34 | init_state axiom sumOfBalances_stake_token == 0; 35 | } 36 | hook Sstore stake_token.b[KEY address a] uint256 balance (uint256 old_balance) { 37 | sumOfBalances_stake_token = sumOfBalances_stake_token - old_balance + balance; 38 | } 39 | hook Sload uint256 balance stake_token.b[KEY address a] { 40 | require balance <= sumOfBalances_stake_token; 41 | } 42 | 43 | // ====================================================================================== 44 | // Invariant: inv_sumOfBalances_eq_totalSupply__stake_token 45 | // Description: The total supply of the staked-token equals the sum of all users' balances. 46 | // Status: PASS 47 | // ====================================================================================== 48 | invariant inv_sumOfBalances_eq_totalSupply__stake_token() 49 | sumOfBalances_stake_token == stake_token.totalSupply(); 50 | 51 | 52 | 53 | // ====================================================================================== 54 | // Invariant: total_supply_GEQ_user_bal 55 | // Description: The total supply amount of shares is greater or equal to any user's share balance. 56 | // Status: PASS 57 | // ====================================================================================== 58 | invariant total_supply_GEQ_user_bal(address user) 59 | totalSupply() >= balanceOf(user) 60 | { 61 | preserved { 62 | requireInvariant inv_sumOfBalances_eq_totalSupply(); 63 | } 64 | } 65 | 66 | 67 | // ====================================================================================== 68 | // Invariant: cooldown_data_correctness 69 | // Description: When cooldown amount of user nonzero, the cooldown had to be triggered 70 | // Status: PASS 71 | // ====================================================================================== 72 | invariant cooldown_data_correctness(address user) 73 | (cooldownWithdrawalWindow(user) > 0 || cooldownAmount(user) > 0) => cooldownEndOfCooldown(user) > 0 74 | { 75 | preserved with (env e) 76 | { 77 | require e.block.timestamp > 0; 78 | require e.block.timestamp < 2^32; 79 | } 80 | } 81 | /* 82 | invariant cooldown_data_correctness2(address user) 83 | cooldownWithdrawalWindow(user) > 0 => cooldownEndOfCooldown(user) > 0 84 | { 85 | preserved with (env e) 86 | { 87 | require e.block.timestamp > 0; 88 | require e.block.timestamp < 2^32; 89 | } 90 | }*/ 91 | 92 | // ====================================================================================== 93 | // Invariant cooldown_amount_not_greater_than_balance 94 | // Description: No user can have greater cooldown amount than is their balance. 95 | // Status: PASS 96 | // ====================================================================================== 97 | invariant cooldown_amount_not_greater_than_balance(env e, address user) 98 | in_withdrawal_window(e,user) => balanceOf(user) >= assert_uint256(cooldownAmount(user)) 99 | { 100 | preserved with (env e2) { 101 | require e2.block.timestamp == e.block.timestamp; 102 | require(asset() == stake_token); 103 | requireInvariant cooldown_data_correctness(user); 104 | requireInvariant total_supply_GEQ_user_bal(user); 105 | requireInvariant inv_sumOfBalances_eq_totalSupply(); 106 | } 107 | } 108 | 109 | 110 | 111 | // ====================================================================================== 112 | // Invariant: calculated_bal_LEQ_real_bal 113 | // Description: Virtual accounting which is (totalAssets()) is always <= the real balance of the contract 114 | // Status: PASS 115 | // ====================================================================================== 116 | invariant calculated_bal_LEQ_real_bal() 117 | totalAssets() <= stake_token.balanceOf(currentContract) 118 | filtered {f -> f.contract == currentContract 119 | } 120 | { 121 | preserved with (env e) 122 | { 123 | require(asset() == stake_token); 124 | require e.msg.sender != currentContract; 125 | requireInvariant inv_sumOfBalances_eq_totalSupply(); 126 | requireInvariant inv_sumOfBalances_eq_totalSupply__stake_token(); 127 | } 128 | } 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /tests/stakeToken/PermitDeposit.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {ERC4626Upgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC4626Upgradeable.sol'; 5 | 6 | import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; 7 | import {IERC20Permit} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol'; 8 | import {IERC20Errors} from 'openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol'; 9 | 10 | import {IERC4626StakeToken} from '../../src/contracts/stakeToken/interfaces/IERC4626StakeToken.sol'; 11 | 12 | import {StakeTestBase} from './utils/StakeTestBase.t.sol'; 13 | 14 | contract PermitDepositTests is StakeTestBase { 15 | bytes32 private constant PERMIT_TYPEHASH = 16 | keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)'); 17 | 18 | bytes32 private constant TYPE_HASH = 19 | keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'); 20 | 21 | bytes32 _hashedName = keccak256(bytes('MockToken')); 22 | bytes32 _hashedVersion = keccak256(bytes('1')); 23 | 24 | function test_permitAndDepositSeparate(uint192 amountToStake) public { 25 | amountToStake = uint192(bound(amountToStake, 1, type(uint192).max)); 26 | 27 | vm.startPrank(user); 28 | 29 | uint256 deadline = block.timestamp + 1e6; 30 | _dealUnderlying(amountToStake, user); 31 | 32 | bytes32 digest = keccak256( 33 | abi.encode(PERMIT_TYPEHASH, user, address(stakeToken), amountToStake, 0, deadline) 34 | ); 35 | 36 | bytes32 hash = toTypedDataHash(_domainSeparator(), digest); 37 | 38 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, hash); 39 | 40 | assertEq(IERC20Permit(address(underlying)).nonces(user), 0); 41 | 42 | IERC20Permit(address(underlying)).permit( 43 | user, 44 | address(stakeToken), 45 | amountToStake, 46 | deadline, 47 | v, 48 | r, 49 | s 50 | ); 51 | 52 | assertEq(IERC20Permit(address(underlying)).nonces(user), 1); 53 | 54 | stakeToken.deposit(amountToStake, user); 55 | 56 | uint256 shares = stakeToken.previewDeposit(amountToStake); 57 | 58 | assertEq(stakeToken.totalAssets(), amountToStake); 59 | assertEq(stakeToken.totalAssets(), underlying.balanceOf(address(stakeToken))); 60 | 61 | assertEq(stakeToken.totalSupply(), shares); 62 | assertEq(stakeToken.balanceOf(user), shares); 63 | } 64 | 65 | function test_permitDeposit(uint192 amountToStake) public { 66 | amountToStake = uint192(bound(amountToStake, 1, type(uint192).max)); 67 | 68 | vm.startPrank(user); 69 | 70 | uint256 deadline = block.timestamp + 1e6; 71 | _dealUnderlying(amountToStake, user); 72 | 73 | bytes32 digest = keccak256( 74 | abi.encode(PERMIT_TYPEHASH, user, address(stakeToken), amountToStake, 0, deadline) 75 | ); 76 | 77 | bytes32 hash = toTypedDataHash(_domainSeparator(), digest); 78 | 79 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, hash); 80 | 81 | IERC4626StakeToken.SignatureParams memory sig = IERC4626StakeToken.SignatureParams(v, r, s); 82 | 83 | assertEq(IERC20Permit(address(underlying)).nonces(user), 0); 84 | 85 | stakeToken.depositWithPermit(amountToStake, user, deadline, sig); 86 | 87 | assertEq(IERC20Permit(address(underlying)).nonces(user), 1); 88 | 89 | uint256 shares = stakeToken.previewDeposit(amountToStake); 90 | 91 | assertEq(stakeToken.totalAssets(), amountToStake); 92 | assertEq(stakeToken.totalAssets(), underlying.balanceOf(address(stakeToken))); 93 | 94 | assertEq(stakeToken.totalSupply(), shares); 95 | assertEq(stakeToken.balanceOf(user), shares); 96 | } 97 | 98 | function test_permitDepositInvalidSignature(uint192 amountToStake) public { 99 | amountToStake = uint192(bound(amountToStake, 2, type(uint192).max)); 100 | 101 | vm.startPrank(user); 102 | 103 | uint256 deadline = block.timestamp + 1e6; 104 | _dealUnderlying(amountToStake, user); 105 | 106 | bytes32 digest = keccak256( 107 | abi.encode(PERMIT_TYPEHASH, user, address(stakeToken), 1, 0, deadline) 108 | ); 109 | 110 | bytes32 hash = toTypedDataHash(_domainSeparator(), digest); 111 | 112 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, hash); 113 | 114 | IERC4626StakeToken.SignatureParams memory sig = IERC4626StakeToken.SignatureParams(v, r, s); 115 | 116 | vm.expectRevert( 117 | abi.encodeWithSelector( 118 | IERC20Errors.ERC20InsufficientAllowance.selector, 119 | address(stakeToken), 120 | 0, 121 | amountToStake 122 | ) 123 | ); 124 | stakeToken.depositWithPermit(amountToStake, user, deadline, sig); 125 | } 126 | 127 | // copy from OZ 128 | function _domainSeparator() private view returns (bytes32) { 129 | return 130 | keccak256( 131 | abi.encode(TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(underlying)) 132 | ); 133 | } 134 | 135 | function toTypedDataHash( 136 | bytes32 domainSeparator, 137 | bytes32 structHash 138 | ) private pure returns (bytes32 digest) { 139 | /// @solidity memory-safe-assembly 140 | assembly { 141 | let ptr := mload(0x40) 142 | mstore(ptr, hex'19_01') 143 | mstore(add(ptr, 0x02), domainSeparator) 144 | mstore(add(ptr, 0x22), structHash) 145 | digest := keccak256(ptr, 0x42) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /assets/operating_conditions.md: -------------------------------------------------------------------------------- 1 | # Operating conditions 2 | 3 | ## Requirements 4 | 5 | To ensure the system meets our expectations, we introduce two types of requirements: 6 | 7 | 1) Hard requirements. These requirements are enforced by the code. For example: 8 | 1. `totalAssets <= totalSupply`, because `StakeToken` slashes reduce only the `totalAssets`. 9 | 2. `targetLiquidity >= 10 ** decimals` (at least 1 whole token). 10 | 3. `maxEmissionPerSecond <= 10^21` and `maxEmissionPerSecond >= minBound`, where `minBound = precisionBound > 2 ? precisionBound : 2`, and `precisionBound = targetLiquidity / 10 ^ 15` 11 | 4. `totalSupply >= 1e6` 12 | 13 | 2) Soft requirements: These are conditions that the code does not enforce, but they provide guidelines for optimal behavior: 14 | 1. `totalAssets/targetLiquidity <= 10`. This condition doesn't make any sense for holders, cause APY in this case will be less than optimal by 12.5 times. Futhermore, holding funds at risk of 100% slashing with such a low APY is not justified. 15 | 2. `totalSupply/totalAssets <= 100`. The `totalSupply/totalAssets` ratio can, due to the architecture, grow up to 2 ^ 256 / 10 ^ 6. We set a limit of 100, once we reach it, we will redeploy the `StakeToken` contract. Because, such an increase in the exchange rate can lead to calculation inaccuracies in the entire system. 16 | 17 | ## Additional notes 18 | 19 | 1) Violating soft requirements doesn't mean the contract will not be functional anymore with this concrete asset, but may result in decreased calculation accuracy (potentially causing new accrued rewards to be zeroed out). 20 | 21 | 2) For simplicity, we will ignore the order of operations in some places and present the proofs in mathematical form. While there may be some exceptions, the following calculations are valid for the vast majority of cases. 22 | 23 | ## `SlopeCurve` - overflow check 24 | 25 | 1) (typeOf(index) = uint144) 2^144 ~= 2.23e43 26 | 2) Maximum possible `maxEmissionPerSecond` = 1e21 27 | 3) Maximum possible `currentEmission = SCALING_FACTOR * maxEmissionPerSecond * totalAssets/targetLiquidity = 1e39` (cause max(totalAssets/targetLiquidity) is 1 on `SlopeCurve`) 28 | 4) (Let's take min possible `totalSupply = 1e6`) 29 | 5) 2.23e43 * 1e6 / 1e39 / 365 / 24 / 3600 = ~707 years without overflow 30 | 31 | ~707 years without overflow under the worst conditions. 32 | 33 | ## Other sectors - overflow check 34 | 35 | Since other sectors result in slower index growth, there's no need to check for overflow, only for a zero `indexIncrease` changes. 36 | 37 | ## `SlopeCurve` - zero `indexIncrease` check 38 | 39 | The index should be able to be updated at least by 1 uint every second (ignoring precision loss for now) 40 | 41 | 1) `1 == (currentEmission * 1) / totalSupply` 42 | 2) `currentEmission == totalSupply` (cause totalSupply != 0 if totalAssets != 0) 43 | 3) `((2 * maxEmissionPerSecond * SCALING_FACTOR - (maxEmissionPerSecond * SCALING_FACTOR * totalAssets) / targetLiquidity) * totalAssets) / targetLiquidity == totalSupply` 44 | 4) `((2 * maxEmissionPerSecond * SCALING_FACTOR - maxEmissionPerSecond * SCALING_FACTOR * totalAssets / targetLiquidity) * totalAssets == totalSupply * targetLiquidity` (due to the fact, that `targetLiquidity` != 0) 45 | 5) `maxEmissionPerSecond * SCALING_FACTOR * totalAssets / targetLiquidity` can't exceed `maxEmissionPerSecond * SCALING_FACTOR` on `SlopeCurve`, so 46 | 6) `maxEmissionPerSecond * SCALING_FACTOR * totalAssets == totalSupply * targetLiquidity` (this is more strict check, than needed) 47 | 7) `maxEmissionPerSecond * SCALING_FACTOR == targetLiquidity * (totalSupply / totalAssets)` 48 | 8) `maxEmissionPerSecond * SCALING_FACTOR == targetLiquidity * 100` (worst case) 49 | 9) `maxEmissionPerSecond == targetLiquidity / 1e16` 50 | 51 | If `maxEmissionPerSecond` is less than `targetLiquidity / 1e16`, then calculations could lose precision (rounding to zero). However, this is addressed by hard requirement 3. 52 | 53 | ## `Flat` - zero `indexIncrease` check 54 | 55 | 1) `indexIncrease = maxEmissionPerSecond * 1e18 * 8 / 10 * 1 / totalSupply` (should be equal to at least 1) 56 | 2) `totalSupply / totalAssets = 100` (worst case) 57 | 3) `totalSupply = 100 * totalAssets` 58 | 4) `totalAssets / targetLiquidity = 10` (worst case) 59 | 5) `totalAssets = 10 * targetLiquidity` 60 | 6) `totalSupply = 100 * 10 * targetLiquidity` 61 | 7) `indexIncrease = maxEmissionPerSecond * 1e18 * 8 / 10 / (1000 * targetLiquidity)` 62 | 8) `maxEmissionPerSecond * 8 / 10 * 1e18 >= 1000 * targetLiquidity` 63 | 9) `maxEmissionPerSecond >= targetLiquidity / 8e14` 64 | 65 | If `maxEmissionPerSecond` exceeds `targetLiquidity / 8e14`, our calculations should be precise (as per hard requirement 3). 66 | 67 | ## `LinearDecreaseCurve` - zero `indexIncrease` check 68 | 69 | Since both the `Flat` and `SlopeCurve` sectors should calculate precisely enough, the `LinearDecreaseCurve` should also do so. This is because the emission in the `LinearDecreaseCurve` is bounded between `maxEmissionPerSecond` and `flatEmission`, both of which have been shown to meet the required precision. 70 | 71 | ## Reward distribution requirements 72 | 73 | The maximum value for `maxEmissionPerSecond` is `1000 * 1e18`. 74 | 75 | * Calculating the number of tokens that can be distributed per year: `1000 * 60 * 60 * 24 * 365 ≈ 31,536,000,000`. This means approximately *31.54 billion tokens per year*. 76 | * If the price of the reward token is at least 0.01 USD, the total value of rewards would be approximately *315.36 million USD annually*. 77 | 78 | Assuming the market size of `stk` could represent about 5-10% of the total market size within the Pool, and the estimated APY should remain in the range of 5-10%, this volume of rewards should be sufficient for nearly any market. 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Business Source License 1.1 2 | 3 | License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. 4 | “Business Source License” is a trademark of MariaDB Corporation Ab. 5 | 6 | --- 7 | 8 | Parameters 9 | 10 | Licensor: Aave DAO, represented by its governance smart contracts 11 | 12 | Licensed Work: Umbrella 13 | The Licensed Work is (c) 2025 Aave DAO, represented by its governance smart contracts 14 | 15 | Additional Use Grant: You are permitted to use, copy, and modify the Licensed Work, subject to 16 | the following conditions: 17 | 18 | - Your use of the Licensed Work shall not, directly or indirectly, enable, facilitate, 19 | or assist in any way with the migration of users and/or funds from the Aave ecosystem. 20 | The "Aave ecosystem" is defined in the context of this License as the collection of 21 | software protocols and applications approved by the Aave governance, including all 22 | those produced within compensated service provider engagements with the Aave DAO. 23 | The Aave DAO is able to waive this requirement for one or more third-parties, if and 24 | only if explicitly indicating it on a record 'authorizations' on umbrella.aavelicense.eth. 25 | - You are neither an individual nor a direct or indirect participant in any incorporated 26 | organization, DAO, or identifiable group, that has deployed in production any original 27 | or derived software ("fork") of the Aave ecosystem for purposes competitive to Aave, 28 | within the preceding two years. 29 | The Aave DAO is able to waive this requirement for one or more third-parties, if and 30 | only if explicitly indicating it on a record 'authorizations' on umbrella.aavelicense.eth. 31 | - You must ensure that the usage of the Licensed Work does not result in any direct or 32 | indirect harm to the Aave ecosystem or the Aave brand. This encompasses, but is not limited to, 33 | reputational damage, omission of proper credit/attribution, or utilization for any malicious 34 | intent. 35 | 36 | Change Date: The earlier of: - 2029-01-08 - The date specified in the 'change-date' record on umbrella.aavelicense.eth 37 | 38 | Change License: MIT 39 | 40 | --- 41 | 42 | Notice 43 | 44 | The Business Source License (this document, or the “License”) is not an Open 45 | Source license. However, the Licensed Work will eventually be made available 46 | under an Open Source License, as stated in this License. 47 | 48 | --- 49 | 50 | Terms 51 | 52 | The Licensor hereby grants you the right to copy, modify, create derivative 53 | works, redistribute, and make non-production use of the Licensed Work. The 54 | Licensor may make an Additional Use Grant, above, permitting limited 55 | production use. 56 | 57 | Effective on the Change Date, or the fourth anniversary of the first publicly 58 | available distribution of a specific version of the Licensed Work under this 59 | License, whichever comes first, the Licensor hereby grants you rights under 60 | the terms of the Change License, and the rights granted in the paragraph 61 | above terminate. 62 | 63 | If your use of the Licensed Work does not comply with the requirements 64 | currently in effect as described in this License, you must purchase a 65 | commercial license from the Licensor, its affiliated entities, or authorized 66 | resellers, or you must refrain from using the Licensed Work. 67 | 68 | All copies of the original and modified Licensed Work, and derivative works 69 | of the Licensed Work, are subject to this License. This License applies 70 | separately for each version of the Licensed Work and the Change Date may vary 71 | for each version of the Licensed Work released by Licensor. 72 | 73 | You must conspicuously display this License on each original or modified copy 74 | of the Licensed Work. If you receive the Licensed Work in original or 75 | modified form from a third party, the terms and conditions set forth in this 76 | License apply to your use of that work. 77 | 78 | Any use of the Licensed Work in violation of this License will automatically 79 | terminate your rights under this License for the current and all other 80 | versions of the Licensed Work. 81 | 82 | This License does not grant you any right in any trademark or logo of 83 | Licensor or its affiliates (provided that you may use a trademark or logo of 84 | Licensor as expressly required by this License). 85 | 86 | TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON 87 | AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, 88 | EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF 89 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND 90 | TITLE. 91 | 92 | MariaDB hereby grants you permission to use this License’s text to license 93 | your works, and to refer to it using the trademark “Business Source License”, 94 | as long as you comply with the Covenants of Licensor below. 95 | 96 | Covenants of Licensor 97 | 98 | In consideration of the right to use this License’s text and the “Business 99 | Source License” name and trademark, Licensor covenants to MariaDB, and to all 100 | other recipients of the licensed work to be provided by Licensor: 101 | 102 | 1. To specify as the Change License the GPL Version 2.0 or any later version, 103 | or a license that is compatible with GPL Version 2.0 or a later version, 104 | where “compatible” means that software provided under the Change License can 105 | be included in a program with software provided under GPL Version 2.0 or a 106 | later version. Licensor may specify additional Change Licenses without 107 | limitation. 108 | 109 | 2. To either: (a) specify an additional grant of rights to use that does not 110 | impose any additional restriction on the right granted in this License, as 111 | the Additional Use Grant; or (b) insert the text “None”. 112 | 113 | 3. To specify a Change Date. 114 | 115 | 4. Not to modify this License in any other way. 116 | -------------------------------------------------------------------------------- /tests/helpers/Pause.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {Ownable} from 'openzeppelin-contracts/contracts/access/Ownable.sol'; 5 | import {Pausable} from 'openzeppelin-contracts/contracts/utils/Pausable.sol'; 6 | 7 | import {IStakeToken} from '../../src/contracts/stakeToken/interfaces/IStakeToken.sol'; 8 | import {IUmbrellaBatchHelper} from '../../src/contracts/helpers/interfaces/IUmbrellaBatchHelper.sol'; 9 | 10 | import {UmbrellaBatchHelperTestBase} from './utils/UmbrellaBatchHelperBase.t.sol'; 11 | 12 | contract PauseTests is UmbrellaBatchHelperTestBase { 13 | function test_setPauseByAdmin() external { 14 | assertEq(umbrellaBatchHelper.paused(), false); 15 | 16 | vm.startPrank(defaultAdmin); 17 | 18 | umbrellaBatchHelper.pause(); 19 | 20 | assertEq(umbrellaBatchHelper.paused(), true); 21 | 22 | umbrellaBatchHelper.unpause(); 23 | 24 | assertEq(umbrellaBatchHelper.paused(), false); 25 | } 26 | 27 | function test_setPauseNotByAdmin(address anyone) external { 28 | vm.assume(anyone != defaultAdmin); 29 | 30 | assertEq(umbrellaBatchHelper.paused(), false); 31 | 32 | vm.startPrank(anyone); 33 | 34 | vm.expectRevert( 35 | abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(anyone)) 36 | ); 37 | umbrellaBatchHelper.pause(); 38 | } 39 | 40 | function test_shouldRevertWhenPauseIsActive() external { 41 | vm.startPrank(defaultAdmin); 42 | umbrellaBatchHelper.pause(); 43 | 44 | vm.stopPrank(); 45 | vm.startPrank(user); 46 | 47 | uint256 amount = 1e18; 48 | uint256 deadline = block.timestamp + 1e6; 49 | 50 | bytes32 hash = getHash(user, spender, tokenAddressesWithStata[2], amount, 0, deadline); 51 | (uint8 v, bytes32 r, bytes32 s) = signHash(userPrivateKey, hash); 52 | 53 | bytes[] memory batch = new bytes[](1); 54 | batch[0] = abi.encodeWithSelector( 55 | IUmbrellaBatchHelper.permit.selector, 56 | IUmbrellaBatchHelper.Permit({ 57 | token: tokenAddressesWithStata[2], 58 | value: amount, 59 | deadline: deadline, 60 | v: v, 61 | r: r, 62 | s: s 63 | }) 64 | ); 65 | 66 | vm.expectRevert(Pausable.EnforcedPause.selector); 67 | umbrellaBatchHelper.multicall(batch); 68 | 69 | vm.expectRevert(Pausable.EnforcedPause.selector); 70 | umbrellaBatchHelper.permit( 71 | IUmbrellaBatchHelper.Permit({ 72 | token: tokenAddressesWithStata[2], 73 | value: amount, 74 | deadline: deadline, 75 | v: v, 76 | r: r, 77 | s: s 78 | }) 79 | ); 80 | 81 | // invalid sign, but we don't care, cause we get revert earlier 82 | batch[0] = abi.encodeWithSelector( 83 | IUmbrellaBatchHelper.cooldownPermit.selector, 84 | IUmbrellaBatchHelper.CooldownPermit({ 85 | stakeToken: IStakeToken(address(stakeTokenWithoutStata)), 86 | deadline: deadline, 87 | v: v, 88 | r: r, 89 | s: s 90 | }) 91 | ); 92 | 93 | vm.expectRevert(Pausable.EnforcedPause.selector); 94 | umbrellaBatchHelper.multicall(batch); 95 | 96 | vm.expectRevert(Pausable.EnforcedPause.selector); 97 | umbrellaBatchHelper.cooldownPermit( 98 | IUmbrellaBatchHelper.CooldownPermit({ 99 | stakeToken: IStakeToken(address(stakeTokenWithoutStata)), 100 | deadline: deadline, 101 | v: v, 102 | r: r, 103 | s: s 104 | }) 105 | ); 106 | 107 | address[] memory addresses = new address[](0); 108 | 109 | // invalid sign, but we don't care, cause we get revert earlier 110 | batch[0] = abi.encodeWithSelector( 111 | IUmbrellaBatchHelper.claimRewardsPermit.selector, 112 | IUmbrellaBatchHelper.ClaimPermit({ 113 | stakeToken: IStakeToken(address(stakeTokenWithoutStata)), 114 | rewards: addresses, 115 | deadline: deadline, 116 | v: v, 117 | r: r, 118 | s: s, 119 | restake: false 120 | }) 121 | ); 122 | 123 | vm.expectRevert(Pausable.EnforcedPause.selector); 124 | umbrellaBatchHelper.multicall(batch); 125 | 126 | vm.expectRevert(Pausable.EnforcedPause.selector); 127 | umbrellaBatchHelper.claimRewardsPermit( 128 | IUmbrellaBatchHelper.ClaimPermit({ 129 | stakeToken: IStakeToken(address(stakeTokenWithoutStata)), 130 | rewards: addresses, 131 | deadline: deadline, 132 | v: v, 133 | r: r, 134 | s: s, 135 | restake: false 136 | }) 137 | ); 138 | 139 | batch[0] = abi.encodeWithSelector( 140 | IUmbrellaBatchHelper.deposit.selector, 141 | IUmbrellaBatchHelper.IOData({ 142 | stakeToken: IStakeToken(address(stakeTokenWithoutStata)), 143 | edgeToken: address(stakeTokenWithoutStata), 144 | value: amount 145 | }) 146 | ); 147 | 148 | vm.expectRevert(Pausable.EnforcedPause.selector); 149 | umbrellaBatchHelper.multicall(batch); 150 | 151 | vm.expectRevert(Pausable.EnforcedPause.selector); 152 | umbrellaBatchHelper.deposit( 153 | IUmbrellaBatchHelper.IOData({ 154 | stakeToken: IStakeToken(address(stakeTokenWithoutStata)), 155 | edgeToken: address(stakeTokenWithoutStata), 156 | value: amount 157 | }) 158 | ); 159 | 160 | batch[0] = abi.encodeWithSelector( 161 | IUmbrellaBatchHelper.redeem.selector, 162 | IUmbrellaBatchHelper.IOData({ 163 | stakeToken: IStakeToken(address(stakeTokenWithoutStata)), 164 | edgeToken: address(stakeTokenWithoutStata), 165 | value: amount 166 | }) 167 | ); 168 | 169 | vm.expectRevert(Pausable.EnforcedPause.selector); 170 | umbrellaBatchHelper.multicall(batch); 171 | 172 | vm.expectRevert(Pausable.EnforcedPause.selector); 173 | umbrellaBatchHelper.redeem( 174 | IUmbrellaBatchHelper.IOData({ 175 | stakeToken: IStakeToken(address(stakeTokenWithoutStata)), 176 | edgeToken: address(stakeTokenWithoutStata), 177 | value: amount 178 | }) 179 | ); 180 | } 181 | } 182 | --------------------------------------------------------------------------------