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