├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── SECURITY.md ├── audits ├── Euler Cantina Code Competition report.pdf ├── Euler Hunter Security report.pdf ├── Euler MixBytes report.pdf └── Euler yAudit Code Competition Fixes report.pdf ├── certora ├── conf │ └── StakingRewardStream.conf ├── harness │ └── ERC20Caller.sol ├── mutations │ ├── base │ │ ├── BaseRewardStreams_mut1.sol │ │ ├── BaseRewardStreams_mut2.sol │ │ ├── BaseRewardStreams_mut3.sol │ │ └── BaseRewardStreams_mut4.sol │ └── staking │ │ ├── StakingRewardStreams_mut1.sol │ │ ├── StakingRewardStreams_mut2.sol │ │ ├── StakingRewardStreams_mut3.sol │ │ ├── StakingRewardStreams_mut4.sol │ │ └── StakingRewardStreams_mut5.sol └── specs │ ├── ERC20 │ └── erc20.spec │ └── StakingRewardStreams.spec ├── certora_all └── properties.md ├── coverage.sh ├── foundry.toml ├── medusa.json ├── package.json ├── script └── placeholder ├── src ├── BaseRewardStreams.sol ├── StakingRewardStreams.sol ├── TrackingRewardStreams.sol └── interfaces │ ├── IBalanceTracker.sol │ └── IRewardStreams.sol └── test ├── harness ├── BaseRewardStreamsHarness.sol ├── StakingRewardStreamsHarness.sol └── TrackingRewardStreamsHarness.sol ├── invariants ├── CryticToFoundry.t.sol ├── HandlerAggregator.t.sol ├── Invariants.t.sol ├── InvariantsSpec.t.sol ├── Setup.t.sol ├── Tester.t.sol ├── _config │ └── echidna_config.yaml ├── base │ ├── BaseHandler.t.sol │ ├── BaseHooks.t.sol │ ├── BaseStorage.t.sol │ ├── BaseTest.t.sol │ └── ProtocolAssertions.t.sol ├── handlers │ ├── BaseRewardsHandler.t.sol │ ├── StakingRewardStreamsHandler.t.sol │ ├── TrackingRewardStreamsHandler.t.sol │ ├── external │ │ └── EVCHandler.t.sol │ ├── interfaces │ │ └── ILiquidationModuleHandler.sol │ └── simulators │ │ ├── ControllerHandler.t.sol │ │ ├── DonationAttackHandler.t.sol │ │ └── ERC20BalanceForwarderHandler.t.sol ├── hooks │ ├── BaseRewardsHooks.t.sol │ └── HookAggregator.t.sol ├── invariants │ ├── BaseInvariants.t.sol │ ├── StakingInvariants.t.sol │ └── TrackingInvariants.t.sol └── utils │ ├── Actor.sol │ ├── DeployPermit2.sol │ ├── Pretty.sol │ ├── PropertiesAsserts.sol │ ├── PropertiesConstants.sol │ └── StdAsserts.sol ├── scripts ├── echidna-assert.sh ├── echidna.sh └── medusa.sh ├── unit ├── POC.t.sol ├── RegisterReward.t.sol ├── Scenarios.t.sol ├── Staking.t.sol └── View.t.sol └── utils ├── MockController.sol ├── MockERC20.sol └── TestUtils.sol /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | submodules: recursive 16 | 17 | - name: Install foundry 18 | uses: foundry-rs/foundry-toolchain@v1 19 | with: 20 | version: nightly 21 | 22 | - name: Run foundry build 23 | run: | 24 | forge --version 25 | forge build 26 | id: build 27 | 28 | lint-check: 29 | runs-on: ubuntu-latest 30 | needs: build 31 | steps: 32 | - uses: actions/checkout@v3 33 | with: 34 | submodules: recursive 35 | - uses: foundry-rs/foundry-toolchain@v1 36 | with: 37 | version: nightly-c99854277c346fa6de7a8f9837230b36fd85850e 38 | 39 | - name: Run foundry fmt check 40 | run: | 41 | forge fmt --check 42 | id: fmt 43 | 44 | test: 45 | runs-on: ubuntu-latest 46 | needs: lint-check 47 | steps: 48 | - uses: actions/checkout@v3 49 | with: 50 | submodules: recursive 51 | - uses: foundry-rs/foundry-toolchain@v1 52 | with: 53 | version: nightly 54 | - name: Run foundry tests 55 | # --ast tests enables inline configs to work https://github.com/foundry-rs/foundry/issues/7310#issuecomment-1978088200 56 | run: | 57 | forge test -vvv --ast 58 | id: test 59 | 60 | coverage: 61 | runs-on: ubuntu-latest 62 | needs: lint-check 63 | steps: 64 | - uses: actions/checkout@v3 65 | with: 66 | submodules: recursive 67 | - uses: foundry-rs/foundry-toolchain@v1 68 | with: 69 | version: nightly 70 | - name: Run foundry coverage 71 | run: | 72 | FOUNDRY_PROFILE=coverage forge coverage --report summary 73 | id: coverage 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE - VSCode 2 | .vscode/ 3 | 4 | # Logs 5 | logs/ 6 | *.log 7 | 8 | # Dependency directories 9 | node_modules/ 10 | 11 | # Optional npm cache directory 12 | .npm/ 13 | 14 | # Compiler files 15 | cache/ 16 | out/ 17 | 18 | # Ignores development broadcast logs 19 | !/broadcast 20 | /broadcast/*/31337/ 21 | /broadcast/**/dry-run/ 22 | 23 | # Dotenv file 24 | *.env 25 | 26 | # System Files 27 | .DS_Store 28 | Thumbs.db 29 | 30 | # Coverage 31 | coverage/ 32 | *.info 33 | 34 | # Gas snapshot 35 | .gas-snapshot 36 | 37 | # Invariant testing files 38 | crytic-export/ 39 | _corpus/ 40 | 41 | # Certora verrifiction temp files 42 | .certora_internal/ 43 | gambit_out/ 44 | collect.json -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/openzeppelin/openzeppelin-contracts 7 | [submodule "lib/ethereum-vault-connector"] 8 | path = lib/ethereum-vault-connector 9 | url = https://github.com/euler-xyz/ethereum-vault-connector 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Euler Security Policy 2 | 3 | ## Vulnerability Disclosure and Bug Bounty 4 | 5 | Security is a top priority at Euler, and we engage in regular security reviews and have an active bug bounty program to ensure the integrity of our systems. 6 | 7 | To report a vulnerability, **please submit it through our bug bounty program**: 8 | [Euler Bug Bounty](https://euler.finance/bug-bounty) 9 | 10 | **Reports sent via email will not be accepted.** Email should only be used for general security inquiries. 11 | 12 | ## Security Team Contact Details 13 | 14 | For security-related questions or inquiries (not vulnerability reports), you can contact us via: 15 | - **Email**: [security@euler.xyz](mailto:security@euler.xyz) 16 | - **PGP Encryption**: [Euler Public Key](https://euler.finance/.well-known/public-key.asc) 17 | 18 | ## Previous Security Reviews 19 | 20 | Euler undergoes regular security audits. You can find details of previous security reviews here: 21 | [Euler Security Reviews](https://docs.euler.finance/security/audits) 22 | 23 | ## Preferred Languages 24 | 25 | We accept security-related inquiries in **English (en)** 26 | -------------------------------------------------------------------------------- /audits/Euler Cantina Code Competition report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/euler-xyz/reward-streams/a63c358265bc8184a6d893e510a2c8566351d2e6/audits/Euler Cantina Code Competition report.pdf -------------------------------------------------------------------------------- /audits/Euler Hunter Security report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/euler-xyz/reward-streams/a63c358265bc8184a6d893e510a2c8566351d2e6/audits/Euler Hunter Security report.pdf -------------------------------------------------------------------------------- /audits/Euler MixBytes report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/euler-xyz/reward-streams/a63c358265bc8184a6d893e510a2c8566351d2e6/audits/Euler MixBytes report.pdf -------------------------------------------------------------------------------- /audits/Euler yAudit Code Competition Fixes report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/euler-xyz/reward-streams/a63c358265bc8184a6d893e510a2c8566351d2e6/audits/Euler yAudit Code Competition Fixes report.pdf -------------------------------------------------------------------------------- /certora/conf/StakingRewardStream.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/StakingRewardStreams.sol", 4 | "certora/harness/ERC20Caller.sol" 5 | ], 6 | "packages": [ 7 | "evc=lib/ethereum-vault-connector/src", 8 | "openzeppelin-contracts=lib/openzeppelin-contracts/contracts", 9 | "forge-std=lib/forge-std/src" 10 | ], 11 | "verify": "StakingRewardStreams:certora/specs/StakingRewardStreams.spec", 12 | "solc": "solc8.23", 13 | "rule_sanity": "basic", 14 | "optimistic_loop": true, 15 | "loop_iter": "3", 16 | "parametric_contracts" : ["StakingRewardStreams","ERC20Caller"], 17 | 18 | // Gambit config 19 | "mutations": { 20 | // Automatically generated mutations 21 | "gambit": [ 22 | { 23 | "filename": "src/StakingRewardStreams.sol", 24 | "num_mutants": 2 25 | }, 26 | { 27 | "filename": "src/BaseRewardStreams.sol", 28 | "num_mutants": 3 29 | } 30 | ], 31 | // Manual mutations 32 | "manual_mutants": [ 33 | { 34 | "file_to_mutate": "src/StakingRewardStreams.sol", 35 | "mutants_location": "certora/mutations/staking/" 36 | }, 37 | { 38 | "file_to_mutate": "src/BaseRewardStreams.sol", 39 | "mutants_location": "certora/mutations/base/" 40 | } 41 | ], 42 | 43 | "msg": "Mutations example" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /certora/harness/ERC20Caller.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.20; 2 | 3 | import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; 4 | 5 | 6 | contract ERC20Caller { 7 | 8 | function externalTransfer(IERC20 token, address to, uint256 amount) public { 9 | token.transferFrom(msg.sender,to,amount); 10 | } 11 | } -------------------------------------------------------------------------------- /certora/mutations/staking/StakingRewardStreams_mut1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import {SafeERC20, IERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import {Set, SetStorage} from "evc/Set.sol"; 7 | import {BaseRewardStreams} from "./BaseRewardStreams.sol"; 8 | import {IStakingRewardStreams} from "./interfaces/IRewardStreams.sol"; 9 | 10 | /// @title StakingRewardStreams 11 | /// @custom:security-contact security@euler.xyz 12 | /// @author Euler Labs (https://www.eulerlabs.com/) 13 | /// @notice This contract inherits from `BaseRewardStreams` and implements `IStakingRewardStreams`. 14 | /// It allows for the rewards to be distributed to the rewarded token holders who have staked it. 15 | contract StakingRewardStreams is BaseRewardStreams, IStakingRewardStreams { 16 | using SafeERC20 for IERC20; 17 | using Set for SetStorage; 18 | 19 | /// @notice Constructor for the StakingRewardStreams contract. 20 | /// @param evc The Ethereum Vault Connector contract. 21 | /// @param periodDuration The duration of a period. 22 | constructor(address evc, uint48 periodDuration) BaseRewardStreams(evc, periodDuration) {} 23 | 24 | /// @notice Allows a user to stake rewarded tokens. 25 | /// @dev If the amount is max, the entire balance of the user is staked. 26 | /// @param rewarded The address of the rewarded token. 27 | /// @param amount The amount of tokens to stake. 28 | function stake(address rewarded, uint256 amount) external virtual override nonReentrant { 29 | address msgSender = _msgSender(); 30 | 31 | if (amount == type(uint256).max) { 32 | amount = IERC20(rewarded).balanceOf(msgSender); 33 | } 34 | 35 | if (amount == 0) { 36 | revert InvalidAmount(); 37 | } 38 | 39 | AccountStorage storage accountStorage = accounts[msgSender][rewarded]; 40 | uint256 currentAccountBalance = accountStorage.balance; 41 | address[] memory rewards = accountStorage.enabledRewards.get(); 42 | 43 | for (uint256 i = 0; i < rewards.length; ++i) { 44 | address reward = rewards[i]; 45 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 46 | 47 | // We always allocate rewards before updating any balances. 48 | updateRewardInternal( 49 | distributionStorage, accountStorage.earned[reward], rewarded, reward, currentAccountBalance, false 50 | ); 51 | 52 | // Mutation: change += to = 53 | distributionStorage.totalEligible = amount; 54 | } 55 | 56 | uint256 newAccountBalance = currentAccountBalance + amount; 57 | accountStorage.balance = newAccountBalance; 58 | 59 | emit BalanceUpdated(msgSender, rewarded, currentAccountBalance, newAccountBalance); 60 | 61 | pullToken(IERC20(rewarded), msgSender, amount); 62 | } 63 | 64 | /// @notice Allows a user to unstake rewarded tokens. 65 | /// @dev This function reverts if the recipient is zero address or is a known non-owner EVC account. 66 | /// @dev If the amount is max, the entire balance of the user is unstaked. 67 | /// @param rewarded The address of the rewarded token. 68 | /// @param recipient The address to receive the unstaked tokens. 69 | /// @param amount The amount of tokens to unstake. 70 | /// @param forfeitRecentReward Whether to forfeit the recent reward and not update the accumulator. 71 | function unstake( 72 | address rewarded, 73 | uint256 amount, 74 | address recipient, 75 | bool forfeitRecentReward 76 | ) external virtual override nonReentrant { 77 | address msgSender = _msgSender(); 78 | AccountStorage storage accountStorage = accounts[msgSender][rewarded]; 79 | uint256 currentAccountBalance = accountStorage.balance; 80 | 81 | if (amount == type(uint256).max) { 82 | amount = currentAccountBalance; 83 | } 84 | 85 | if (amount == 0 || amount > currentAccountBalance) { 86 | revert InvalidAmount(); 87 | } 88 | 89 | address[] memory rewards = accountStorage.enabledRewards.get(); 90 | 91 | for (uint256 i = 0; i < rewards.length; ++i) { 92 | address reward = rewards[i]; 93 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 94 | 95 | // We always allocate rewards before updating any balances. 96 | updateRewardInternal( 97 | distributionStorage, 98 | accountStorage.earned[reward], 99 | rewarded, 100 | reward, 101 | currentAccountBalance, 102 | forfeitRecentReward 103 | ); 104 | 105 | distributionStorage.totalEligible -= amount; 106 | } 107 | 108 | uint256 newAccountBalance = currentAccountBalance - amount; 109 | accountStorage.balance = newAccountBalance; 110 | 111 | emit BalanceUpdated(msgSender, rewarded, currentAccountBalance, newAccountBalance); 112 | 113 | pushToken(IERC20(rewarded), recipient, amount); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /certora/mutations/staking/StakingRewardStreams_mut2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import {SafeERC20, IERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import {Set, SetStorage} from "evc/Set.sol"; 7 | import {BaseRewardStreams} from "./BaseRewardStreams.sol"; 8 | import {IStakingRewardStreams} from "./interfaces/IRewardStreams.sol"; 9 | 10 | /// @title StakingRewardStreams 11 | /// @custom:security-contact security@euler.xyz 12 | /// @author Euler Labs (https://www.eulerlabs.com/) 13 | /// @notice This contract inherits from `BaseRewardStreams` and implements `IStakingRewardStreams`. 14 | /// It allows for the rewards to be distributed to the rewarded token holders who have staked it. 15 | contract StakingRewardStreams is BaseRewardStreams, IStakingRewardStreams { 16 | using SafeERC20 for IERC20; 17 | using Set for SetStorage; 18 | 19 | /// @notice Constructor for the StakingRewardStreams contract. 20 | /// @param evc The Ethereum Vault Connector contract. 21 | /// @param periodDuration The duration of a period. 22 | constructor(address evc, uint48 periodDuration) BaseRewardStreams(evc, periodDuration) {} 23 | 24 | /// @notice Allows a user to stake rewarded tokens. 25 | /// @dev If the amount is max, the entire balance of the user is staked. 26 | /// @param rewarded The address of the rewarded token. 27 | /// @param amount The amount of tokens to stake. 28 | function stake(address rewarded, uint256 amount) external virtual override nonReentrant { 29 | address msgSender = _msgSender(); 30 | 31 | if (amount == type(uint256).max) { 32 | amount = IERC20(rewarded).balanceOf(msgSender); 33 | } 34 | 35 | if (amount == 0) { 36 | revert InvalidAmount(); 37 | } 38 | 39 | AccountStorage storage accountStorage = accounts[msgSender][rewarded]; 40 | uint256 currentAccountBalance = accountStorage.balance; 41 | address[] memory rewards = accountStorage.enabledRewards.get(); 42 | 43 | for (uint256 i = 0; i < rewards.length; ++i) { 44 | address reward = rewards[i]; 45 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 46 | 47 | // We always allocate rewards before updating any balances. 48 | updateRewardInternal( 49 | distributionStorage, accountStorage.earned[reward], rewarded, reward, currentAccountBalance, false 50 | ); 51 | 52 | distributionStorage.totalEligible += amount; 53 | } 54 | 55 | uint256 newAccountBalance = currentAccountBalance + amount; 56 | accountStorage.balance = newAccountBalance; 57 | 58 | emit BalanceUpdated(msgSender, rewarded, currentAccountBalance, newAccountBalance); 59 | 60 | pullToken(IERC20(rewarded), msgSender, amount); 61 | } 62 | 63 | /// @notice Allows a user to unstake rewarded tokens. 64 | /// @dev This function reverts if the recipient is zero address or is a known non-owner EVC account. 65 | /// @dev If the amount is max, the entire balance of the user is unstaked. 66 | /// @param rewarded The address of the rewarded token. 67 | /// @param recipient The address to receive the unstaked tokens. 68 | /// @param amount The amount of tokens to unstake. 69 | /// @param forfeitRecentReward Whether to forfeit the recent reward and not update the accumulator. 70 | function unstake( 71 | address rewarded, 72 | uint256 amount, 73 | address recipient, 74 | bool forfeitRecentReward 75 | ) external virtual override nonReentrant { 76 | address msgSender = _msgSender(); 77 | AccountStorage storage accountStorage = accounts[msgSender][rewarded]; 78 | uint256 currentAccountBalance = accountStorage.balance; 79 | 80 | if (amount == type(uint256).max) { 81 | amount = currentAccountBalance; 82 | } 83 | 84 | if (amount == 0 || amount > currentAccountBalance) { 85 | revert InvalidAmount(); 86 | } 87 | 88 | address[] memory rewards = accountStorage.enabledRewards.get(); 89 | 90 | for (uint256 i = 0; i < rewards.length; ++i) { 91 | address reward = rewards[i]; 92 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 93 | 94 | // We always allocate rewards before updating any balances. 95 | updateRewardInternal( 96 | distributionStorage, 97 | accountStorage.earned[reward], 98 | rewarded, 99 | reward, 100 | currentAccountBalance, 101 | forfeitRecentReward 102 | ); 103 | 104 | distributionStorage.totalEligible -= amount; 105 | } 106 | 107 | // Mutation: change - to + 108 | uint256 newAccountBalance = currentAccountBalance + amount; 109 | accountStorage.balance = newAccountBalance; 110 | 111 | emit BalanceUpdated(msgSender, rewarded, currentAccountBalance, newAccountBalance); 112 | 113 | pushToken(IERC20(rewarded), recipient, amount); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /certora/mutations/staking/StakingRewardStreams_mut3.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import {SafeERC20, IERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import {Set, SetStorage} from "evc/Set.sol"; 7 | import {BaseRewardStreams} from "./BaseRewardStreams.sol"; 8 | import {IStakingRewardStreams} from "./interfaces/IRewardStreams.sol"; 9 | 10 | /// @title StakingRewardStreams 11 | /// @custom:security-contact security@euler.xyz 12 | /// @author Euler Labs (https://www.eulerlabs.com/) 13 | /// @notice This contract inherits from `BaseRewardStreams` and implements `IStakingRewardStreams`. 14 | /// It allows for the rewards to be distributed to the rewarded token holders who have staked it. 15 | contract StakingRewardStreams is BaseRewardStreams, IStakingRewardStreams { 16 | using SafeERC20 for IERC20; 17 | using Set for SetStorage; 18 | 19 | /// @notice Constructor for the StakingRewardStreams contract. 20 | /// @param evc The Ethereum Vault Connector contract. 21 | /// @param periodDuration The duration of a period. 22 | constructor(address evc, uint48 periodDuration) BaseRewardStreams(evc, periodDuration) {} 23 | 24 | /// @notice Allows a user to stake rewarded tokens. 25 | /// @dev If the amount is max, the entire balance of the user is staked. 26 | /// @param rewarded The address of the rewarded token. 27 | /// @param amount The amount of tokens to stake. 28 | function stake(address rewarded, uint256 amount) external virtual override nonReentrant { 29 | address msgSender = _msgSender(); 30 | 31 | if (amount == type(uint256).max) { 32 | amount = IERC20(rewarded).balanceOf(msgSender); 33 | } 34 | 35 | if (amount == 0) { 36 | revert InvalidAmount(); 37 | } 38 | 39 | AccountStorage storage accountStorage = accounts[msgSender][rewarded]; 40 | uint256 currentAccountBalance = accountStorage.balance; 41 | address[] memory rewards = accountStorage.enabledRewards.get(); 42 | 43 | for (uint256 i = 0; i < rewards.length; ++i) { 44 | address reward = rewards[i]; 45 | // Mutation: switching reward and rewarded 46 | DistributionStorage storage distributionStorage = distributions[reward][rewarded]; 47 | 48 | // We always allocate rewards before updating any balances. 49 | updateRewardInternal( 50 | distributionStorage, accountStorage.earned[reward], rewarded, reward, currentAccountBalance, false 51 | ); 52 | 53 | distributionStorage.totalEligible += amount; 54 | } 55 | 56 | uint256 newAccountBalance = currentAccountBalance + amount; 57 | accountStorage.balance = newAccountBalance; 58 | 59 | emit BalanceUpdated(msgSender, rewarded, currentAccountBalance, newAccountBalance); 60 | 61 | pullToken(IERC20(rewarded), msgSender, amount); 62 | } 63 | 64 | /// @notice Allows a user to unstake rewarded tokens. 65 | /// @dev This function reverts if the recipient is zero address or is a known non-owner EVC account. 66 | /// @dev If the amount is max, the entire balance of the user is unstaked. 67 | /// @param rewarded The address of the rewarded token. 68 | /// @param recipient The address to receive the unstaked tokens. 69 | /// @param amount The amount of tokens to unstake. 70 | /// @param forfeitRecentReward Whether to forfeit the recent reward and not update the accumulator. 71 | function unstake( 72 | address rewarded, 73 | uint256 amount, 74 | address recipient, 75 | bool forfeitRecentReward 76 | ) external virtual override nonReentrant { 77 | address msgSender = _msgSender(); 78 | AccountStorage storage accountStorage = accounts[msgSender][rewarded]; 79 | uint256 currentAccountBalance = accountStorage.balance; 80 | 81 | if (amount == type(uint256).max) { 82 | amount = currentAccountBalance; 83 | } 84 | 85 | if (amount == 0 || amount > currentAccountBalance) { 86 | revert InvalidAmount(); 87 | } 88 | 89 | address[] memory rewards = accountStorage.enabledRewards.get(); 90 | 91 | for (uint256 i = 0; i < rewards.length; ++i) { 92 | address reward = rewards[i]; 93 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 94 | 95 | // We always allocate rewards before updating any balances. 96 | updateRewardInternal( 97 | distributionStorage, 98 | accountStorage.earned[reward], 99 | rewarded, 100 | reward, 101 | currentAccountBalance, 102 | forfeitRecentReward 103 | ); 104 | 105 | distributionStorage.totalEligible -= amount; 106 | } 107 | 108 | uint256 newAccountBalance = currentAccountBalance - amount; 109 | accountStorage.balance = newAccountBalance; 110 | 111 | emit BalanceUpdated(msgSender, rewarded, currentAccountBalance, newAccountBalance); 112 | 113 | pushToken(IERC20(rewarded), recipient, amount); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /certora/mutations/staking/StakingRewardStreams_mut4.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import {SafeERC20, IERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import {Set, SetStorage} from "evc/Set.sol"; 7 | import {BaseRewardStreams} from "./BaseRewardStreams.sol"; 8 | import {IStakingRewardStreams} from "./interfaces/IRewardStreams.sol"; 9 | 10 | /// @title StakingRewardStreams 11 | /// @custom:security-contact security@euler.xyz 12 | /// @author Euler Labs (https://www.eulerlabs.com/) 13 | /// @notice This contract inherits from `BaseRewardStreams` and implements `IStakingRewardStreams`. 14 | /// It allows for the rewards to be distributed to the rewarded token holders who have staked it. 15 | contract StakingRewardStreams is BaseRewardStreams, IStakingRewardStreams { 16 | using SafeERC20 for IERC20; 17 | using Set for SetStorage; 18 | 19 | /// @notice Constructor for the StakingRewardStreams contract. 20 | /// @param evc The Ethereum Vault Connector contract. 21 | /// @param periodDuration The duration of a period. 22 | constructor(address evc, uint48 periodDuration) BaseRewardStreams(evc, periodDuration) {} 23 | 24 | /// @notice Allows a user to stake rewarded tokens. 25 | /// @dev If the amount is max, the entire balance of the user is staked. 26 | /// @param rewarded The address of the rewarded token. 27 | /// @param amount The amount of tokens to stake. 28 | function stake(address rewarded, uint256 amount) external virtual override nonReentrant { 29 | address msgSender = _msgSender(); 30 | 31 | if (amount == type(uint256).max) { 32 | amount = IERC20(rewarded).balanceOf(msgSender); 33 | } 34 | 35 | if (amount == 0) { 36 | revert InvalidAmount(); 37 | } 38 | 39 | AccountStorage storage accountStorage = accounts[msgSender][rewarded]; 40 | uint256 currentAccountBalance = accountStorage.balance; 41 | address[] memory rewards = accountStorage.enabledRewards.get(); 42 | 43 | for (uint256 i = 0; i < rewards.length; ++i) { 44 | address reward = rewards[i]; 45 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 46 | 47 | // We always allocate rewards before updating any balances. 48 | updateRewardInternal( 49 | distributionStorage, accountStorage.earned[reward], rewarded, reward, currentAccountBalance, false 50 | ); 51 | 52 | distributionStorage.totalEligible += amount; 53 | } 54 | 55 | uint256 newAccountBalance = currentAccountBalance + amount; 56 | accountStorage.balance = newAccountBalance; 57 | 58 | emit BalanceUpdated(msgSender, rewarded, currentAccountBalance, newAccountBalance); 59 | 60 | pullToken(IERC20(rewarded), msgSender, amount); 61 | } 62 | 63 | /// @notice Allows a user to unstake rewarded tokens. 64 | /// @dev This function reverts if the recipient is zero address or is a known non-owner EVC account. 65 | /// @dev If the amount is max, the entire balance of the user is unstaked. 66 | /// @param rewarded The address of the rewarded token. 67 | /// @param recipient The address to receive the unstaked tokens. 68 | /// @param amount The amount of tokens to unstake. 69 | /// @param forfeitRecentReward Whether to forfeit the recent reward and not update the accumulator. 70 | function unstake( 71 | address rewarded, 72 | uint256 amount, 73 | address recipient, 74 | bool forfeitRecentReward 75 | ) external virtual override nonReentrant { 76 | address msgSender = _msgSender(); 77 | AccountStorage storage accountStorage = accounts[msgSender][rewarded]; 78 | uint256 currentAccountBalance = accountStorage.balance; 79 | 80 | if (amount == type(uint256).max) { 81 | amount = currentAccountBalance; 82 | } 83 | 84 | if (amount == 0 || amount > currentAccountBalance) { 85 | revert InvalidAmount(); 86 | } 87 | 88 | address[] memory rewards = accountStorage.enabledRewards.get(); 89 | 90 | for (uint256 i = 0; i < rewards.length; ++i) { 91 | address reward = rewards[i]; 92 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 93 | 94 | // We always allocate rewards before updating any balances. 95 | updateRewardInternal( 96 | distributionStorage, 97 | accountStorage.earned[reward], 98 | rewarded, 99 | reward, 100 | currentAccountBalance, 101 | forfeitRecentReward 102 | ); 103 | 104 | distributionStorage.totalEligible -= amount; 105 | } 106 | 107 | uint256 newAccountBalance = currentAccountBalance - amount; 108 | accountStorage.balance = newAccountBalance; 109 | 110 | emit BalanceUpdated(msgSender, rewarded, currentAccountBalance, newAccountBalance); 111 | 112 | // Mutation: removed token push 113 | // pushToken(IERC20(rewarded), recipient, amount); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /certora/mutations/staking/StakingRewardStreams_mut5.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import {SafeERC20, IERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import {Set, SetStorage} from "evc/Set.sol"; 7 | import {BaseRewardStreams} from "./BaseRewardStreams.sol"; 8 | import {IStakingRewardStreams} from "./interfaces/IRewardStreams.sol"; 9 | 10 | /// @title StakingRewardStreams 11 | /// @custom:security-contact security@euler.xyz 12 | /// @author Euler Labs (https://www.eulerlabs.com/) 13 | /// @notice This contract inherits from `BaseRewardStreams` and implements `IStakingRewardStreams`. 14 | /// It allows for the rewards to be distributed to the rewarded token holders who have staked it. 15 | contract StakingRewardStreams is BaseRewardStreams, IStakingRewardStreams { 16 | using SafeERC20 for IERC20; 17 | using Set for SetStorage; 18 | 19 | /// @notice Constructor for the StakingRewardStreams contract. 20 | /// @param evc The Ethereum Vault Connector contract. 21 | /// @param periodDuration The duration of a period. 22 | constructor(address evc, uint48 periodDuration) BaseRewardStreams(evc, periodDuration) {} 23 | 24 | /// @notice Allows a user to stake rewarded tokens. 25 | /// @dev If the amount is max, the entire balance of the user is staked. 26 | /// @param rewarded The address of the rewarded token. 27 | /// @param amount The amount of tokens to stake. 28 | function stake(address rewarded, uint256 amount) external virtual override nonReentrant { 29 | address msgSender = _msgSender(); 30 | 31 | if (amount == type(uint256).max) { 32 | amount = IERC20(rewarded).balanceOf(msgSender); 33 | } 34 | 35 | if (amount == 0) { 36 | revert InvalidAmount(); 37 | } 38 | 39 | AccountStorage storage accountStorage = accounts[msgSender][rewarded]; 40 | uint256 currentAccountBalance = accountStorage.balance; 41 | address[] memory rewards = accountStorage.enabledRewards.get(); 42 | 43 | for (uint256 i = 0; i < rewards.length; ++i) { 44 | address reward = rewards[i]; 45 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 46 | 47 | // We always allocate rewards before updating any balances. 48 | updateRewardInternal( 49 | distributionStorage, accountStorage.earned[reward], rewarded, reward, currentAccountBalance, false 50 | ); 51 | 52 | distributionStorage.totalEligible += amount; 53 | } 54 | 55 | uint256 newAccountBalance = currentAccountBalance + amount; 56 | accountStorage.balance = newAccountBalance; 57 | 58 | emit BalanceUpdated(msgSender, rewarded, currentAccountBalance, newAccountBalance); 59 | 60 | // mutation- no tokens transfered 61 | //pullToken(IERC20(rewarded), msgSender, amount); 62 | } 63 | 64 | /// @notice Allows a user to unstake rewarded tokens. 65 | /// @dev This function reverts if the recipient is zero address or is a known non-owner EVC account. 66 | /// @dev If the amount is max, the entire balance of the user is unstaked. 67 | /// @param rewarded The address of the rewarded token. 68 | /// @param recipient The address to receive the unstaked tokens. 69 | /// @param amount The amount of tokens to unstake. 70 | /// @param forfeitRecentReward Whether to forfeit the recent reward and not update the accumulator. 71 | function unstake( 72 | address rewarded, 73 | uint256 amount, 74 | address recipient, 75 | bool forfeitRecentReward 76 | ) external virtual override nonReentrant { 77 | address msgSender = _msgSender(); 78 | AccountStorage storage accountStorage = accounts[msgSender][rewarded]; 79 | uint256 currentAccountBalance = accountStorage.balance; 80 | 81 | if (amount == type(uint256).max) { 82 | amount = currentAccountBalance; 83 | } 84 | 85 | if (amount == 0 || amount > currentAccountBalance) { 86 | revert InvalidAmount(); 87 | } 88 | 89 | address[] memory rewards = accountStorage.enabledRewards.get(); 90 | 91 | for (uint256 i = 0; i < rewards.length; ++i) { 92 | address reward = rewards[i]; 93 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 94 | 95 | // We always allocate rewards before updating any balances. 96 | updateRewardInternal( 97 | distributionStorage, 98 | accountStorage.earned[reward], 99 | rewarded, 100 | reward, 101 | currentAccountBalance, 102 | forfeitRecentReward 103 | ); 104 | 105 | distributionStorage.totalEligible -= amount; 106 | } 107 | 108 | uint256 newAccountBalance = currentAccountBalance - amount; 109 | accountStorage.balance = newAccountBalance; 110 | 111 | emit BalanceUpdated(msgSender, rewarded, currentAccountBalance, newAccountBalance); 112 | 113 | pushToken(IERC20(rewarded), recipient, amount); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /certora/specs/ERC20/erc20.spec: -------------------------------------------------------------------------------- 1 | methods { 2 | // ERC20 standard 3 | function _.name() external => NONDET; // can we use PER_CALLEE_CONSTANT? 4 | function _.symbol() external => NONDET; // can we use PER_CALLEE_CONSTANT? 5 | function _.decimals() external => PER_CALLEE_CONSTANT; 6 | function _.totalSupply() external => totalSupplyByToken[calledContract] expect uint256; 7 | function _.balanceOf(address a) external => balanceByToken[calledContract][a] expect uint256; 8 | function _.allowance(address a, address b) external => allowanceByToken[calledContract][a][b] expect uint256; 9 | function _.approve(address a, uint256 x) external with (env e) => approveCVL(calledContract, e.msg.sender, a, x) expect bool; 10 | function _.transfer(address a, uint256 x) external with (env e) => transferCVL(calledContract, e.msg.sender, a, x) expect bool; 11 | function _.transferFrom(address a, address b, uint256 x) external with (env e) => transferFromCVL(calledContract, e.msg.sender, a, b, x) expect bool; 12 | 13 | } 14 | 15 | 16 | /// CVL simple implementations of IERC20: 17 | /// token => totalSupply 18 | ghost mapping(address => uint256) totalSupplyByToken ; 19 | 20 | /// token => account => balance 21 | ghost mapping(address => mapping(address => uint256)) balanceByToken { 22 | init_state axiom forall address token. balanceByToken[token][currentContract] == 0; 23 | } 24 | 25 | /// token => owner => spender => allowance 26 | ghost mapping(address => mapping(address => mapping(address => uint256))) allowanceByToken; 27 | 28 | function externalBalanceOf(address token, address account) returns uint256 { 29 | return balanceByToken[token][account]; 30 | } 31 | 32 | function approveCVL(address token, address approver, address spender, uint256 amount) returns bool { 33 | // should be randomly reverting xxx 34 | bool nondetSuccess; 35 | if (!nondetSuccess) return false; 36 | 37 | allowanceByToken[token][approver][spender] = amount; 38 | return true; 39 | } 40 | 41 | function transferFromCVL(address token, address spender, address from, address to, uint256 amount) returns bool { 42 | // should be randomly reverting xxx 43 | bool nondetSuccess; 44 | if (!nondetSuccess) return false; 45 | 46 | if (allowanceByToken[token][from][spender] < amount) return false; 47 | allowanceByToken[token][from][spender] = assert_uint256(allowanceByToken[token][from][spender] - amount); 48 | return transferCVL(token, from, to, amount); 49 | } 50 | 51 | function transferCVL(address token, address from, address to, uint256 amount) returns bool { 52 | // should be randomly reverting xxx 53 | bool nondetSuccess; 54 | if (!nondetSuccess) return false; 55 | 56 | if(balanceByToken[token][from] < amount) return false; 57 | require(from != to => balanceByToken[token][from] + balanceByToken[token][to] <= to_mathint(totalSupplyByToken[token])); 58 | balanceByToken[token][from] = assert_uint256(balanceByToken[token][from] - amount); 59 | balanceByToken[token][to] = require_uint256(balanceByToken[token][to] + amount); // We neglect overflows. 60 | return true; 61 | } -------------------------------------------------------------------------------- /certora/specs/StakingRewardStreams.spec: -------------------------------------------------------------------------------- 1 | //// @title Verification of StakingRewardStream 2 | /* 3 | to run: 4 | certoraRun certora/conf/StakingRewardStream.conf 5 | see: 6 | docs.certora.com on CVL and Certora Prover 7 | 8 | */ 9 | 10 | import "./ERC20/erc20.spec"; 11 | 12 | methods { 13 | // Main contract 14 | function balanceOf(address, address) external returns (uint256) envfree; 15 | function rewardAmount(address, address, uint48) external returns (uint256) envfree; 16 | function totalRewardClaimed(address, address) external returns (uint256) envfree; 17 | function totalRewardRegistered(address, address) external returns (uint256) envfree; 18 | function getEpoch(uint48) external returns (uint48) envfree; 19 | 20 | // In order to assume msgSender is not EVC and not the current contract, we summarize msgSedner function to return the current msg.sender 21 | function EVCUtil._msgSender() internal returns (address) with (env e) => assumeNotEVC(e.msg.sender); 22 | 23 | } 24 | 25 | 26 | /******* CVL Functions and Definitions *******/ 27 | function assumeNotEVC(address msgSender) returns address { 28 | require msgSender != currentContract.evc; 29 | require msgSender != currentContract; 30 | return msgSender; 31 | } 32 | 33 | definition getEpochCVL(uint256 storageIndex, uint256 slot) returns mathint = storageIndex*2+slot; 34 | 35 | /// @title The `SCALER` - since we cannot use constants in quantifiers 36 | definition SCALER() returns uint256 = 2* 10^19; 37 | 38 | /// @title The `MAX_EPOCH_DURATION` (70 days) - since we cannot use constants 39 | definition MAX_EPOCH_DURATION() returns uint256 = 10 * 7 * 24 * 60 * 60; 40 | 41 | /******* Ghost and Hooks *******/ 42 | 43 | 44 | /// @notice `sumBalancesPerRewarded[rewarded]` represents the sum of `accounts[account][rewarded].balance` for all account 45 | ghost mapping(address => mathint) sumBalancesPerRewarded { 46 | init_state axiom forall address t. sumBalancesPerRewarded[t]==0; 47 | } 48 | 49 | 50 | /// @notice Hook onto `AccountStorage` to update `sumBalancesPerRewarded` 51 | hook Sstore accounts[KEY address account][KEY address rewarded].balance uint256 value (uint256 oldValue) 52 | { 53 | sumBalancesPerRewarded[rewarded] = sumBalancesPerRewarded[rewarded] + value - oldValue; 54 | } 55 | 56 | /** @notice sumOfNotDistributed[reward] represents the sum of reward token not distributed yet. It is computed as the sum of`totalRegistered` minus the `totalClaimed` 57 | */ 58 | ghost mapping(address => mathint) sumOfNotDistributed { 59 | init_state axiom forall address t. sumOfNotDistributed[t]==0; 60 | } 61 | 62 | 63 | /// @notice Hook onto `DistributionStorage.totalRegistered` to update `sumOfNotDistributed` 64 | hook Sstore distributions[KEY address rewarded][KEY address reward].totalRegistered uint128 value (uint128 oldValue) 65 | { 66 | sumOfNotDistributed[reward] = sumOfNotDistributed[reward] + value - oldValue; 67 | } 68 | 69 | /// @notice Hook onto `DistributionStorage.totalClaimed` to update `sumOfNotDistributed` 70 | hook Sstore distributions[KEY address rewarded][KEY address reward].totalClaimed uint128 value (uint128 oldValue) 71 | { 72 | sumOfNotDistributed[reward] = sumOfNotDistributed[reward] - value + oldValue; 73 | } 74 | 75 | 76 | /** @notice Partial sum of amounts to distribute per start epoch up to end epoch. 77 | sumOfAmountPerEpochStartToEpochEnd[rewarded][reward][start-epoch][end-epoch] represents the of total distribution amount of reward for rewarded between start-epoch to end-epoch (including both start and end epochs) 78 | */ 79 | 80 | ghost mapping(address => mapping(address => mapping(mathint => mapping(mathint => mathint)))) sumOfAmountPerEpochStartToEpochEnd { 81 | init_state axiom forall address rewarded. 82 | forall address reward. 83 | forall mathint start. 84 | forall mathint end. 85 | sumOfAmountPerEpochStartToEpochEnd[rewarded][reward][start][end] == 0; 86 | } 87 | /** @notice Hook onto `DistributionStorage.amounts` to update `sumOfAmountPerEpochStartToEpochEnd` . A single update to some epoch i updates all 88 | sumOfAmountPerEpochStartToEpochEnd[j][k] in which j <= i <= k 89 | */ 90 | hook Sstore distributions[KEY address _rewarded][KEY address _reward].amounts[KEY uint256 storageIndex][INDEX uint256 slot] uint128 value (uint128 oldValue) { 91 | havoc sumOfAmountPerEpochStartToEpochEnd assuming 92 | forall address rewarded. 93 | forall address reward. 94 | forall mathint start. 95 | forall mathint end. 96 | sumOfAmountPerEpochStartToEpochEnd@new[rewarded][reward][start][end] == 97 | sumOfAmountPerEpochStartToEpochEnd@old[rewarded][reward][start][end] + 98 | ( 99 | // if it is the update rewarded, reward and the epoch is in between the start and the end, update the ghost 100 | (rewarded == _rewarded && reward == _reward && start >= getEpochCVL(storageIndex, slot) && end <= getEpochCVL(storageIndex, slot)) ? 101 | (value - oldValue) : 0 102 | ); 103 | 104 | } 105 | 106 | /******* Valid State *******/ 107 | 108 | 109 | 110 | /// @title `EPOCH_DURATION` is not larger than `MAX_EPOCH_DURATION` 111 | invariant EpochDurationSizeLimit() 112 | currentContract.EPOCH_DURATION <= MAX_EPOCH_DURATION(); 113 | 114 | 115 | /// @title Each sumOfAmountPerEpochStartToEpochEnd is less than the corresponding totalRegistered 116 | invariant epochSumsLETotalRegistered() 117 | forall address rewarded. 118 | forall address reward. 119 | forall mathint start. 120 | forall mathint end. 121 | sumOfAmountPerEpochStartToEpochEnd[rewarded][reward][start][end] <= to_mathint(currentContract.distributions[rewarded][reward].totalRegistered); 122 | 123 | 124 | /// @title `totalRewardRegistered` is limited 125 | /// totalRewardRegistered(rewarded, reward) * SCALER() <= max_uint160; 126 | invariant totalRegisteredMaxValue() 127 | ( 128 | forall address rewarded. forall address reward. 129 | to_mathint(currentContract.distributions[rewarded][reward].totalRegistered) * SCALER() <= max_uint160 130 | ) { 131 | preserved { 132 | requireInvariant epochSumsLETotalRegistered(); 133 | } 134 | } 135 | 136 | 137 | /// @title Reward amount per epoch is not greater than total registered reward 138 | invariant rewardAmountLessThanTotal(address rewarded, address reward, uint48 epoch) 139 | rewardAmount(rewarded, reward, epoch) <= totalRewardRegistered(rewarded, reward); 140 | 141 | 142 | 143 | /******* High Level Properties *******/ 144 | 145 | 146 | /** @title Invariant: reward token solvency. 147 | The expected balance of rewarded and reward token: 148 | reward.balanceOf(this) === forall rewarded : sum of (totalRegistered - totalClaimed ) 149 | rewarded.balanceOf(this) === sum forall account balanceOf(account,rewarded); 150 | 151 | The same token can be both a reward and rewarded. 152 | */ 153 | 154 | invariant solvency(address token) 155 | to_mathint(externalBalanceOf(token, currentContract)) >= sumBalancesPerRewarded[token] + sumOfNotDistributed[token] 156 | 157 | { 158 | preserved ERC20Caller.externalTransfer(address erc20, address to, uint256 amount) with (env e) { 159 | //assume ( better to prove this) that current contract does not have a dynamic call 160 | require e.msg.sender != currentContract; 161 | } 162 | } 163 | 164 | 165 | 166 | /******* Staking Properties *******/ 167 | 168 | /// @title Staking increases staked balance by given amount 169 | rule stakeIntegrity(address rewarded, uint256 amount) { 170 | env e; 171 | require e.msg.sender != currentContract.evc; 172 | uint256 preBalance = balanceOf(e.msg.sender, rewarded); 173 | 174 | stake(e, rewarded, amount); 175 | 176 | uint256 postBalance = balanceOf(e.msg.sender, rewarded); 177 | 178 | assert ( 179 | amount != max_uint256 => to_mathint(postBalance) == preBalance + amount, 180 | "Staking increases staked balance by given amount" 181 | ); 182 | } 183 | 184 | 185 | /******* Rewards Properties *******/ 186 | /// @title An example showing rewards can be given 187 | rule canBeRewarded(address rewarded, address reward, address recipient, bool forfeitRecentReward) { 188 | uint256 preBalance = externalBalanceOf(rewarded, recipient); 189 | 190 | env e; 191 | claimReward(e, rewarded, reward, recipient, forfeitRecentReward); 192 | 193 | uint256 postBalance = externalBalanceOf(rewarded, recipient); 194 | satisfy postBalance > preBalance; 195 | } 196 | 197 | 198 | /// @title Those that stake more can earn more rewards 199 | 200 | rule stakeMoreEarnMore( 201 | address staker1, 202 | address staker2, 203 | address rewarded, 204 | address reward, 205 | bool forfeitRecentReward 206 | ) { 207 | 208 | 209 | env earlyEnv; 210 | env lateEnv; 211 | require lateEnv.block.timestamp > earlyEnv.block.timestamp; 212 | 213 | uint256 earned1 = earnedReward(earlyEnv, staker1, rewarded, reward, forfeitRecentReward); 214 | uint256 earned2 = earnedReward(earlyEnv, staker2, rewarded, reward, forfeitRecentReward); 215 | 216 | uint256 earned1Late = earnedReward(lateEnv, staker1, rewarded, reward, forfeitRecentReward); 217 | uint256 earned2Late = earnedReward(lateEnv, staker2, rewarded, reward, forfeitRecentReward); 218 | 219 | mathint diff1 = earned1Late - earned1; 220 | mathint diff2 = earned2Late - earned2; 221 | 222 | 223 | satisfy ( 224 | balanceOf(staker1, rewarded) > balanceOf(staker2, rewarded) && diff1 > diff2 225 | ); 226 | } 227 | 228 | /// @title Staking and immediately unstaking should not yield profit 229 | rule stakeUnStakeNoBonus(uint256 amount, address token, address staker, bool forfeitRecentReward) { 230 | 231 | require amount < max_uint256; 232 | uint256 preBalance = externalBalanceOf(token, staker); 233 | 234 | env e; 235 | require e.msg.sender == staker; 236 | stake(e, token, amount); 237 | unstake(e, token, amount, staker, forfeitRecentReward); 238 | 239 | uint256 postBalance = externalBalanceOf(token, staker); 240 | assert ( 241 | postBalance <= preBalance, 242 | "staking and immediately un-staking should give no reward" 243 | ); 244 | } 245 | 246 | 247 | 248 | // ---- Claimed and total reward ----------------------------------------------- 249 | 250 | /// @title Total claimed is non-decreasing 251 | rule totalClaimedIsNonDecreasing(method f, address rewarded, address reward) { 252 | uint256 preClaimed = totalRewardClaimed(rewarded, reward); 253 | 254 | env e; 255 | calldataarg args; 256 | f(e, args); 257 | 258 | uint256 postClaimed = totalRewardClaimed(rewarded, reward); 259 | assert (postClaimed >= preClaimed, "total claimed is non-decreasing"); 260 | } 261 | 262 | 263 | /// @title Staked balance is reduced only by calling `unstake` 264 | rule stakedReduceProperty(method f, address account, address rewarded) { 265 | uint256 preBalance = balanceOf(account, rewarded); 266 | 267 | env e; 268 | calldataarg args; 269 | f(e, args); 270 | 271 | uint256 postBalance = balanceOf(account, rewarded); 272 | assert ( 273 | postBalance < preBalance => 274 | f.selector == sig:unstake(address, uint256, address, bool).selector, 275 | "staked reduced only by unstake" 276 | ); 277 | } 278 | 279 | 280 | 281 | -------------------------------------------------------------------------------- /certora_all/properties.md: -------------------------------------------------------------------------------- 1 | 2 | ## uint-test: 3 | * Integrity of f 4 | * stake(r, x); stake(r, y) === stake(r, x+ y) 5 | comparing only internal balance - easy 6 | compare also balances external - harder 7 | all storage 8 | * unstake(r, x, to, b); unstake(r, y, to, b) === unstake(r, x+ y, to, b) 9 | * earnedReward(...) == change to balance due to claimReward(...); 10 | * earnedReward(...) <= totalRewardRegistered(...) 11 | 12 | 13 | ## valid-state: 14 | * for each (reard, rewarded) totalRegistered >= totalClaimed 15 | * totalRegistered sum of amounts 16 | 17 | ## state change: 18 | * disableReward() => no change to enable return 19 | f(); 20 | 21 | 22 | (rewarded, reward) : not-registered, registered, registered and active, registered and not active 23 | 24 | per user (rewarded, reward) : enable - per user, disable - per user, not-claimable, earned , 25 | 26 | epoch: not-yet, active, over 27 | 28 | ## variable change: 29 | * accumulator is update together lasttimestamp 30 | 31 | ## risk-assessment: 32 | * double claim 33 | 34 | token.balanceOf(this) >= ... 35 | 36 | 37 | user.accumulated == 0 => f() ; earnedReward() == 0 even if time elapses 38 | ## high level: 39 | ========== 40 | 41 | token.balanceOf(this) decrease => 42 | balanceOf(e.msg.sender, token) decrease || 43 | earnedReward(e.msg.sender || 0 , X , token, b) should decrease 44 | 45 | 46 | // no free lunch: 47 | 48 | if no time elapes total assets of user should not change: 49 | token.balanceOf(user) + balanceOf(user, token) + earnedReward(user, all token, token) 50 | 51 | /* reward token */ 52 | reward.balanceOf(this) == forall rewarded : sum of (totalRegistered - totalClaimed ) 53 | rewarded.balanceOf(this) == sum balanceOf(account,rewarded); 54 | 55 | ========== community review ========= 56 | https://github.com/0xgreywolf/euler-vault-cantina-fv/tree/competition-official 57 | 58 | https://prover.certora.com/output/541734/71448b9ab45d4917a768c4a6b1ddb085/?anonymousKey=97ed825f33895e86082418bc7cc900718bca528a 59 | 0xGreyWolf earned 95$ 60 | 61 | alexzoid 62 | https://github.com/alexzoid-eth/euler-vault-cantina-fv/tree/master/certora/specs 63 | 64 | https://prover.certora.com/output/52567/e045e959a0554172878936bb6a12953d/?anonymousKey=5796c3ed8001f3f4a3dac91d14252521910383dc 65 | 66 | 67 | found almost all -------------------------------------------------------------------------------- /coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # generates lcov.info 4 | forge coverage --report lcov --no-match-test testFork 5 | 6 | if ! command -v lcov &>/dev/null; then 7 | echo "lcov is not installed. Installing..." 8 | # check if its macos or linux. 9 | if [ "$(uname)" == "Darwin" ]; then 10 | brew install lcov 11 | else 12 | sudo apt-get install lcov 13 | fi 14 | fi 15 | 16 | lcov --version 17 | 18 | # forge does not instrument libraries https://github.com/foundry-rs/foundry/issues/4854 19 | EXCLUDE="*test* *mock* *node_modules* $(grep -r 'library' contracts -l)" 20 | lcov --rc lcov_branch_coverage=1 \ 21 | --output-file forge-pruned-lcov.info \ 22 | --remove lcov.info $EXCLUDE 23 | 24 | if [ "$CI" != "true" ]; then 25 | genhtml --rc lcov_branch_coverage=1 \ 26 | --ignore-errors category \ 27 | --output-directory coverage forge-pruned-lcov.info \ 28 | && open coverage/index.html 29 | fi 30 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | solc = "0.8.24" 6 | optimizer_runs = 100_000 7 | remappings = [ 8 | "forge-std/=lib/forge-std/src/", 9 | "openzeppelin-contracts/=lib/openzeppelin-contracts/contracts", 10 | "evc/=lib/ethereum-vault-connector/src" 11 | ] 12 | 13 | [profile.default.fuzz] 14 | max_test_rejects = 1_000_000 15 | runs = 1_000 16 | seed = "0xee1d0f7d9556539a9c0e26aed5e63556" 17 | 18 | [profile.default.fmt] 19 | line_length = 120 20 | tab_width = 4 21 | bracket_spacing = false 22 | int_types = "long" 23 | multiline_func_header = "params_first" 24 | quote_style = "double" 25 | number_underscore = "preserve" 26 | override_spacing = true 27 | wrap_comments = true 28 | ignore = [ 29 | "src/interfaces/IRewardStreams.sol" 30 | ] -------------------------------------------------------------------------------- /medusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 12, 4 | "workerResetLimit": 50, 5 | "timeout": 0, 6 | "testLimit": 0, 7 | "callSequenceLength": 100, 8 | "corpusDirectory": "test/invariants/_corpus/medusa", 9 | "coverageEnabled": true, 10 | "deploymentOrder": [ 11 | "Tester" 12 | ], 13 | "targetContracts": [ 14 | "Tester" 15 | ], 16 | "targetContractsBalances": ["0xffffffffffffffffffffffffffffff"], 17 | "constructorArgs": {}, 18 | "deployerAddress": "0x30000", 19 | "senderAddresses": [ 20 | "0x10000", 21 | "0x20000", 22 | "0x30000" 23 | ], 24 | "blockNumberDelayMax": 60480, 25 | "blockTimestampDelayMax": 604800, 26 | "blockGasLimit": 12500000000, 27 | "transactionGasLimit": 1250000000, 28 | "testing": { 29 | "stopOnFailedTest": true, 30 | "stopOnFailedContractMatching": false, 31 | "stopOnNoTests": true, 32 | "testAllContracts": false, 33 | "traceAll": false, 34 | "assertionTesting": { 35 | "enabled": true, 36 | "testViewMethods": true, 37 | "panicCodeConfig": { 38 | "failOnCompilerInsertedPanic": false, 39 | "failOnAssertion": true, 40 | "failOnArithmeticUnderflow": false, 41 | "failOnDivideByZero": false, 42 | "failOnEnumTypeConversionOutOfBounds": false, 43 | "failOnIncorrectStorageAccess": false, 44 | "failOnPopEmptyArray": false, 45 | "failOnOutOfBoundsArrayAccess": false, 46 | "failOnAllocateTooMuchMemory": false, 47 | "failOnCallUninitializedVariable": false 48 | } 49 | }, 50 | "propertyTesting": { 51 | "enabled": true, 52 | "testPrefixes": [ 53 | "fuzz_", 54 | "echidna_" 55 | ] 56 | }, 57 | "optimizationTesting": { 58 | "enabled": false, 59 | "testPrefixes": [ 60 | "optimize_" 61 | ] 62 | } 63 | }, 64 | "chainConfig": { 65 | "codeSizeCheckDisabled": true, 66 | "cheatCodes": { 67 | "cheatCodesEnabled": true, 68 | "enableFFI": false 69 | } 70 | } 71 | }, 72 | "compilation": { 73 | "platform": "crytic-compile", 74 | "platformConfig": { 75 | "target": "test/invariants/Tester.t.sol", 76 | "solcVersion": "", 77 | "exportDirectory": "", 78 | "args": [ 79 | "--solc-remaps", 80 | "@crytic/properties/=lib/properties/ forge-std/=lib/forge-std/src/ ds-test/=lib/forge-std/lib/ds-test/src/ evc/=lib/ethereum-vault-connector/src/ solmate/=lib/solmate/src/ openzeppelin/=lib/openzeppelin-contracts/contracts/", 81 | "--compile-libraries=(Pretty,0xf01),(Strings,0xf02)" 82 | ] 83 | } 84 | }, 85 | "logging": { 86 | "level": "info", 87 | "logDirectory": "" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "euler-rewards", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "Euler Labs", 6 | "license": "MIT", 7 | "scripts": {}, 8 | "devDependencies": {} 9 | } 10 | -------------------------------------------------------------------------------- /script/placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/euler-xyz/reward-streams/a63c358265bc8184a6d893e510a2c8566351d2e6/script/placeholder -------------------------------------------------------------------------------- /src/StakingRewardStreams.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import {SafeERC20, IERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import {Set, SetStorage} from "evc/Set.sol"; 7 | import {BaseRewardStreams} from "./BaseRewardStreams.sol"; 8 | import {IStakingRewardStreams} from "./interfaces/IRewardStreams.sol"; 9 | 10 | /// @title StakingRewardStreams 11 | /// @custom:security-contact security@euler.xyz 12 | /// @author Euler Labs (https://www.eulerlabs.com/) 13 | /// @notice This contract inherits from `BaseRewardStreams` and implements `IStakingRewardStreams`. 14 | /// It allows for the rewards to be distributed to the rewarded token holders who have staked it. 15 | contract StakingRewardStreams is BaseRewardStreams, IStakingRewardStreams { 16 | using SafeERC20 for IERC20; 17 | using Set for SetStorage; 18 | 19 | /// @notice Constructor for the StakingRewardStreams contract. 20 | /// @param evc The Ethereum Vault Connector contract. 21 | /// @param periodDuration The duration of a period. 22 | constructor(address evc, uint48 periodDuration) BaseRewardStreams(evc, periodDuration) {} 23 | 24 | /// @notice Allows a user to stake rewarded tokens. 25 | /// @dev If the amount is max, the entire balance of the user is staked. 26 | /// @param rewarded The address of the rewarded token. 27 | /// @param amount The amount of tokens to stake. 28 | function stake(address rewarded, uint256 amount) external virtual override nonReentrant { 29 | address msgSender = _msgSender(); 30 | 31 | if (amount == type(uint256).max) { 32 | amount = IERC20(rewarded).balanceOf(msgSender); 33 | } 34 | 35 | if (amount == 0) { 36 | revert InvalidAmount(); 37 | } 38 | 39 | AccountStorage storage accountStorage = accounts[msgSender][rewarded]; 40 | uint256 currentAccountBalance = accountStorage.balance; 41 | address[] memory rewards = accountStorage.enabledRewards.get(); 42 | 43 | for (uint256 i = 0; i < rewards.length; ++i) { 44 | address reward = rewards[i]; 45 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 46 | 47 | // We always allocate rewards before updating any balances. 48 | updateRewardInternal( 49 | distributionStorage, accountStorage.earned[reward], rewarded, reward, currentAccountBalance, false 50 | ); 51 | 52 | distributionStorage.totalEligible += amount; 53 | } 54 | 55 | uint256 newAccountBalance = currentAccountBalance + amount; 56 | accountStorage.balance = newAccountBalance; 57 | 58 | emit BalanceUpdated(msgSender, rewarded, currentAccountBalance, newAccountBalance); 59 | 60 | pullToken(IERC20(rewarded), msgSender, amount); 61 | } 62 | 63 | /// @notice Allows a user to unstake rewarded tokens. 64 | /// @dev This function reverts if the recipient is zero address or is a known non-owner EVC account. 65 | /// @dev If the amount is max, the entire balance of the user is unstaked. 66 | /// @param rewarded The address of the rewarded token. 67 | /// @param recipient The address to receive the unstaked tokens. 68 | /// @param amount The amount of tokens to unstake. 69 | /// @param forfeitRecentReward Whether to forfeit the recent reward and not update the accumulator. 70 | function unstake( 71 | address rewarded, 72 | uint256 amount, 73 | address recipient, 74 | bool forfeitRecentReward 75 | ) external virtual override nonReentrant { 76 | address msgSender = _msgSender(); 77 | AccountStorage storage accountStorage = accounts[msgSender][rewarded]; 78 | uint256 currentAccountBalance = accountStorage.balance; 79 | 80 | if (amount == type(uint256).max) { 81 | amount = currentAccountBalance; 82 | } 83 | 84 | if (amount == 0 || amount > currentAccountBalance) { 85 | revert InvalidAmount(); 86 | } 87 | 88 | address[] memory rewards = accountStorage.enabledRewards.get(); 89 | 90 | for (uint256 i = 0; i < rewards.length; ++i) { 91 | address reward = rewards[i]; 92 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 93 | 94 | // We always allocate rewards before updating any balances. 95 | updateRewardInternal( 96 | distributionStorage, 97 | accountStorage.earned[reward], 98 | rewarded, 99 | reward, 100 | currentAccountBalance, 101 | forfeitRecentReward 102 | ); 103 | 104 | distributionStorage.totalEligible -= amount; 105 | } 106 | 107 | uint256 newAccountBalance = currentAccountBalance - amount; 108 | accountStorage.balance = newAccountBalance; 109 | 110 | emit BalanceUpdated(msgSender, rewarded, currentAccountBalance, newAccountBalance); 111 | 112 | pushToken(IERC20(rewarded), recipient, amount); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/TrackingRewardStreams.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import {Set, SetStorage} from "evc/Set.sol"; 6 | import {BaseRewardStreams} from "./BaseRewardStreams.sol"; 7 | import {ITrackingRewardStreams} from "./interfaces/IRewardStreams.sol"; 8 | 9 | /// @title TrackingRewardStreams 10 | /// @custom:security-contact security@euler.xyz 11 | /// @author Euler Labs (https://www.eulerlabs.com/) 12 | /// @notice This contract inherits from `BaseRewardStreams` and implements `ITrackingRewardStreams`. 13 | /// It allows for the rewards to be distributed to the rewarded token holders without requiring explicit staking. 14 | /// The rewarded token contract must implement `IBalanceTracker` and the `balanceTrackerHook` function. 15 | /// `balanceTrackerHook` must be called with: 16 | /// - the account's new balance when account's balance changes, 17 | /// - the current account's balance when the balance forwarding is enabled, 18 | /// - the account's balance of 0 when the balance forwarding is disabled. 19 | contract TrackingRewardStreams is BaseRewardStreams, ITrackingRewardStreams { 20 | using Set for SetStorage; 21 | 22 | /// @notice Constructor for the TrackingRewardStreams contract. 23 | /// @param evc The Ethereum Vault Connector contract. 24 | /// @param epochDuration The duration of an epoch. 25 | constructor(address evc, uint48 epochDuration) BaseRewardStreams(evc, epochDuration) {} 26 | 27 | /// @notice Executes the balance tracking hook for an account 28 | /// @param account The account address to execute the hook for 29 | /// @param newAccountBalance The new balance of the account 30 | /// @param forfeitRecentReward Whether to forfeit the most recent reward and not update the accumulator. Ignored 31 | /// when the new balance is greater than the current balance. 32 | function balanceTrackerHook( 33 | address account, 34 | uint256 newAccountBalance, 35 | bool forfeitRecentReward 36 | ) external override { 37 | address rewarded = msg.sender; 38 | AccountStorage storage accountStorage = accounts[account][rewarded]; 39 | uint256 currentAccountBalance = accountStorage.balance; 40 | address[] memory rewards = accountStorage.enabledRewards.get(); 41 | 42 | if (newAccountBalance > currentAccountBalance) forfeitRecentReward = false; 43 | 44 | for (uint256 i = 0; i < rewards.length; ++i) { 45 | address reward = rewards[i]; 46 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 47 | 48 | // We always allocate rewards before updating any balances. 49 | updateRewardInternal( 50 | distributionStorage, 51 | accountStorage.earned[reward], 52 | rewarded, 53 | reward, 54 | currentAccountBalance, 55 | forfeitRecentReward 56 | ); 57 | 58 | distributionStorage.totalEligible = 59 | distributionStorage.totalEligible + newAccountBalance - currentAccountBalance; 60 | } 61 | 62 | accountStorage.balance = newAccountBalance; 63 | 64 | emit BalanceUpdated(account, rewarded, currentAccountBalance, newAccountBalance); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/interfaces/IBalanceTracker.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity >=0.8.0; 4 | 5 | /// @title IBalanceTracker 6 | /// @custom:security-contact security@euler.xyz 7 | /// @author Euler Labs (https://www.eulerlabs.com/) 8 | /// @notice Provides an interface for tracking the balance of accounts. 9 | interface IBalanceTracker { 10 | /// @notice Executes the balance tracking hook for an account. 11 | /// @dev This function must be called with the current balance of the account when enabling the balance forwarding 12 | /// for it. This function must be called with 0 balance of the account when disabling the balance forwarding for it. 13 | /// This function allows to be called on zero balance transfers, when the newAccountBalance is the same as the 14 | /// previous one. To prevent DOS attacks, forfeitRecentReward should be used appropriately. 15 | /// @param account The account address to execute the hook for. 16 | /// @param newAccountBalance The new balance of the account. 17 | /// @param forfeitRecentReward Whether to forfeit the most recent reward and not update the accumulator. 18 | function balanceTrackerHook(address account, uint256 newAccountBalance, bool forfeitRecentReward) external; 19 | } 20 | -------------------------------------------------------------------------------- /src/interfaces/IRewardStreams.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | pragma solidity >=0.8.0; 4 | 5 | import "./IBalanceTracker.sol"; 6 | 7 | /// @title IRewardStreams 8 | /// @custom:security-contact security@euler.xyz 9 | /// @author Euler Labs (https://www.eulerlabs.com/) 10 | /// @notice Interface for Reward Streams distributor contract. 11 | interface IRewardStreams { 12 | function EPOCH_DURATION() external view returns (uint256); 13 | function MAX_EPOCHS_AHEAD() external view returns (uint256); 14 | function MAX_DISTRIBUTION_LENGTH() external view returns (uint256); 15 | function MAX_REWARDS_ENABLED() external view returns (uint256); 16 | function registerReward(address rewarded, address reward, uint48 startEpoch, uint128[] calldata rewardAmounts) external; 17 | function updateReward(address rewarded, address reward, address recipient) external returns (uint256); 18 | function claimReward(address rewarded, address reward, address recipient, bool ignoreRecentReward) external returns (uint256); 19 | function enableReward(address rewarded, address reward) external returns (bool); 20 | function disableReward(address rewarded, address reward, bool forfeitRecentReward) external returns (bool); 21 | function earnedReward(address account, address rewarded, address reward, bool ignoreRecentReward) external view returns (uint256); 22 | function enabledRewards(address account, address rewarded) external view returns (address[] memory); 23 | function isRewardEnabled(address account, address rewarded, address reward) external view returns (bool); 24 | function balanceOf(address account, address rewarded) external view returns (uint256); 25 | function rewardAmount(address rewarded, address reward) external view returns (uint256); 26 | function totalRewardedEligible(address rewarded, address reward) external view returns (uint256); 27 | function totalRewardRegistered(address rewarded, address reward) external view returns (uint256); 28 | function totalRewardClaimed(address rewarded, address reward) external view returns (uint256); 29 | function rewardAmount(address rewarded, address reward, uint48 epoch) external view returns (uint256); 30 | function currentEpoch() external view returns (uint48); 31 | function getEpoch(uint48 timestamp) external view returns (uint48); 32 | function getEpochStartTimestamp(uint48 epoch) external view returns (uint48); 33 | function getEpochEndTimestamp(uint48 epoch) external view returns (uint48); 34 | } 35 | 36 | /// @title ITrackingRewardStreams 37 | /// @author Euler Labs (https://www.eulerlabs.com/) 38 | /// @notice Interface for Tracking Reward Streams. Extends `IRewardStreams` and `IBalanceTracker`. 39 | interface ITrackingRewardStreams is IRewardStreams, IBalanceTracker {} 40 | 41 | /// @title IStakingRewardStreams 42 | /// @author Euler Labs (https://www.eulerlabs.com/) 43 | /// @notice Interface for Staking Reward Streams. Extends `IRewardStreams` with staking functionality. 44 | interface IStakingRewardStreams is IRewardStreams { 45 | function stake(address rewarded, uint256 amount) external; 46 | function unstake(address rewarded, uint256 amount, address recipient, bool forfeitRecentReward) external; 47 | } 48 | -------------------------------------------------------------------------------- /test/harness/BaseRewardStreamsHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity 0.8.24; 4 | 5 | import "../../src/BaseRewardStreams.sol"; 6 | 7 | contract BaseRewardStreamsHarness is BaseRewardStreams { 8 | using SafeERC20 for IERC20; 9 | using Set for SetStorage; 10 | 11 | constructor(address evc, uint48 epochDuration) BaseRewardStreams(evc, epochDuration) {} 12 | 13 | function setDistributionAmount(address rewarded, address reward, uint48 epoch, uint128 amount) external { 14 | distributions[rewarded][reward].amounts[epoch / EPOCHS_PER_SLOT][epoch % EPOCHS_PER_SLOT] = amount; 15 | } 16 | 17 | function getDistributionData( 18 | address rewarded, 19 | address reward 20 | ) 21 | external 22 | view 23 | returns ( 24 | uint48, /* lastUpdated */ 25 | uint208, /* accumulator */ 26 | uint256, /* totalEligible */ 27 | uint128, /* totalRegistered */ 28 | uint128 /* totalClaimed */ 29 | ) 30 | { 31 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 32 | return ( 33 | distributionStorage.lastUpdated, 34 | distributionStorage.accumulator, 35 | distributionStorage.totalEligible, 36 | distributionStorage.totalRegistered, 37 | distributionStorage.totalClaimed 38 | ); 39 | } 40 | 41 | function setDistributionData( 42 | address rewarded, 43 | address reward, 44 | uint48 lastUpdated, 45 | uint160 accumulator, 46 | uint256 totalEligible, 47 | uint128 totalRegistered, 48 | uint128 totalClaimed 49 | ) external { 50 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 51 | distributionStorage.lastUpdated = lastUpdated; 52 | distributionStorage.accumulator = accumulator; 53 | distributionStorage.totalEligible = totalEligible; 54 | distributionStorage.totalRegistered = totalRegistered; 55 | distributionStorage.totalClaimed = totalClaimed; 56 | } 57 | 58 | function getDistributionTotals( 59 | address rewarded, 60 | address reward 61 | ) external view returns (uint256, uint128, uint128) { 62 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 63 | return 64 | (distributionStorage.totalEligible, distributionStorage.totalRegistered, distributionStorage.totalClaimed); 65 | } 66 | 67 | function getEpochData(address rewarded, address reward, uint48 epoch) external view returns (uint256) { 68 | mapping(uint256 => uint128[EPOCHS_PER_SLOT]) storage storageAmounts = distributions[rewarded][reward].amounts; 69 | return storageAmounts[epoch / EPOCHS_PER_SLOT][epoch % EPOCHS_PER_SLOT]; 70 | } 71 | 72 | function setDistributionTotals( 73 | address rewarded, 74 | address reward, 75 | uint256 totalEligible, 76 | uint128 totalRegistered, 77 | uint128 totalClaimed 78 | ) external { 79 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 80 | distributionStorage.totalEligible = totalEligible; 81 | distributionStorage.totalRegistered = totalRegistered; 82 | distributionStorage.totalClaimed = totalClaimed; 83 | } 84 | 85 | function getAccountBalance(address account, address rewarded) external view returns (uint256) { 86 | return accounts[account][rewarded].balance; 87 | } 88 | 89 | function setAccountBalance(address account, address rewarded, uint256 balance) external { 90 | accounts[account][rewarded].balance = balance; 91 | } 92 | 93 | function insertReward(address account, address rewarded, address reward) external { 94 | accounts[account][rewarded].enabledRewards.insert(reward); 95 | } 96 | 97 | function getAccountEarnedData( 98 | address account, 99 | address rewarded, 100 | address reward 101 | ) external view returns (EarnStorage memory) { 102 | return accounts[account][rewarded].earned[reward]; 103 | } 104 | 105 | function setAccountEarnedData( 106 | address account, 107 | address rewarded, 108 | address reward, 109 | EarnStorage memory earnStorage 110 | ) external { 111 | accounts[account][rewarded].earned[reward] = earnStorage; 112 | } 113 | 114 | function getTimeElapsedInEpoch(uint48 epoch, uint48 lastUpdated) external view returns (uint256) { 115 | return timeElapsedInEpoch(epoch, lastUpdated); 116 | } 117 | 118 | function getUpdatedAccumulator(address rewarded, address reward) external view returns (uint160) { 119 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 120 | EarnStorage storage earnStorage = accounts[msg.sender][rewarded].earned[reward]; 121 | (, uint160 accumulator,,) = calculateRewards(distributionStorage, earnStorage, 0, false); 122 | return accumulator; 123 | } 124 | 125 | function timeElapsedInEpochPublic(uint48 epoch, uint48 lastUpdated) external view returns (uint256) { 126 | return timeElapsedInEpoch(epoch, lastUpdated); 127 | } 128 | 129 | function msgSender() external view returns (address) { 130 | return _msgSender(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /test/harness/StakingRewardStreamsHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity 0.8.24; 4 | 5 | import "../../src/StakingRewardStreams.sol"; 6 | 7 | contract StakingRewardStreamsHarness is StakingRewardStreams { 8 | using SafeERC20 for IERC20; 9 | using Set for SetStorage; 10 | 11 | constructor(address evc, uint48 epochDuration) StakingRewardStreams(evc, epochDuration) {} 12 | 13 | function setDistributionAmount(address rewarded, address reward, uint48 epoch, uint128 amount) external { 14 | distributions[rewarded][reward].amounts[epoch / EPOCHS_PER_SLOT][epoch % EPOCHS_PER_SLOT] = amount; 15 | } 16 | 17 | function getDistributionData( 18 | address rewarded, 19 | address reward 20 | ) 21 | external 22 | view 23 | returns ( 24 | uint48, /* lastUpdated */ 25 | uint208, /* accumulator */ 26 | uint256, /* totalEligible */ 27 | uint128, /* totalRegistered */ 28 | uint128 /* totalClaimed */ 29 | ) 30 | { 31 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 32 | return ( 33 | distributionStorage.lastUpdated, 34 | distributionStorage.accumulator, 35 | distributionStorage.totalEligible, 36 | distributionStorage.totalRegistered, 37 | distributionStorage.totalClaimed 38 | ); 39 | } 40 | 41 | function setDistributionData( 42 | address rewarded, 43 | address reward, 44 | uint48 lastUpdated, 45 | uint160 accumulator, 46 | uint256 totalEligible, 47 | uint128 totalRegistered, 48 | uint128 totalClaimed 49 | ) external { 50 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 51 | distributionStorage.lastUpdated = lastUpdated; 52 | distributionStorage.accumulator = accumulator; 53 | distributionStorage.totalEligible = totalEligible; 54 | distributionStorage.totalRegistered = totalRegistered; 55 | distributionStorage.totalClaimed = totalClaimed; 56 | } 57 | 58 | function getEpochData(address rewarded, address reward, uint48 epoch) external view returns (uint256) { 59 | mapping(uint256 => uint128[EPOCHS_PER_SLOT]) storage storageAmounts = distributions[rewarded][reward].amounts; 60 | return storageAmounts[epoch / EPOCHS_PER_SLOT][epoch % EPOCHS_PER_SLOT]; 61 | } 62 | 63 | function getDistributionTotals( 64 | address rewarded, 65 | address reward 66 | ) external view returns (uint256, uint128, uint128) { 67 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 68 | return 69 | (distributionStorage.totalEligible, distributionStorage.totalRegistered, distributionStorage.totalClaimed); 70 | } 71 | 72 | function setDistributionTotals( 73 | address rewarded, 74 | address reward, 75 | uint256 totalEligible, 76 | uint128 totalRegistered, 77 | uint128 totalClaimed 78 | ) external { 79 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 80 | distributionStorage.totalEligible = totalEligible; 81 | distributionStorage.totalRegistered = totalRegistered; 82 | distributionStorage.totalClaimed = totalClaimed; 83 | } 84 | 85 | function getAccountBalance(address account, address rewarded) external view returns (uint256) { 86 | return accounts[account][rewarded].balance; 87 | } 88 | 89 | function setAccountBalance(address account, address rewarded, uint256 balance) external { 90 | accounts[account][rewarded].balance = balance; 91 | } 92 | 93 | function insertReward(address account, address rewarded, address reward) external { 94 | accounts[account][rewarded].enabledRewards.insert(reward); 95 | } 96 | 97 | function getAccountEarnedData( 98 | address account, 99 | address rewarded, 100 | address reward 101 | ) external view returns (EarnStorage memory) { 102 | return accounts[account][rewarded].earned[reward]; 103 | } 104 | 105 | function setAccountEarnedData( 106 | address account, 107 | address rewarded, 108 | address reward, 109 | EarnStorage memory earnStorage 110 | ) external { 111 | accounts[account][rewarded].earned[reward] = earnStorage; 112 | } 113 | 114 | function getUpdatedAccumulator(address rewarded, address reward) external view returns (uint208) { 115 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 116 | EarnStorage storage earnStorage = accounts[msg.sender][rewarded].earned[reward]; 117 | (, uint208 accumulator,,) = calculateRewards(distributionStorage, earnStorage, 0, false); 118 | return accumulator; 119 | } 120 | 121 | function timeElapsedInEpochPublic(uint48 epoch, uint48 lastUpdated) external view returns (uint256) { 122 | return timeElapsedInEpoch(epoch, lastUpdated); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /test/harness/TrackingRewardStreamsHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity 0.8.24; 4 | 5 | import {SafeERC20, IERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import "../../src/TrackingRewardStreams.sol"; 7 | 8 | contract TrackingRewardStreamsHarness is TrackingRewardStreams { 9 | using SafeERC20 for IERC20; 10 | using Set for SetStorage; 11 | 12 | constructor(address evc, uint48 epochDuration) TrackingRewardStreams(evc, epochDuration) {} 13 | 14 | function setDistributionAmount(address rewarded, address reward, uint48 epoch, uint128 amount) external { 15 | distributions[rewarded][reward].amounts[epoch / EPOCHS_PER_SLOT][epoch % EPOCHS_PER_SLOT] = amount; 16 | } 17 | 18 | function getDistributionData( 19 | address rewarded, 20 | address reward 21 | ) 22 | external 23 | view 24 | returns ( 25 | uint48, /* lastUpdated */ 26 | uint208, /* accumulator */ 27 | uint256, /* totalEligible */ 28 | uint128, /* totalRegistered */ 29 | uint128 /* totalClaimed */ 30 | ) 31 | { 32 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 33 | return ( 34 | distributionStorage.lastUpdated, 35 | distributionStorage.accumulator, 36 | distributionStorage.totalEligible, 37 | distributionStorage.totalRegistered, 38 | distributionStorage.totalClaimed 39 | ); 40 | } 41 | 42 | function setDistributionData( 43 | address rewarded, 44 | address reward, 45 | uint48 lastUpdated, 46 | uint160 accumulator, 47 | uint256 totalEligible, 48 | uint128 totalRegistered, 49 | uint128 totalClaimed 50 | ) external { 51 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 52 | distributionStorage.lastUpdated = lastUpdated; 53 | distributionStorage.accumulator = accumulator; 54 | distributionStorage.totalEligible = totalEligible; 55 | distributionStorage.totalRegistered = totalRegistered; 56 | distributionStorage.totalClaimed = totalClaimed; 57 | } 58 | 59 | function getEpochData(address rewarded, address reward, uint48 epoch) external view returns (uint256) { 60 | mapping(uint256 => uint128[EPOCHS_PER_SLOT]) storage storageAmounts = distributions[rewarded][reward].amounts; 61 | return storageAmounts[epoch / EPOCHS_PER_SLOT][epoch % EPOCHS_PER_SLOT]; 62 | } 63 | 64 | function getDistributionTotals( 65 | address rewarded, 66 | address reward 67 | ) external view returns (uint256, uint128, uint128) { 68 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 69 | return 70 | (distributionStorage.totalEligible, distributionStorage.totalRegistered, distributionStorage.totalClaimed); 71 | } 72 | 73 | function setDistributionTotals( 74 | address rewarded, 75 | address reward, 76 | uint256 totalEligible, 77 | uint128 totalRegistered, 78 | uint128 totalClaimed 79 | ) external { 80 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 81 | distributionStorage.totalEligible = totalEligible; 82 | distributionStorage.totalRegistered = totalRegistered; 83 | distributionStorage.totalClaimed = totalClaimed; 84 | } 85 | 86 | function getAccountBalance(address account, address rewarded) external view returns (uint256) { 87 | return accounts[account][rewarded].balance; 88 | } 89 | 90 | function setAccountBalance(address account, address rewarded, uint256 balance) external { 91 | accounts[account][rewarded].balance = balance; 92 | } 93 | 94 | function insertReward(address account, address rewarded, address reward) external { 95 | accounts[account][rewarded].enabledRewards.insert(reward); 96 | } 97 | 98 | function getAccountEarnedData( 99 | address account, 100 | address rewarded, 101 | address reward 102 | ) external view returns (EarnStorage memory) { 103 | return accounts[account][rewarded].earned[reward]; 104 | } 105 | 106 | function setAccountEarnedData( 107 | address account, 108 | address rewarded, 109 | address reward, 110 | EarnStorage memory earnStorage 111 | ) external { 112 | accounts[account][rewarded].earned[reward] = earnStorage; 113 | } 114 | 115 | function getUpdatedAccumulator(address rewarded, address reward) external view returns (uint208) { 116 | DistributionStorage storage distributionStorage = distributions[rewarded][reward]; 117 | EarnStorage storage earnStorage = accounts[msg.sender][rewarded].earned[reward]; 118 | (, uint208 accumulator,,) = calculateRewards(distributionStorage, earnStorage, 0, false); 119 | return accumulator; 120 | } 121 | 122 | function timeElapsedInEpochPublic(uint48 epoch, uint48 lastUpdated) external view returns (uint256) { 123 | return timeElapsedInEpoch(epoch, lastUpdated); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /test/invariants/CryticToFoundry.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Libraries 5 | import "forge-std/Test.sol"; 6 | import "forge-std/console.sol"; 7 | 8 | // Test Contracts 9 | import {Invariants} from "./Invariants.t.sol"; 10 | import {Setup} from "./Setup.t.sol"; 11 | 12 | /// @title CryticToFoundry 13 | /// @notice Foundry wrapper for fuzzer failed call sequences 14 | /// @dev Regression testing for failed call sequences 15 | contract CryticToFoundry is Invariants, Setup { 16 | modifier setup() override { 17 | _; 18 | } 19 | 20 | /// @dev Foundry compatibility faster setup debugging 21 | function setUp() public { 22 | // Deploy protocol contracts and protocol actors 23 | _setUp(); 24 | 25 | // Deploy actors 26 | _setUpActors(); 27 | 28 | // Initialize handler contracts 29 | _setUpHandlers(); 30 | 31 | actor = actors[USER1]; 32 | } 33 | 34 | /////////////////////////////////////////////////////////////////////////////////////////////// 35 | // INVARIANTS REPLAY // 36 | /////////////////////////////////////////////////////////////////////////////////////////////// 37 | 38 | function test_enableReward() public { 39 | // 1 40 | this.enableReward(0); 41 | // 2 42 | uint128[] memory rewards = new uint128[](1); 43 | rewards[0] = 1; 44 | this.registerReward(0, 0, rewards); 45 | // 3 46 | _delay(1); 47 | this.enableReward(0); 48 | } 49 | 50 | function test_claimSpilloverReward1() public { 51 | uint128[] memory rewards = new uint128[](1); 52 | rewards[0] = 1; 53 | this.registerReward(0, 0, rewards); 54 | this.enableReward(0); 55 | _delay(1); 56 | this.claimSpilloverReward(0, 0); 57 | } 58 | 59 | function test_claimRewards1() public { 60 | uint128[] memory rewards = new uint128[](1); 61 | rewards[0] = 1; 62 | this.enableReward(0); 63 | this.registerReward(0, 0, rewards); 64 | _delay(1); 65 | this.claimReward(0, 0, true); 66 | } 67 | 68 | function test_unstake1() public { 69 | this.stake(115792089237316195423570985008687907853269984665640564039457584007913129639935); 70 | this.enableReward(0); 71 | uint128[] memory rewards = new uint128[](1); 72 | rewards[0] = 77139; 73 | this.registerReward(0, 0, rewards); 74 | _delay(1); 75 | this.unstake(0, 1, true); 76 | } 77 | 78 | function test_disableReward1() public { 79 | uint128[] memory rewards = new uint128[](1); 80 | rewards[0] = 228074428775375066; 81 | this.registerReward(0, 0, rewards); 82 | this.enableReward(0); 83 | _delay(1); 84 | this.disableReward(0, true); 85 | } 86 | 87 | function test_STAKING_INVARIANT_A1() public { 88 | this.stake(115792089237316195423570985008687907853269984665640564039457584007913129639935); 89 | echidna_STAKING_INVARIANTS(); 90 | } 91 | 92 | function test_BASE_INVARIANTS() public { 93 | echidna_BASE_INVARIANTS(); 94 | } 95 | 96 | function test_DISTRIBUTION_INVARIANTS1() public { 97 | this.stake(115792089237316195423570985008687907853269984665640564039457584007913129639935); 98 | this.enableReward(0); 99 | echidna_DISTRIBUTION_INVARIANTS(); 100 | } 101 | 102 | function test_DISTRIBUTION_INVARIANTS2() public { 103 | this.stake(20232243); 104 | echidna_DISTRIBUTION_INVARIANTS(); 105 | } 106 | 107 | function test_DISTRIBUTION_INVARIANTS3() public { 108 | vm.warp(187403); 109 | uint128[] memory rewards = new uint128[](1); 110 | rewards[0] = 66; 111 | this.registerReward(0, 0, rewards); 112 | _delay(8 days); 113 | this.enableReward(0); 114 | this.claimSpilloverReward(0, 0); 115 | echidna_DISTRIBUTION_INVARIANTS(); 116 | } 117 | 118 | function test_DISTRIBUTION_INVARIANTS4() public { 119 | uint128[] memory rewards = new uint128[](1); 120 | rewards[0] = 1390; 121 | this.registerReward(0, 0, rewards); 122 | _delay(128780); 123 | this.enableReward(0); 124 | _delay(240075); 125 | this.stake(2623374470054327805470411115388864044445339010392982102470475852441352); 126 | _delay(490635); 127 | this.unstake(0, 0, false); 128 | _delay(1292434); 129 | rewards[0] = 1; 130 | this.registerReward(0, 0, rewards); 131 | echidna_DISTRIBUTION_INVARIANTS(); 132 | } 133 | 134 | function test_DISTRIBUTION_INVARIANTS5() public { 135 | this.stake(1); 136 | _delay(130498); 137 | this.enableReward(0); 138 | uint128[] memory rewards = new uint128[](1); 139 | rewards[0] = 2; 140 | this.registerReward(0, 0, rewards); 141 | _delay(163951 + 169761 + 1006652); 142 | echidna_DISTRIBUTION_INVARIANTS(); 143 | this.claimReward(0, 0, false); 144 | echidna_DISTRIBUTION_INVARIANTS(); 145 | } 146 | 147 | function test_UPDATE_REWARDS_INVARIANTS1() public { 148 | this.stake(1); 149 | _delay(127715); 150 | this.enableReward(0); 151 | uint128[] memory rewards = new uint128[](1); 152 | rewards[0] = 2; 153 | this.registerReward(0, 0, rewards); 154 | _delay(163951 + 168054 + 1916652); 155 | echidna_UPDATE_REWARDS_INVARIANT(); 156 | this.claimReward(0, 0, false); 157 | echidna_UPDATE_REWARDS_INVARIANT(); 158 | } 159 | 160 | function test_UPDATE_REWARDS_INVARIANTS2() public { 161 | console.log( 162 | "zero address rewards: ", _getZeroAddressRewards(stakingRewarded, reward, address(stakingDistributor)) 163 | ); 164 | console.log( 165 | "global accumulator: ", _getDistributionAccumulator(stakingRewarded, reward, address(stakingDistributor)) 166 | ); 167 | this.stake(1); 168 | uint128[] memory rewards = new uint128[](1); 169 | rewards[0] = 2; 170 | console.log( 171 | "zero address rewards: ", _getZeroAddressRewards(stakingRewarded, reward, address(stakingDistributor)) 172 | ); 173 | console.log( 174 | "global accumulator: ", _getDistributionAccumulator(stakingRewarded, reward, address(stakingDistributor)) 175 | ); 176 | 177 | this.registerReward(0, 0, rewards); 178 | console.log( 179 | "zero address rewards: ", _getZeroAddressRewards(stakingRewarded, reward, address(stakingDistributor)) 180 | ); 181 | console.log( 182 | "global accumulator: ", _getDistributionAccumulator(stakingRewarded, reward, address(stakingDistributor)) 183 | ); 184 | 185 | _delay(290782 + 1180496 + 137559); 186 | console.log( 187 | "zero address rewards: ", _getZeroAddressRewards(stakingRewarded, reward, address(stakingDistributor)) 188 | ); 189 | console.log( 190 | "global accumulator: ", _getDistributionAccumulator(stakingRewarded, reward, address(stakingDistributor)) 191 | ); 192 | 193 | console.log("-----------------------------------"); 194 | 195 | this.enableReward(0); 196 | console.log( 197 | "zero address rewards: ", _getZeroAddressRewards(stakingRewarded, reward, address(stakingDistributor)) 198 | ); 199 | console.log( 200 | "global accumulator: ", _getDistributionAccumulator(stakingRewarded, reward, address(stakingDistributor)) 201 | ); 202 | 203 | this.claimSpilloverReward(0, 0); 204 | console.log( 205 | "zero address rewards: ", _getZeroAddressRewards(stakingRewarded, reward, address(stakingDistributor)) 206 | ); 207 | console.log( 208 | "global accumulator: ", _getDistributionAccumulator(stakingRewarded, reward, address(stakingDistributor)) 209 | ); 210 | 211 | echidna_UPDATE_REWARDS_INVARIANT(); 212 | } 213 | 214 | function test_UPDATE_REWARDS_INVARIANTS3() public { 215 | _delay(32737 + 279023 + 319648 + 53742); 216 | this.enableReward(0); 217 | _delay(177942 + 1239800); 218 | uint128[] memory rewards = new uint128[](1); 219 | rewards[0] = 10339251737; 220 | this.registerReward(1, 0, rewards); 221 | _delay(506572); 222 | this.enableReward(1); 223 | this.stake(1); 224 | echidna_UPDATE_REWARDS_INVARIANT(); 225 | console.log("zero address rewards: ", stakingDistributor.totalRewardedEligible(stakingRewarded, reward)); 226 | console.log("e2e rewarded: ", stakingRewarded); 227 | console.log("e2e reward: ", reward); 228 | this.claimSpilloverReward(0, 1); 229 | console.log("zero address rewards: ", stakingDistributor.totalRewardedEligible(stakingRewarded, reward)); 230 | echidna_UPDATE_REWARDS_INVARIANT(); 231 | } 232 | 233 | function test_claimSpilloverReward() public { 234 | uint128[] memory rewards = new uint128[](1); 235 | rewards[0] = 2; 236 | this.registerReward(0, 0, rewards); 237 | _delay(4026900 + 204167); 238 | this.claimSpilloverReward(0, 0); 239 | } 240 | 241 | function test_DISTRIBUTION_INVARIANTS8() public { 242 | uint128[] memory rewards = new uint128[](1); 243 | rewards[0] = 152; 244 | this.registerReward(0, 0, rewards); 245 | _delay(1800477); 246 | this.claimSpilloverReward(0, 0); 247 | echidna_DISTRIBUTION_INVARIANTS(); 248 | } 249 | 250 | /////////////////////////////////////////////////////////////////////////////////////////////// 251 | // HELPERS // 252 | /////////////////////////////////////////////////////////////////////////////////////////////// 253 | 254 | function getBytecode(address _contractAddress) internal view returns (bytes memory) { 255 | uint256 size; 256 | assembly { 257 | size := extcodesize(_contractAddress) 258 | } 259 | bytes memory bytecode = new bytes(size); 260 | assembly { 261 | extcodecopy(_contractAddress, add(bytecode, 0x20), 0, size) 262 | } 263 | return bytecode; 264 | } 265 | 266 | function _setUpBlockAndActor(uint256 _block, address _user) internal { 267 | vm.roll(_block); 268 | actor = actors[_user]; 269 | } 270 | 271 | function _delay(uint256 _seconds) internal { 272 | vm.warp(block.timestamp + _seconds); 273 | } 274 | 275 | function _setUpActor(address _origin) internal { 276 | actor = actors[_origin]; 277 | } 278 | 279 | function _setUpActorAndDelay(address _origin, uint256 _seconds) internal { 280 | actor = actors[_origin]; 281 | vm.warp(block.timestamp + _seconds); 282 | } 283 | 284 | function _setUpTimestampAndActor(uint256 _timestamp, address _user) internal { 285 | vm.warp(_timestamp); 286 | actor = actors[_user]; 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /test/invariants/HandlerAggregator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Handler Contracts 5 | import {BaseRewardsHandler} from "./handlers/BaseRewardsHandler.t.sol"; 6 | import {StakingRewardStreamsHandler} from "./handlers/StakingRewardStreamsHandler.t.sol"; 7 | import {EVCHandler} from "./handlers/external/EVCHandler.t.sol"; 8 | 9 | // Simulators 10 | import {DonationAttackHandler} from "./handlers/simulators/DonationAttackHandler.t.sol"; 11 | import {ERC20BalanceForwarderHandler} from "./handlers/simulators/ERC20BalanceForwarderHandler.t.sol"; 12 | import {ControllerHandler} from "./handlers/simulators/ControllerHandler.t.sol"; 13 | 14 | /// @notice Helper contract to aggregate all handler contracts, inherited in BaseInvariants 15 | abstract contract HandlerAggregator is 16 | BaseRewardsHandler, // Module handlers 17 | StakingRewardStreamsHandler, 18 | EVCHandler, // EVC handler 19 | DonationAttackHandler, // Simulator handlers 20 | ERC20BalanceForwarderHandler, 21 | ControllerHandler 22 | { 23 | /// @notice Helper function in case any handler requires additional setup 24 | function _setUpHandlers() internal {} 25 | } 26 | -------------------------------------------------------------------------------- /test/invariants/Invariants.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Invariant Contracts 5 | import {BaseInvariants, BaseRewardStreamsHarness} from "./invariants/BaseInvariants.t.sol"; 6 | import {StakingInvariants} from "./invariants/StakingInvariants.t.sol"; 7 | 8 | /// @title Invariants 9 | /// @notice Wrappers for the protocol invariants implemented in each invariants contract 10 | /// @dev recognised by Echidna when property mode is activated 11 | /// @dev Inherits BaseInvariants that inherits HandlerAggregator 12 | abstract contract Invariants is BaseInvariants, StakingInvariants { 13 | /////////////////////////////////////////////////////////////////////////////////////////////// 14 | // BASE INVARIANTS // 15 | /////////////////////////////////////////////////////////////////////////////////////////////// 16 | 17 | function echidna_BASE_INVARIANTS() public returns (bool) { 18 | for (uint256 i; i < distributionSetups.length; ++i) { 19 | (address rewarded, address _target) = _getSetupData(i); 20 | assert_BASE_INVARIANT_B(rewarded, reward, _target); 21 | assert_BASE_INVARIANT_E(rewarded, reward, _target); 22 | } 23 | return true; 24 | } 25 | 26 | /////////////////////////////////////////////////////////////////////////////////////////////// 27 | // UPDATE REWARDS INVARIANTS // 28 | /////////////////////////////////////////////////////////////////////////////////////////////// 29 | 30 | function echidna_UPDATE_REWARDS_INVARIANT() public returns (bool) { 31 | for (uint256 i; i < distributionSetups.length; ++i) { 32 | (address rewarded, address _target) = _getSetupData(i); 33 | assert_UPDATE_REWARDS_INVARIANT_B(rewarded, reward, _target); 34 | for (uint256 j; j < actorAddresses.length; ++j) { 35 | assert_UPDATE_REWARDS_INVARIANT_C(rewarded, reward, _target, actorAddresses[j]); 36 | } 37 | assert_UPDATE_REWARDS_INVARIANT_D(rewarded, reward, _target); 38 | } 39 | return true; 40 | } 41 | 42 | /////////////////////////////////////////////////////////////////////////////////////////////// 43 | // DISTRIBUTION INVARIANTS // 44 | /////////////////////////////////////////////////////////////////////////////////////////////// 45 | 46 | function echidna_DISTRIBUTION_INVARIANTS() public returns (bool) { 47 | for (uint256 i; i < distributionSetups.length; ++i) { 48 | (address rewarded, address _target) = _getSetupData(i); 49 | assert_DISTRIBUTION_INVARIANT_C(rewarded, reward, _target); 50 | assert_DISTRIBUTION_INVARIANT_D(rewarded, reward, _target); 51 | assert_DISTRIBUTION_INVARIANT_E(rewarded, reward, _target); 52 | //assert_DISTRIBUTION_INVARIANT_F(rewarded, reward, _target); 53 | 54 | uint256 sumRewardedBalances; 55 | for (uint256 j; j < actorAddresses.length; ++j) { 56 | if (BaseRewardStreamsHarness(_target).isRewardEnabled(actorAddresses[j], rewarded, reward)) { 57 | sumRewardedBalances += BaseRewardStreamsHarness(_target).balanceOf(actorAddresses[j], rewarded); 58 | } 59 | } 60 | assert_DISTRIBUTION_INVARIANT_I(rewarded, reward, _target, sumRewardedBalances); 61 | } 62 | return true; 63 | } 64 | 65 | /////////////////////////////////////////////////////////////////////////////////////////////// 66 | // STAKING INVARIANTS // 67 | /////////////////////////////////////////////////////////////////////////////////////////////// 68 | 69 | function echidna_STAKING_INVARIANTS() public returns (bool) { 70 | for (uint256 i; i < actorAddresses.length; ++i) { 71 | assert_STAKING_INVARIANT_A(actorAddresses[i]); 72 | } 73 | return true; 74 | } 75 | 76 | /////////////////////////////////////////////////////////////////////////////////////////////// 77 | // VIEW INVARIANTS // 78 | /////////////////////////////////////////////////////////////////////////////////////////////// 79 | } 80 | -------------------------------------------------------------------------------- /test/invariants/InvariantsSpec.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | /// @title InvariantsSpec 5 | /// @notice Invariants specification for the protocol 6 | /// @dev Contains pseudo code and description for the invariants in the protocol 7 | abstract contract InvariantsSpec { 8 | /*///////////////////////////////////////////////////////////////////////////////////////////// 9 | // PROPERTY TYPES // 10 | /////////////////////////////////////////////////////////////////////////////////////////////// 11 | 12 | /// @dev On this invariant testing framework there exists two types of Properties: 13 | 14 | - INVARIANTS (INV): 15 | - These are properties that should always hold true in the system. 16 | - They are implemented under /invariants folder. 17 | 18 | - POSTCONDITIONS: 19 | - These are properties that should hold true after an action is executed. 20 | - They are implemented under /hooks and /handlers. 21 | 22 | - There exists two types of POSTCONDITIONS: 23 | - GLOBAL POSTCONDITIONS (GPOST): 24 | - These are properties that should always hold true after an action is executed. 25 | - They are checked in `_checkPostConditions` function in the HookAggregator contract. 26 | 27 | - HANDLER SPECIFIC POSTCONDITIONS (HSPOST): 28 | // - These are properties that should hold true after an specific action is executed in a specific context. 29 | - They are implemented on each handler function under HANDLER SPECIFIC POSTCONDITIONS comment. 30 | 31 | The following list of system prooperties have a comment indicating the type of property they are therefore making it 32 | easier to identify their implementations in the system (INV, GPOST, HSPOST): 33 | 34 | /////////////////////////////////////////////////////////////////////////////////////////////*/ 35 | 36 | /////////////////////////////////////////////////////////////////////////////////////////////// 37 | // BASE // 38 | /////////////////////////////////////////////////////////////////////////////////////////////// 39 | 40 | // HSPOST 41 | string constant BASE_INVARIANT_A = 42 | "BASE_INVARIANT_A: forfeitRecentRewards == true => rewards should be always cheaply enabled / disabled"; 43 | // INV 44 | string constant BASE_INVARIANT_B = 45 | "BASE_INVARIANT_B: in case nobody is earning rewards, at least someone can claim them"; 46 | // HSPOST 47 | string constant BASE_INVARIANT_C = 48 | "BASE_INVARIANT_C: distribution stream should be at most MAX_DISTRIBUTION_LENGTH epochs long"; 49 | // HSPOST 50 | string constant BASE_INVARIANT_D = 51 | "BASE_INVARIANT_D: startEpoch should be at most MAX_EPOCHS_AHEAD epochs in the future"; 52 | // INV 53 | string constant BASE_INVARIANT_E = 54 | "BASE_INVARIANT_E: totalRegistered of a distribution should always be greater than totalClaimed"; 55 | // HSPOST 56 | string constant BASE_INVARIANT_F = "BASE_INVARIANT_F: startEpoch should always be 0 or greater than currentEpoch"; 57 | /////////////////////////////////////////////////////////////////////////////////////////////// 58 | // UPDATE REWARDS // 59 | /////////////////////////////////////////////////////////////////////////////////////////////// 60 | 61 | // HSPOST 62 | string constant UPDATE_REWARDS_INVARIANT_A = 63 | "UPDATE_REWARDS_INVARIANT_A: after any interaction requiring reward updates updateReward should be called"; 64 | // INV 65 | string constant UPDATE_REWARDS_INVARIANT_B = "UPDATE_REWARDS_INVARIANT_B: current epoch >= lastUpdated epoch"; 66 | // INV 67 | string constant UPDATE_REWARDS_INVARIANT_C = "UPDATE_REWARDS_INVARIANT_C: global accumulator >= user accumulator"; 68 | // INV 69 | string constant UPDATE_REWARDS_INVARIANT_D = 70 | "UPDATE_REWARDS_INVARIANT_D: zeroAddress account accumulator should always be 0"; 71 | 72 | /////////////////////////////////////////////////////////////////////////////////////////////// 73 | // DISTRIBUTION // 74 | /////////////////////////////////////////////////////////////////////////////////////////////// 75 | 76 | // GPOST 77 | string constant DISTRIBUTION_INVARIANT_A = 78 | "DISTRIBUTION_INVARIANT_A: lastUpdated of a distribution increases monotonically"; 79 | // GPOST 80 | string constant DISTRIBUTION_INVARIANT_B = 81 | "DISTRIBUTION_INVARIANT_B: accumulator of a distribution increases monotonically"; 82 | //INV 83 | string constant DISTRIBUTION_INVARIANT_C = 84 | "DISTRIBUTION_INVARIANT_C: reward token amount on the contract should be greater or equal than totalRegisteres minus totalClaimed"; 85 | //INV 86 | string constant DISTRIBUTION_INVARIANT_D = 87 | "DISTRIBUTION_INVARIANT_D: totalClaimed of a distribution should equal the amount transferred out"; 88 | //INV 89 | string constant DISTRIBUTION_INVARIANT_E = 90 | "DISTRIBUTION_INVARIANT_E: the number of epochs of a distribution should be between bounds"; 91 | // HSPOST 92 | string constant DISTRIBUTION_INVARIANT_G = 93 | "DISTRIBUTION_INVARIANT_G: after registerReward is called storageAmounts are updated correctly"; 94 | // HSPOST 95 | string constant DISTRIBUTION_INVARIANT_H = 96 | "DISTRIBUTION_INVARIANT_H: after registerReward is called the correct amount of tokens is transferred in"; 97 | // INV 98 | string constant DISTRIBUTION_INVARIANT_I = 99 | "DISTRIBUTION_INVARIANT_I: totalEligible should equal the sum of rewarded balance of all the active accounts"; 100 | // HSPOST 101 | string constant DISTRIBUTION_INVARIANT_J = 102 | "DISTRIBUTION_INVARIANT_J: after calling claimSpilloverReward spill over claimable rewards are set to 0"; 103 | 104 | /////////////////////////////////////////////////////////////////////////////////////////////// 105 | // ACCOUNT STORAGE // 106 | /////////////////////////////////////////////////////////////////////////////////////////////// 107 | 108 | // GPOST 109 | string constant ACCOUNT_STORAGE_INVARIANT_A = 110 | "ACCOUNT_STORAGE_INVARIANT_A: earn storage accumulator should increase monotonically"; 111 | 112 | /////////////////////////////////////////////////////////////////////////////////////////////// 113 | // STAKING // 114 | /////////////////////////////////////////////////////////////////////////////////////////////// 115 | 116 | // INV 117 | string constant STAKING_INVARIANT_A = "STAKING_INVARIANT_A: user balance must always equal the sum of user deposits"; 118 | 119 | /////////////////////////////////////////////////////////////////////////////////////////////// 120 | // TRACKING // 121 | /////////////////////////////////////////////////////////////////////////////////////////////// 122 | 123 | // HSPOST 124 | string constant TRACKING_INVARIANT_A = "TRACKING_INVARIANT_A: balanceTrackerHook can never revert"; 125 | 126 | string constant TRACKING_INVARIANT_B = 127 | "TRACKING_INVARIANT_B: forfeitRecentRewards is enough to prevent DOS on liquidation flow"; 128 | 129 | string constant TRACKING_INVARIANT_C = 130 | "TRACKING_INVARIANT_C: calling balanceTrackerHook multiple times with the same balance does not affect the distributor"; 131 | 132 | /////////////////////////////////////////////////////////////////////////////////////////////// 133 | // VIEW // 134 | /////////////////////////////////////////////////////////////////////////////////////////////// 135 | 136 | // INV 137 | string constant VIEW_INVARIANT_A = "VIEW_INVARIANT_A: timeElapsedInEpoch cannot revert"; 138 | } 139 | -------------------------------------------------------------------------------- /test/invariants/Setup.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Contracts 5 | import {EthereumVaultConnector} from "evc/EthereumVaultConnector.sol"; 6 | 7 | // Test Contracts 8 | import {StakingRewardStreamsHarness} from "test/harness/StakingRewardStreamsHarness.sol"; 9 | import {TrackingRewardStreamsHarness} from "test/harness/TrackingRewardStreamsHarness.sol"; 10 | import {BaseTest} from "./base/BaseTest.t.sol"; 11 | 12 | // Mock Contracts 13 | import {MockController} from "test/utils/MockController.sol"; 14 | import {MockERC20, MockERC20BalanceForwarder} from "test/utils/MockERC20.sol"; 15 | 16 | // Utils 17 | import {Actor} from "./utils/Actor.sol"; 18 | 19 | /// @title Setup 20 | /// @notice Setup contract for the invariant test Suite, inherited by Tester 21 | contract Setup is BaseTest { 22 | function _setUp() internal { 23 | // Deplopy EVC and needed contracts 24 | _deployProtocolCore(); 25 | } 26 | 27 | function _deployProtocolCore() internal { 28 | // Deploy the EVC 29 | evc = new EthereumVaultConnector(); 30 | 31 | // Deploy the Distributors 32 | stakingDistributor = new StakingRewardStreamsHarness(address(evc), 10 days); 33 | trackingDistributor = new TrackingRewardStreamsHarness(address(evc), 10 days); 34 | 35 | // Deploy assets 36 | stakingRewarded = address(new MockERC20("Staking Rewarded", "SRWDD")); 37 | trackingRewarded = 38 | address(new MockERC20BalanceForwarder(evc, trackingDistributor, "Tracking Rewarded", "SFRWDD")); 39 | reward = address(new MockERC20("Reward", "RWD")); 40 | 41 | assetAddresses.push(stakingRewarded); 42 | assetAddresses.push(trackingRewarded); 43 | assetAddresses.push(reward); 44 | 45 | // Store extra setup data 46 | Setup memory _setup1 = Setup({rewarded: stakingRewarded, distributor: address(stakingDistributor)}); 47 | distributionSetups.push(_setup1); 48 | 49 | Setup memory _setup2 = Setup({rewarded: trackingRewarded, distributor: address(trackingDistributor)}); 50 | distributionSetups.push(_setup2); 51 | 52 | // Deploy the controller 53 | controller = new MockController(evc); 54 | } 55 | 56 | function _setUpActors() internal { 57 | address[] memory addresses = new address[](3); 58 | addresses[0] = USER1; 59 | addresses[1] = USER2; 60 | addresses[2] = USER3; 61 | 62 | address[] memory tokens = new address[](3); 63 | tokens[0] = stakingRewarded; 64 | tokens[1] = trackingRewarded; 65 | tokens[2] = reward; 66 | 67 | address[] memory callers = new address[](2); 68 | callers[0] = address(stakingDistributor); 69 | callers[1] = address(trackingDistributor); 70 | 71 | for (uint256 i; i < NUMBER_OF_ACTORS; i++) { 72 | // Deply actor proxies and approve system contracts 73 | address _actor = _setUpActor(addresses[i], tokens, callers); 74 | 75 | // Mint initial balances to actors 76 | for (uint256 j = 0; j < tokens.length; j++) { 77 | MockERC20 _token = MockERC20(tokens[j]); 78 | _token.mint(_actor, INITIAL_BALANCE); 79 | } 80 | actorAddresses.push(_actor); 81 | } 82 | } 83 | 84 | function _setUpActor( 85 | address userAddress, 86 | address[] memory tokens, 87 | address[] memory callers 88 | ) internal returns (address actorAddress) { 89 | bool success; 90 | Actor _actor = new Actor(tokens, callers); 91 | actors[userAddress] = _actor; 92 | (success,) = address(_actor).call{value: INITIAL_ETH_BALANCE}(""); 93 | assert(success); 94 | actorAddress = address(_actor); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/invariants/Tester.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Test Contracts 5 | import {Invariants} from "./Invariants.t.sol"; 6 | import {Setup} from "./Setup.t.sol"; 7 | 8 | /// @title Tester 9 | /// @notice Entry point for invariant testing, inherits all contracts, invariants & handler 10 | /// @dev Mono contract that contains all the testing logic 11 | contract Tester is Invariants, Setup { 12 | constructor() payable { 13 | setUp(); 14 | } 15 | 16 | /// @dev Foundry compatibility faster setup debugging 17 | function setUp() internal { 18 | // Deploy protocol contracts and protocol actors 19 | _setUp(); 20 | 21 | // Deploy actors 22 | _setUpActors(); 23 | 24 | // Initialize handler contracts 25 | _setUpHandlers(); 26 | } 27 | 28 | /// @dev Needed in order for foundry to recognise the contract as a test, faster debugging 29 | //function testAux() public view {} 30 | } 31 | -------------------------------------------------------------------------------- /test/invariants/_config/echidna_config.yaml: -------------------------------------------------------------------------------- 1 | #codeSize max code size for deployed contratcs (default 24576, per EIP-170) 2 | codeSize: 224576 3 | 4 | #whether ot not to use the multi-abi mode of testing 5 | #it’s not working for us, see: https://github.com/crytic/echidna/issues/547 6 | #multi-abi: true 7 | 8 | #balanceAddr is default balance for addresses 9 | balanceAddr: 0xffffffffffffffffffffffff 10 | #balanceContract overrides balanceAddr for the contract address (2^128 = ~3e38) 11 | balanceContract: 0xffffffffffffffffffffffffffffffffffffffffffffffff 12 | 13 | #testLimit is the number of test sequences to run 14 | testLimit: 20000000 15 | 16 | #seqLen defines how many transactions are in a test sequence 17 | seqLen: 300 18 | 19 | #shrinkLimit determines how much effort is spent shrinking failing sequences 20 | shrinkLimit: 2500 21 | 22 | #propMaxGas defines gas cost at which a property fails 23 | propMaxGas: 1000000000 24 | 25 | #testMaxGas is a gas limit; does not cause failure, but terminates sequence 26 | testMaxGas: 1000000000 27 | 28 | # list of methods to filter 29 | #filterFunctions: ["openCdpExt"] 30 | # by default, blacklist methods in filterFunctions 31 | #filterBlacklist: false 32 | 33 | #stopOnFail makes echidna terminate as soon as any property fails and has been shrunk 34 | stopOnFail: false 35 | 36 | #coverage controls coverage guided testing 37 | coverage: true 38 | 39 | # list of file formats to save coverage reports in; default is all possible formats 40 | coverageFormats: ["lcov", "html"] 41 | 42 | #directory to save the corpus; by default is disabled 43 | corpusDir: "test/invariants/_corpus/echidna/default/_data/corpus" 44 | # constants for corpus mutations (for experimentation only) 45 | #mutConsts: [100, 1, 1] 46 | 47 | #remappings 48 | cryticArgs: ["--solc-remaps", "@crytic/properties/=lib/properties/ forge-std/=lib/forge-std/src/ ds-test/=lib/forge-std/lib/ds-test/src/ ethereum-vault-connector/=lib/ethereum-vault-connector/src openzeppelin/=lib/openzeppelin-contracts/contracts/", "--compile-libraries=(Pretty,0xf01),(Strings,0xf02)"] 49 | 50 | deployContracts: [["0xf01", "Pretty"], ["0xf02", "Strings"]] 51 | 52 | # maximum value to send to payable functions 53 | maxValue: 100000000000000000000000 # 100000 eth 54 | 55 | #quiet produces (much) less verbose output 56 | quiet: false 57 | 58 | # concurrent workers 59 | workers: 10 60 | -------------------------------------------------------------------------------- /test/invariants/base/BaseHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Libraries 5 | import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 6 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import {MockERC20} from "test/utils/MockERC20.sol"; 8 | 9 | // Contracts 10 | import {Actor} from "../utils/Actor.sol"; 11 | import {HookAggregator} from "../hooks/HookAggregator.t.sol"; 12 | 13 | /// @title BaseHandler 14 | /// @notice Contains common logic for all handlers 15 | /// @dev inherits all suite assertions since per-action assertions are implemented in the handlers 16 | contract BaseHandler is HookAggregator { 17 | using EnumerableSet for EnumerableSet.AddressSet; 18 | 19 | /////////////////////////////////////////////////////////////////////////////////////////////// 20 | // SHARED VARAIBLES // 21 | /////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | /////////////////////////////////////////////////////////////////////////////////////////////// 24 | // HELPERS // 25 | /////////////////////////////////////////////////////////////////////////////////////////////// 26 | 27 | function _sumRewardAmounts(uint128[] calldata rewardAmounts) internal pure returns (uint256 sum) { 28 | for (uint256 i = 0; i < rewardAmounts.length; i++) { 29 | sum += rewardAmounts[i]; 30 | } 31 | } 32 | 33 | /// @notice Helper function to randomize a uint256 seed with a string salt 34 | function _randomize(uint256 seed, string memory salt) internal pure returns (uint256) { 35 | return uint256(keccak256(abi.encodePacked(seed, salt))); 36 | } 37 | 38 | function _getRandomValue(uint256 modulus) internal view returns (uint256) { 39 | uint256 randomNumber = uint256(keccak256(abi.encode(block.timestamp, block.prevrandao, msg.sender))); 40 | return randomNumber % modulus; // Adjust the modulus to the desired range 41 | } 42 | 43 | /// @notice Helper function to approve an amount of tokens to a spender, a proxy Actor 44 | function _approve(address token, Actor actor_, address spender, uint256 amount) internal { 45 | bool success; 46 | bytes memory returnData; 47 | (success, returnData) = actor_.proxy(token, abi.encodeWithSelector(0x095ea7b3, spender, amount)); 48 | require(success, string(returnData)); 49 | } 50 | 51 | /// @notice Helper function to safely approve an amount of tokens to a spender 52 | function _approve(address token, address owner, address spender, uint256 amount) internal { 53 | vm.prank(owner); 54 | _safeApprove(token, spender, 0); 55 | vm.prank(owner); 56 | _safeApprove(token, spender, amount); 57 | } 58 | 59 | function _safeApprove(address token, address spender, uint256 amount) internal { 60 | (bool success, bytes memory retdata) = 61 | token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, amount)); 62 | assert(success); 63 | if (retdata.length > 0) assert(abi.decode(retdata, (bool))); 64 | } 65 | 66 | function _mint(address token, address receiver, uint256 amount) internal { 67 | MockERC20(token).mint(receiver, amount); 68 | } 69 | 70 | function _mintAndApprove(address token, address owner, address spender, uint256 amount) internal { 71 | _mint(token, owner, amount); 72 | _approve(token, owner, spender, amount); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/invariants/base/BaseHooks.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Contracts 5 | import {ProtocolAssertions} from "./ProtocolAssertions.t.sol"; 6 | 7 | // Test Contracts 8 | import {InvariantsSpec} from "../InvariantsSpec.t.sol"; 9 | 10 | /// @title BaseHooks 11 | /// @notice Contains common logic for all handlers 12 | /// @dev inherits all suite assertions since per-action assertions are implemented in the handlers 13 | /// @dev inherits the Invariant Specifications contract 14 | contract BaseHooks is ProtocolAssertions, InvariantsSpec { 15 | /////////////////////////////////////////////////////////////////////////////////////////////// 16 | // HELPERS // 17 | /////////////////////////////////////////////////////////////////////////////////////////////// 18 | } 19 | -------------------------------------------------------------------------------- /test/invariants/base/BaseStorage.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Contracts 5 | import {EthereumVaultConnector} from "evc/EthereumVaultConnector.sol"; 6 | 7 | // Test Contracts 8 | import {StakingRewardStreamsHarness} from "test/harness/StakingRewardStreamsHarness.sol"; 9 | import {TrackingRewardStreamsHarness} from "test/harness/TrackingRewardStreamsHarness.sol"; 10 | 11 | // Mock Contracts 12 | import {MockController} from "test/utils/MockController.sol"; 13 | 14 | // Utils 15 | import {Actor} from "../utils/Actor.sol"; 16 | 17 | // Interfaces 18 | import {BaseRewardStreamsHarness} from "test/harness/BaseRewardStreamsHarness.sol"; 19 | 20 | /// @notice BaseStorage contract for all test contracts, works in tandem with BaseTest 21 | abstract contract BaseStorage { 22 | /////////////////////////////////////////////////////////////////////////////////////////////// 23 | // CONSTANTS // 24 | /////////////////////////////////////////////////////////////////////////////////////////////// 25 | 26 | uint256 constant MAX_TOKEN_AMOUNT = 1e29; 27 | 28 | uint256 constant ONE_DAY = 1 days; 29 | uint256 constant ONE_MONTH = ONE_YEAR / 12; 30 | uint256 constant ONE_YEAR = 365 days; 31 | 32 | uint256 internal constant NUMBER_OF_ACTORS = 3; 33 | uint256 internal constant INITIAL_ETH_BALANCE = 1e26; 34 | uint256 internal constant INITIAL_COLL_BALANCE = 1e21; 35 | 36 | uint256 constant VIRTUAL_DEPOSIT_AMOUNT = 1e6; 37 | 38 | /////////////////////////////////////////////////////////////////////////////////////////////// 39 | // ACTORS // 40 | /////////////////////////////////////////////////////////////////////////////////////////////// 41 | 42 | /// @notice Stores the actor during a handler call 43 | Actor internal actor; 44 | 45 | /// @notice Mapping of fuzzer user addresses to actors 46 | mapping(address => Actor) internal actors; 47 | 48 | /// @notice Array of all actor addresses 49 | address[] internal actorAddresses; 50 | 51 | /////////////////////////////////////////////////////////////////////////////////////////////// 52 | // SUITE STORAGE // 53 | /////////////////////////////////////////////////////////////////////////////////////////////// 54 | 55 | // STAKING CONTRACTS 56 | 57 | /// @notice Testing targets 58 | StakingRewardStreamsHarness internal stakingDistributor; 59 | TrackingRewardStreamsHarness internal trackingDistributor; 60 | 61 | BaseRewardStreamsHarness internal target; 62 | 63 | /// @notice EVC contract 64 | EthereumVaultConnector internal evc; 65 | 66 | // ASSETS 67 | 68 | /// @notice mock assets 69 | address internal stakingRewarded; 70 | address internal trackingRewarded; 71 | address internal reward; 72 | 73 | /// @notice mock controller 74 | MockController internal controller; 75 | 76 | /// @notice Rewards seeder 77 | address seeder; 78 | 79 | /////////////////////////////////////////////////////////////////////////////////////////////// 80 | // EXTRA VARIABLES // 81 | /////////////////////////////////////////////////////////////////////////////////////////////// 82 | 83 | address[] internal assetAddresses; 84 | 85 | struct Setup { 86 | address rewarded; 87 | address distributor; 88 | } 89 | 90 | Setup[] internal distributionSetups; 91 | } 92 | -------------------------------------------------------------------------------- /test/invariants/base/BaseTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Libraries 5 | import {Vm} from "forge-std/Base.sol"; 6 | import {StdUtils} from "forge-std/StdUtils.sol"; 7 | 8 | // Utils 9 | import {Actor} from "../utils/Actor.sol"; 10 | import {PropertiesConstants} from "../utils/PropertiesConstants.sol"; 11 | import {StdAsserts} from "../utils/StdAsserts.sol"; 12 | 13 | // Base 14 | import {BaseStorage, BaseRewardStreamsHarness} from "./BaseStorage.t.sol"; 15 | 16 | // Interfaces 17 | import {IRewardStreams} from "src/interfaces/IRewardStreams.sol"; 18 | 19 | /// @notice Base contract for all test contracts extends BaseStorage 20 | /// @dev Provides setup modifier and cheat code setup 21 | /// @dev inherits Storage, Testing constants assertions and utils needed for testing 22 | abstract contract BaseTest is BaseStorage, PropertiesConstants, StdAsserts, StdUtils { 23 | bool public IS_TEST = true; 24 | 25 | /////////////////////////////////////////////////////////////////////////////////////////////// 26 | // ACTOR PROXY MECHANISM // 27 | /////////////////////////////////////////////////////////////////////////////////////////////// 28 | 29 | /// @dev Actor proxy mechanism 30 | modifier setup() virtual { 31 | actor = actors[msg.sender]; 32 | _; 33 | actor = Actor(payable(address(0))); 34 | } 35 | 36 | /////////////////////////////////////////////////////////////////////////////////////////////// 37 | // CHEAT CODE SETUP // 38 | /////////////////////////////////////////////////////////////////////////////////////////////// 39 | 40 | /// @dev Cheat code address, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D. 41 | address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); 42 | 43 | Vm internal constant vm = Vm(VM_ADDRESS); 44 | 45 | /////////////////////////////////////////////////////////////////////////////////////////////// 46 | // HELPERS // 47 | /////////////////////////////////////////////////////////////////////////////////////////////// 48 | 49 | function _getZeroAddressRewards( 50 | address _rewarded, 51 | address _reward, 52 | address _target 53 | ) internal view returns (uint256) { 54 | BaseRewardStreamsHarness.EarnStorage memory earnStorage = 55 | BaseRewardStreamsHarness(_target).getAccountEarnedData(address(0), _rewarded, _reward); 56 | return earnStorage.claimable; 57 | } 58 | 59 | function _getDistributionAccumulator( 60 | address _rewarded, 61 | address _reward, 62 | address _target 63 | ) internal view returns (uint256) { 64 | return BaseRewardStreamsHarness(_target).getUpdatedAccumulator(_rewarded, _reward); 65 | } 66 | 67 | /// @notice Helper function that returns either the staking or tracking reward setup and sets the target 68 | function _getRandomRewards(uint256 _i) internal returns (address rewarded, address distributor) { 69 | if (_i % 2 == 0) { 70 | rewarded = stakingRewarded; 71 | distributor = address(stakingDistributor); 72 | } else { 73 | rewarded = trackingRewarded; 74 | distributor = address(trackingDistributor); 75 | } 76 | _setTarget(distributor); 77 | } 78 | 79 | function _distributionActive(address rewarded, address _rewards, address _target) internal view returns (bool) { 80 | (uint48 lastUpdated,,,,) = BaseRewardStreamsHarness(_target).getDistributionData(rewarded, _rewards); 81 | return lastUpdated > 0; 82 | } 83 | 84 | function _enabledRewards(address account, address rewarded, address _target) internal view returns (bool) { 85 | address[] memory enabledRewards = IRewardStreams(_target).enabledRewards(account, rewarded); 86 | return enabledRewards.length > 0; 87 | } 88 | 89 | function _getSetupData(uint256 _i) internal view returns (address, address) { 90 | Setup memory _setup = distributionSetups[_i]; 91 | return (_setup.rewarded, _setup.distributor); 92 | } 93 | 94 | function _getRandomAsset(uint256 _i) internal view returns (address) { 95 | uint256 _assetIndex = _i % assetAddresses.length; 96 | return assetAddresses[_assetIndex]; 97 | } 98 | 99 | function _getRandomActor(uint256 _i) internal view returns (address) { 100 | uint256 _actorIndex = _i % NUMBER_OF_ACTORS; 101 | return actorAddresses[_actorIndex]; 102 | } 103 | 104 | function _makeAddr(string memory name) internal pure returns (address addr) { 105 | uint256 privateKey = uint256(keccak256(abi.encodePacked(name))); 106 | addr = vm.addr(privateKey); 107 | } 108 | 109 | function _setTarget(address _target) internal { 110 | target = BaseRewardStreamsHarness(_target); 111 | } 112 | 113 | function _resetTarget() internal { 114 | target = BaseRewardStreamsHarness(address(0)); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /test/invariants/base/ProtocolAssertions.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Base 5 | import {BaseTest} from "./BaseTest.t.sol"; 6 | import {StdAsserts} from "test/invariants/utils/StdAsserts.sol"; 7 | 8 | /// @title ProtocolAssertions 9 | /// @notice Helper contract for protocol specific assertions 10 | abstract contract ProtocolAssertions is StdAsserts, BaseTest { 11 | //Protocol specific assertions 12 | } 13 | -------------------------------------------------------------------------------- /test/invariants/handlers/BaseRewardsHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Interfaces 5 | import {IERC20} from "forge-std/interfaces/IERC20.sol"; 6 | 7 | // Test Contracts 8 | import {Actor} from "../utils/Actor.sol"; 9 | import {BaseHandler} from "../base/BaseHandler.t.sol"; 10 | import {BaseRewardStreamsHarness} from "test/harness/BaseRewardStreamsHarness.sol"; 11 | 12 | // Interfaces 13 | import {IRewardStreams} from "src/interfaces/IRewardStreams.sol"; 14 | 15 | import "forge-std/console.sol"; 16 | 17 | /// @title BaseRewardsHandler 18 | /// @notice Handler test contract for the risk balance forwarder module actions 19 | contract BaseRewardsHandler is BaseHandler { 20 | /////////////////////////////////////////////////////////////////////////////////////////////// 21 | // STATE VARIABLES // 22 | /////////////////////////////////////////////////////////////////////////////////////////////// 23 | 24 | /////////////////////////////////////////////////////////////////////////////////////////////// 25 | // GHOST VARAIBLES // 26 | /////////////////////////////////////////////////////////////////////////////////////////////// 27 | 28 | mapping(address target => mapping(address rewarded => mapping(address reward => uint256))) public ghost_claims; 29 | 30 | mapping(address target => uint256) public ghost_addressZeroClaimedRewards; 31 | 32 | /////////////////////////////////////////////////////////////////////////////////////////////// 33 | // ACTIONS // 34 | /////////////////////////////////////////////////////////////////////////////////////////////// 35 | 36 | function registerReward(uint8 i, uint48 startEpoch, uint128[] calldata rewardAmounts) external setup { 37 | bool success; 38 | bytes memory returnData; 39 | 40 | // Get one of the two setups randomly 41 | (address _rewarded, address _target) = _getRandomRewards(i); 42 | 43 | uint256 rewardBalanceBefore = IERC20(reward).balanceOf(address(actor)); 44 | 45 | _before(address(actor), _rewarded, reward); 46 | 47 | (success, returnData) = actor.proxy( 48 | _target, 49 | abi.encodeWithSelector(IRewardStreams.registerReward.selector, _rewarded, reward, startEpoch, rewardAmounts) 50 | ); 51 | 52 | if (success) { 53 | _after(address(actor), _rewarded, reward); 54 | 55 | ////////////////// HANDLER SPECIFIC POSTCONDITIONS ////////////////// 56 | 57 | // BASE POSTCONDITIONS 58 | assertLe(rewardAmounts.length, MAX_DISTRIBUTION_LENGTH, BASE_INVARIANT_C); 59 | assertLe(startEpoch, target.currentEpoch() + MAX_EPOCHS_AHEAD, BASE_INVARIANT_D); 60 | 61 | if (startEpoch != 0) { 62 | assertGe(startEpoch, target.currentEpoch(), BASE_INVARIANT_F); 63 | } 64 | 65 | // DISTRIBUTION POSTCONDITIONS 66 | uint256 totalAmount = _sumRewardAmounts(rewardAmounts); 67 | assert_DISTRIBUTION_INVARIANT_G(totalAmount); 68 | assertEq( 69 | rewardBalanceBefore - IERC20(reward).balanceOf(address(actor)), totalAmount, DISTRIBUTION_INVARIANT_H 70 | ); 71 | 72 | // UPDATE REWARDS POSTCONDITIONS 73 | assert_UPDATE_REWARDS_INVARIANT_A(address(actor), _rewarded, reward); 74 | } 75 | } 76 | 77 | function updateReward(uint8 i) external setup { 78 | bool success; 79 | bytes memory returnData; 80 | 81 | // Get one of the two setups randomly 82 | (address _rewarded, address _target) = _getRandomRewards(i); 83 | 84 | _before(address(actor), _rewarded, reward); 85 | 86 | (success, returnData) = actor.proxy( 87 | _target, abi.encodeWithSelector(IRewardStreams.updateReward.selector, _rewarded, reward, address(0)) 88 | ); 89 | 90 | if (success) { 91 | _after(address(actor), _rewarded, reward); 92 | 93 | ////////////////// HANDLER SPECIFIC POSTCONDITIONS ////////////////// 94 | 95 | // UPDATE REWARDS POSTCONDITIONS 96 | assert_UPDATE_REWARDS_INVARIANT_A(address(actor), _rewarded, reward); 97 | } 98 | } 99 | 100 | function claimReward(uint8 i, uint8 j, bool forfeitRecentReward) external setup { 101 | bool success; 102 | bytes memory returnData; 103 | 104 | // Get one of the three actors randomly 105 | address recipient = _getRandomActor(i); 106 | 107 | // Get one of the two setups randomly 108 | (address _rewarded, address _target) = _getRandomRewards(j); 109 | 110 | uint256 earnedReward = target.earnedReward(address(actor), _rewarded, reward, forfeitRecentReward); 111 | 112 | _before(address(actor), _rewarded, reward); 113 | 114 | (success, returnData) = actor.proxy( 115 | _target, 116 | abi.encodeWithSelector( 117 | IRewardStreams.claimReward.selector, _rewarded, reward, recipient, forfeitRecentReward 118 | ) 119 | ); 120 | 121 | if (success) { 122 | _after(address(actor), _rewarded, reward); 123 | 124 | ghost_claims[_target][_rewarded][reward] += earnedReward; 125 | 126 | ////////////////// HANDLER SPECIFIC POSTCONDITIONS ////////////////// 127 | 128 | // UPDATE REWARDS POSTCONDITIONS 129 | if (!forfeitRecentReward) { 130 | assert_UPDATE_REWARDS_INVARIANT_A(address(actor), _rewarded, reward); 131 | } 132 | } 133 | } 134 | 135 | function claimSpilloverReward(uint8 i, uint8 j) external setup { 136 | bool success; 137 | bytes memory returnData; 138 | 139 | // Get one of the three actors randomly 140 | address recipient = _getRandomActor(i); 141 | 142 | // Get one of the two setups randomly 143 | (address _rewarded, address _target) = _getRandomRewards(j); 144 | 145 | uint256 spilloverReward = target.earnedReward(address(0), _rewarded, reward, false); 146 | 147 | _before(address(actor), _rewarded, reward); 148 | 149 | (success, returnData) = actor.proxy( 150 | _target, abi.encodeWithSelector(IRewardStreams.updateReward.selector, _rewarded, reward, recipient) 151 | ); 152 | 153 | if (success) { 154 | _after(address(actor), _rewarded, reward); 155 | 156 | ghost_claims[_target][_rewarded][reward] += spilloverReward; 157 | 158 | ghost_addressZeroClaimedRewards[_target] += spilloverReward; 159 | 160 | ////////////////// HANDLER SPECIFIC POSTCONDITIONS ////////////////// 161 | assert_DISTRIBUTION_INVARIANT_J(_rewarded, reward, _target); 162 | } 163 | } 164 | 165 | function enableReward(uint8 i) external setup { 166 | bool success; 167 | bytes memory returnData; 168 | 169 | // Get one of the two setups randomly 170 | (address _rewarded, address _target) = _getRandomRewards(i); 171 | 172 | _before(address(actor), _rewarded, reward); 173 | 174 | (success, returnData) = 175 | actor.proxy(_target, abi.encodeWithSelector(IRewardStreams.enableReward.selector, _rewarded, reward)); 176 | 177 | if (success) { 178 | _after(address(actor), _rewarded, reward); 179 | 180 | ////////////////// HANDLER SPECIFIC POSTCONDITIONS ////////////////// 181 | 182 | // UPDATE REWARDS POSTCONDITIONS 183 | if (!target.isRewardEnabled(address(actor), _rewarded, reward)) { 184 | assert_UPDATE_REWARDS_INVARIANT_A(address(actor), _rewarded, reward); 185 | } 186 | } 187 | } 188 | 189 | function disableReward(uint8 i, bool forfeitRecentReward) external setup { 190 | bool success; 191 | bytes memory returnData; 192 | 193 | // Get one of the two setups randomly 194 | (address _rewarded, address _target) = _getRandomRewards(i); 195 | 196 | bool enabledBefore = _enabledRewards(address(actor), _rewarded, address(target)); 197 | 198 | _before(address(actor), _rewarded, reward); 199 | 200 | (success, returnData) = actor.proxy( 201 | _target, 202 | abi.encodeWithSelector(IRewardStreams.disableReward.selector, _rewarded, reward, forfeitRecentReward) 203 | ); 204 | 205 | if (success) { 206 | _after(address(actor), _rewarded, reward); 207 | 208 | ////////////////// HANDLER SPECIFIC POSTCONDITIONS ////////////////// 209 | 210 | // UPDATE REWARDS POSTCONDITIONS 211 | if (!forfeitRecentReward && enabledBefore && _distributionActive(_rewarded, reward, address(target))) { 212 | assertEq(baseRewardsVars.lastUpdatedAfter, block.timestamp, UPDATE_REWARDS_INVARIANT_A); 213 | } 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /test/invariants/handlers/StakingRewardStreamsHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Test Contracts 5 | import {Actor, IERC20} from "../utils/Actor.sol"; 6 | import {BaseHandler} from "../base/BaseHandler.t.sol"; 7 | 8 | // Interfaces 9 | import {IStakingRewardStreams} from "src/interfaces/IRewardStreams.sol"; 10 | 11 | import "forge-std/console.sol"; 12 | 13 | /// @title StakingRewardStreamsHandler 14 | /// @notice Handler test contract for the BorrowingModule actions 15 | contract StakingRewardStreamsHandler is BaseHandler { 16 | /////////////////////////////////////////////////////////////////////////////////////////////// 17 | // STATE VARIABLES // 18 | /////////////////////////////////////////////////////////////////////////////////////////////// 19 | 20 | /////////////////////////////////////////////////////////////////////////////////////////////// 21 | // GHOST VARAIBLES // 22 | /////////////////////////////////////////////////////////////////////////////////////////////// 23 | 24 | mapping(address => mapping(address => uint256)) public ghost_deposits; 25 | 26 | /////////////////////////////////////////////////////////////////////////////////////////////// 27 | // ACTIONS // 28 | /////////////////////////////////////////////////////////////////////////////////////////////// 29 | 30 | function stake(uint256 amount) external setup { 31 | bool success; 32 | bytes memory returnData; 33 | 34 | address _target = address(stakingDistributor); 35 | 36 | _setTarget(_target); 37 | 38 | uint256 balanceOfActorBefgore = IERC20(stakingRewarded).balanceOf(address(actor)); 39 | 40 | _before(address(actor), stakingRewarded, reward); 41 | 42 | (success, returnData) = 43 | actor.proxy(_target, abi.encodeWithSelector(IStakingRewardStreams.stake.selector, stakingRewarded, amount)); 44 | 45 | if (success) { 46 | _after(address(actor), stakingRewarded, reward); 47 | 48 | if (amount == type(uint256).max) { 49 | amount = balanceOfActorBefgore; 50 | } 51 | 52 | ghost_deposits[address(actor)][stakingRewarded] += amount; 53 | 54 | ////////////////// HANDLER SPECIFIC POSTCONDITIONS ////////////////// 55 | 56 | // UPDATE REWARDS POSTCONDITIONS 57 | assert_UPDATE_REWARDS_INVARIANT_A(address(actor), address(stakingRewarded), reward); 58 | } 59 | } 60 | 61 | function unstake(uint8 i, uint256 amount, bool forfeitRecentReward) external setup { 62 | bool success; 63 | bytes memory returnData; 64 | 65 | // Get one of the three actors randomly 66 | address recipient = _getRandomActor(i); 67 | 68 | address _target = address(stakingDistributor); 69 | 70 | _setTarget(_target); 71 | 72 | _before(address(actor), address(stakingRewarded), reward); 73 | 74 | (success, returnData) = actor.proxy( 75 | _target, 76 | abi.encodeWithSelector( 77 | IStakingRewardStreams.unstake.selector, stakingRewarded, amount, recipient, forfeitRecentReward 78 | ) 79 | ); 80 | 81 | if (success) { 82 | _after(address(actor), address(stakingRewarded), reward); 83 | 84 | ghost_deposits[address(actor)][stakingRewarded] -= amount; 85 | 86 | ////////////////// HANDLER SPECIFIC POSTCONDITIONS ////////////////// 87 | 88 | // UPDATE REWARDS POSTCONDITIONS 89 | if (!forfeitRecentReward) { 90 | assert_UPDATE_REWARDS_INVARIANT_A(address(actor), address(stakingRewarded), reward); 91 | } 92 | } 93 | } 94 | 95 | /////////////////////////////////////////////////////////////////////////////////////////////// 96 | // OWNER ACTIONS // 97 | /////////////////////////////////////////////////////////////////////////////////////////////// 98 | 99 | /////////////////////////////////////////////////////////////////////////////////////////////// 100 | // HELPERS // 101 | /////////////////////////////////////////////////////////////////////////////////////////////// 102 | } 103 | -------------------------------------------------------------------------------- /test/invariants/handlers/TrackingRewardStreamsHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Test Contracts 5 | import {Actor} from "../utils/Actor.sol"; 6 | import {BaseHandler} from "../base/BaseHandler.t.sol"; 7 | 8 | import {MockERC20BalanceForwarder} from "test/invariants/Setup.t.sol"; 9 | 10 | /// @title TrackingRewardStreamsHandler 11 | /// @notice Handler test contract for ERC20 contacts 12 | contract TrackingRewardStreamsHandler is BaseHandler { 13 | /////////////////////////////////////////////////////////////////////////////////////////////// 14 | // STATE VARIABLES // 15 | /////////////////////////////////////////////////////////////////////////////////////////////// 16 | 17 | /////////////////////////////////////////////////////////////////////////////////////////////// 18 | // GHOST VARAIBLES // 19 | /////////////////////////////////////////////////////////////////////////////////////////////// 20 | 21 | /////////////////////////////////////////////////////////////////////////////////////////////// 22 | // ACTIONS // 23 | /////////////////////////////////////////////////////////////////////////////////////////////// 24 | 25 | function assert_TRACKING_INVARIANT_A(uint8 i, uint256 newAccountBalance, bool forfeitRecentReward) external { 26 | // Get one of the three actors randomly 27 | address account = _getRandomActor(i); 28 | 29 | try MockERC20BalanceForwarder(trackingRewarded).balanceTrackerHookSimulator( 30 | account, newAccountBalance, forfeitRecentReward 31 | ) {} catch { 32 | assertTrue(false, TRACKING_INVARIANT_A); 33 | } 34 | } 35 | 36 | /////////////////////////////////////////////////////////////////////////////////////////////// 37 | // HELPERS // 38 | /////////////////////////////////////////////////////////////////////////////////////////////// 39 | } 40 | -------------------------------------------------------------------------------- /test/invariants/handlers/external/EVCHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Libraries 5 | import {EthereumVaultConnector} from "evc/EthereumVaultConnector.sol"; 6 | 7 | // Testing contracts 8 | import {Actor} from "../../utils/Actor.sol"; 9 | import {BaseHandler, EnumerableSet} from "../../base/BaseHandler.t.sol"; 10 | 11 | /// @title EVCHandler 12 | /// @notice Handler test contract for the EVC actions 13 | contract EVCHandler is BaseHandler { 14 | using EnumerableSet for EnumerableSet.AddressSet; 15 | 16 | /////////////////////////////////////////////////////////////////////////////////////////////// 17 | // STATE VARIABLES // 18 | /////////////////////////////////////////////////////////////////////////////////////////////// 19 | 20 | /////////////////////////////////////////////////////////////////////////////////////////////// 21 | // GHOST VARAIBLES // 22 | /////////////////////////////////////////////////////////////////////////////////////////////// 23 | 24 | /////////////////////////////////////////////////////////////////////////////////////////////// 25 | // ACTIONS // 26 | /////////////////////////////////////////////////////////////////////////////////////////////// 27 | function setAccountOperator(uint8 i, uint8 j, bool authorised) external setup { 28 | bool success; 29 | bytes memory returnData; 30 | 31 | address account = _getRandomActor(i); 32 | 33 | address operator = _getRandomActor(j); 34 | 35 | (success, returnData) = actor.proxy( 36 | address(evc), 37 | abi.encodeWithSelector(EthereumVaultConnector.setAccountOperator.selector, account, operator, authorised) 38 | ); 39 | 40 | if (success) { 41 | assert(true); 42 | } 43 | } 44 | 45 | // COLLATERAL 46 | 47 | function enableCollateral(uint8 i) external setup { 48 | bool success; 49 | bytes memory returnData; 50 | 51 | // Get one of the three actors randomly 52 | address account = _getRandomActor(i); 53 | 54 | (success, returnData) = actor.proxy( 55 | address(evc), 56 | abi.encodeWithSelector(EthereumVaultConnector.enableCollateral.selector, account, trackingRewarded) 57 | ); 58 | 59 | if (success) { 60 | assert(true); 61 | } 62 | } 63 | 64 | function disableCollateral(uint8 i) external setup { 65 | bool success; 66 | bytes memory returnData; 67 | 68 | // Get one of the three actors randomly 69 | address account = _getRandomActor(i); 70 | 71 | (success, returnData) = actor.proxy( 72 | address(evc), 73 | abi.encodeWithSelector(EthereumVaultConnector.disableCollateral.selector, account, trackingRewarded) 74 | ); 75 | 76 | if (success) { 77 | assert(true); 78 | } 79 | } 80 | 81 | // CONTROLLER 82 | 83 | function enableController(uint8 i) external setup { 84 | bool success; 85 | bytes memory returnData; 86 | 87 | // Get one of the three actors randomly 88 | address account = _getRandomActor(i); 89 | 90 | (success, returnData) = actor.proxy( 91 | address(evc), 92 | abi.encodeWithSelector(EthereumVaultConnector.enableController.selector, account, address(controller)) 93 | ); 94 | 95 | if (success) { 96 | assert(true); 97 | } 98 | } 99 | 100 | function disableControllerEVC(uint8 i) external setup { 101 | bool success; 102 | bytes memory returnData; 103 | 104 | // Get one of the three actors randomly 105 | address account = _getRandomActor(i); 106 | 107 | address[] memory controllers = evc.getControllers(account); 108 | 109 | (success, returnData) = actor.proxy( 110 | address(evc), abi.encodeWithSelector(EthereumVaultConnector.disableController.selector, account) 111 | ); 112 | 113 | address[] memory controllersAfter = evc.getControllers(account); 114 | if (controllers.length == 0) { 115 | assertTrue(success); 116 | assertTrue(controllersAfter.length == 0); 117 | } else { 118 | assertEq(controllers.length, controllersAfter.length); 119 | } 120 | } 121 | 122 | /////////////////////////////////////////////////////////////////////////////////////////////// 123 | // HELPERS // 124 | /////////////////////////////////////////////////////////////////////////////////////////////// 125 | } 126 | -------------------------------------------------------------------------------- /test/invariants/handlers/interfaces/ILiquidationModuleHandler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | interface ILiquidationModuleHandler { 5 | function liquidate(uint256 repayAssets, uint256 minYielBalance, uint256 i) external; 6 | } 7 | -------------------------------------------------------------------------------- /test/invariants/handlers/simulators/ControllerHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Contracts 5 | import {BaseHandler} from "../../base/BaseHandler.t.sol"; 6 | 7 | // Mock Contracts 8 | import {MockController} from "test/utils/MockController.sol"; 9 | 10 | /// @title ControllerHandler 11 | /// @notice Handler test contract for the IRM actions 12 | contract ControllerHandler is BaseHandler { 13 | /////////////////////////////////////////////////////////////////////////////////////////////// 14 | // STATE VARIABLES // 15 | /////////////////////////////////////////////////////////////////////////////////////////////// 16 | /////////////////////////////////////////////////////////////////////////////////////////////// 17 | // GHOST VARAIBLES // 18 | /////////////////////////////////////////////////////////////////////////////////////////////// 19 | /////////////////////////////////////////////////////////////////////////////////////////////// 20 | // ACTIONS // 21 | /////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | function liquidateCollateralShares(uint8 i, uint256 amount) external setup { 24 | bool success; 25 | bytes memory returnData; 26 | 27 | // Get one of the three actors randomly 28 | address liquidated = _getRandomActor(i); 29 | 30 | (success, returnData) = actor.proxy( 31 | address(controller), 32 | abi.encodeWithSelector( 33 | MockController.liquidateCollateralShares.selector, 34 | trackingRewarded, 35 | liquidated, 36 | trackingRewarded, 37 | amount 38 | ) 39 | ); 40 | 41 | if (success) { 42 | assert(true); 43 | } 44 | } 45 | 46 | /////////////////////////////////////////////////////////////////////////////////////////////// 47 | // HELPERS // 48 | /////////////////////////////////////////////////////////////////////////////////////////////// 49 | } 50 | -------------------------------------------------------------------------------- /test/invariants/handlers/simulators/DonationAttackHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Libraries 5 | import {MockERC20} from "test/utils/MockERC20.sol"; 6 | 7 | // Contracts 8 | import {Actor} from "../../utils/Actor.sol"; 9 | import {BaseHandler} from "../../base/BaseHandler.t.sol"; 10 | 11 | /// @title DonationAttackHandler 12 | /// @notice Handler test contract for the DonationAttack actions 13 | contract DonationAttackHandler is BaseHandler { 14 | /////////////////////////////////////////////////////////////////////////////////////////////// 15 | // STATE VARIABLES // 16 | /////////////////////////////////////////////////////////////////////////////////////////////// 17 | 18 | /////////////////////////////////////////////////////////////////////////////////////////////// 19 | // GHOST VARAIBLES // 20 | /////////////////////////////////////////////////////////////////////////////////////////////// 21 | 22 | /////////////////////////////////////////////////////////////////////////////////////////////// 23 | // ACTIONS // 24 | /////////////////////////////////////////////////////////////////////////////////////////////// 25 | 26 | /// @notice This function transfers any amount of assets to a contract in the system 27 | /// @dev Flashloan simulator 28 | function donate(uint8 i, uint8 j, uint256 amount) external { 29 | // Get one of the tsystem assets randomly 30 | MockERC20 _token = MockERC20(_getRandomAsset(i)); 31 | 32 | // Get one of the two setups randomly 33 | (, address _target) = _getRandomRewards(j); 34 | 35 | _token.mint(address(this), amount); 36 | 37 | _token.transfer(_target, amount); 38 | 39 | _resetTarget(); 40 | } 41 | 42 | /////////////////////////////////////////////////////////////////////////////////////////////// 43 | // HELPERS // 44 | /////////////////////////////////////////////////////////////////////////////////////////////// 45 | } 46 | -------------------------------------------------------------------------------- /test/invariants/handlers/simulators/ERC20BalanceForwarderHandler.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Contracts 5 | import {BaseHandler} from "../../base/BaseHandler.t.sol"; 6 | 7 | // Interfaces 8 | import {ERC20, IBalanceForwarder} from "test/utils/MockERC20.sol"; 9 | 10 | /// @title ERC20BalanceForwarderHandler 11 | /// @notice Handler test contract for the IRM actions 12 | contract ERC20BalanceForwarderHandler is BaseHandler { 13 | /////////////////////////////////////////////////////////////////////////////////////////////// 14 | // STATE VARIABLES // 15 | /////////////////////////////////////////////////////////////////////////////////////////////// 16 | /////////////////////////////////////////////////////////////////////////////////////////////// 17 | // GHOST VARAIBLES // 18 | /////////////////////////////////////////////////////////////////////////////////////////////// 19 | /////////////////////////////////////////////////////////////////////////////////////////////// 20 | // ACTIONS // 21 | /////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | function enableBalanceForwarding() external setup { 24 | bool success; 25 | bytes memory returnData; 26 | 27 | (success, returnData) = 28 | actor.proxy(trackingRewarded, abi.encodeWithSelector(IBalanceForwarder.enableBalanceForwarding.selector)); 29 | 30 | if (success) { 31 | assert(true); 32 | } 33 | } 34 | 35 | function disableBalanceForwarding() external setup { 36 | bool success; 37 | bytes memory returnData; 38 | 39 | (success, returnData) = 40 | actor.proxy(trackingRewarded, abi.encodeWithSelector(IBalanceForwarder.disableBalanceForwarding.selector)); 41 | 42 | if (success) { 43 | assert(true); 44 | } 45 | } 46 | 47 | function transfer(uint256 i, uint256 amount) external setup { 48 | bool success; 49 | bytes memory returnData; 50 | 51 | address account = _getRandomActor(i); 52 | 53 | (success, returnData) = 54 | actor.proxy(trackingRewarded, abi.encodeWithSelector(ERC20.transfer.selector, account, amount)); 55 | 56 | if (success) { 57 | assert(true); 58 | } 59 | } 60 | 61 | /////////////////////////////////////////////////////////////////////////////////////////////// 62 | // HELPERS // 63 | /////////////////////////////////////////////////////////////////////////////////////////////// 64 | } 65 | -------------------------------------------------------------------------------- /test/invariants/hooks/BaseRewardsHooks.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Contracts 5 | import {BaseRewardStreams} from "src/BaseRewardStreams.sol"; 6 | import {BaseRewardStreamsHarness} from "test/harness/BaseRewardStreamsHarness.sol"; 7 | 8 | // Test Helpers 9 | import {Pretty, Strings} from "../utils/Pretty.sol"; 10 | 11 | // Test Contracts 12 | import {BaseHooks} from "../base/BaseHooks.t.sol"; 13 | 14 | import "forge-std/console.sol"; 15 | 16 | /// @title BaseRewards Before After Hooks 17 | /// @notice Helper contract for before and after hooks 18 | /// @dev This contract is inherited by handlers 19 | abstract contract BaseRewardsHooks is BaseHooks { 20 | using Strings for string; 21 | using Pretty for uint256; 22 | using Pretty for int256; 23 | 24 | struct BaseRewardsVars { 25 | // Rewards Accounting 26 | uint256 totalRewardedBefore; 27 | uint256 totalRewardedAfter; 28 | // Account Storage 29 | uint160 accumulatorBefore; 30 | uint160 accumulatorAfter; 31 | // Distribution Storage 32 | uint256 lastUpdatedBefore; 33 | uint256 lastUpdatedAfter; 34 | uint256 distributionAccumulatorBefore; 35 | uint256 distributionAccumulatorAfter; 36 | uint256 totalRegisteredBefore; 37 | uint256 totalRegisteredAfter; 38 | } 39 | 40 | BaseRewardsVars baseRewardsVars; 41 | 42 | function _baseRewardsBefore(address account, address rewarded, address _reward) internal { 43 | // Account Storage 44 | BaseRewardStreams.EarnStorage memory earnStorage = target.getAccountEarnedData(account, rewarded, _reward); 45 | baseRewardsVars.accumulatorBefore = earnStorage.accumulator; 46 | 47 | // Distribution Storage 48 | ( 49 | baseRewardsVars.lastUpdatedBefore, 50 | baseRewardsVars.distributionAccumulatorBefore, 51 | , 52 | baseRewardsVars.totalRegisteredBefore, 53 | ) = target.getDistributionData(rewarded, _reward); 54 | } 55 | 56 | function _baseRewardsAfter(address account, address rewarded, address _reward) internal { 57 | // Account Storage 58 | BaseRewardStreams.EarnStorage memory earnStorage = target.getAccountEarnedData(account, rewarded, _reward); 59 | baseRewardsVars.accumulatorAfter = earnStorage.accumulator; 60 | 61 | // Distribution Storage 62 | ( 63 | baseRewardsVars.lastUpdatedAfter, 64 | baseRewardsVars.distributionAccumulatorAfter, 65 | , 66 | baseRewardsVars.totalRegisteredAfter, 67 | ) = target.getDistributionData(rewarded, _reward); 68 | } 69 | 70 | /*///////////////////////////////////////////////////////////////////////////////////////////// 71 | // POST CONDITION INVARIANTS // 72 | /////////////////////////////////////////////////////////////////////////////////////////////*/ 73 | 74 | /////////////////////////////////////////////////////////////////////////////////////////////// 75 | // UPDATE REWARDS // 76 | /////////////////////////////////////////////////////////////////////////////////////////////// 77 | 78 | function assert_UPDATE_REWARDS_INVARIANT_A(address account, address rewarded, address _reward) internal { 79 | if ( 80 | _enabledRewards(account, rewarded, address(target)) 81 | && _distributionActive(rewarded, _reward, address(target)) 82 | ) { 83 | assertEq(baseRewardsVars.lastUpdatedAfter, block.timestamp, UPDATE_REWARDS_INVARIANT_A); 84 | } 85 | } 86 | 87 | /////////////////////////////////////////////////////////////////////////////////////////////// 88 | // DISTRIBUTION // 89 | /////////////////////////////////////////////////////////////////////////////////////////////// 90 | 91 | function assert_DISTRIBUTION_INVARIANT_A() internal { 92 | assertGe(baseRewardsVars.lastUpdatedAfter, baseRewardsVars.lastUpdatedBefore, DISTRIBUTION_INVARIANT_A); 93 | } 94 | 95 | function assert_DISTRIBUTION_INVARIANT_B() internal { 96 | assertGe( 97 | baseRewardsVars.distributionAccumulatorAfter, 98 | baseRewardsVars.distributionAccumulatorBefore, 99 | DISTRIBUTION_INVARIANT_B 100 | ); 101 | } 102 | 103 | function assert_DISTRIBUTION_INVARIANT_G(uint256 amount) internal { 104 | assertEq( 105 | baseRewardsVars.totalRegisteredAfter, 106 | baseRewardsVars.totalRegisteredBefore + amount, 107 | DISTRIBUTION_INVARIANT_G 108 | ); 109 | } 110 | 111 | function assert_DISTRIBUTION_INVARIANT_J(address _rewarded, address _reward, address _target) internal { 112 | BaseRewardStreamsHarness target_ = BaseRewardStreamsHarness(_target); 113 | BaseRewardStreamsHarness.EarnStorage memory earnStorage = 114 | target_.getAccountEarnedData(address(0), _rewarded, _reward); 115 | assertEq(earnStorage.claimable, 0, DISTRIBUTION_INVARIANT_J); 116 | } 117 | 118 | /////////////////////////////////////////////////////////////////////////////////////////////// 119 | // ACCOUNT STORAGE // 120 | /////////////////////////////////////////////////////////////////////////////////////////////// 121 | 122 | function assert_ACCOUNT_STORAGE_INVARIANT_A() internal { 123 | assertGe(baseRewardsVars.accumulatorAfter, baseRewardsVars.accumulatorBefore, ACCOUNT_STORAGE_INVARIANT_A); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /test/invariants/hooks/HookAggregator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Hook Contracts 5 | import {BaseRewardsHooks} from "./BaseRewardsHooks.t.sol"; 6 | 7 | /// @title HookAggregator 8 | /// @notice Helper contract to aggregate all before / after hook contracts, inherited on each handler 9 | abstract contract HookAggregator is BaseRewardsHooks { 10 | /// @notice Modular hook selector, per module 11 | function _before(address account, address rewarded, address _reward) internal { 12 | _baseRewardsBefore(account, rewarded, _reward); 13 | } 14 | 15 | /// @notice Modular hook selector, per module 16 | function _after(address account, address rewarded, address _reward) internal { 17 | _baseRewardsAfter(account, rewarded, _reward); 18 | 19 | // Postconditions 20 | _checkPostConditions(); 21 | } 22 | 23 | /// @notice Global Postconditions for the handlers 24 | /// @dev This function is called after each "hooked" action to check the postconditions 25 | /// @dev Individual postconditions are checked in the respective handler functions 26 | function _checkPostConditions() internal { 27 | // Distribution Postconditions 28 | assert_DISTRIBUTION_INVARIANT_A(); 29 | assert_DISTRIBUTION_INVARIANT_B(); 30 | 31 | // Account Storage Postconditions 32 | assert_ACCOUNT_STORAGE_INVARIANT_A(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/invariants/invariants/BaseInvariants.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Interfaces 5 | import {IERC20} from "forge-std/interfaces/IERC20.sol"; 6 | 7 | // Contracts 8 | import {BaseRewardStreamsHarness} from "test/harness/BaseRewardStreamsHarness.sol"; 9 | import {HandlerAggregator} from "../HandlerAggregator.t.sol"; 10 | 11 | import "forge-std/console.sol"; 12 | 13 | /// @title BaseInvariants 14 | /// @notice Implements Invariants for the protocol 15 | /// @dev Inherits HandlerAggregator to check actions in assertion testing mode 16 | abstract contract BaseInvariants is HandlerAggregator { 17 | /////////////////////////////////////////////////////////////////////////////////////////////// 18 | // BASE // 19 | /////////////////////////////////////////////////////////////////////////////////////////////// 20 | 21 | function assert_BASE_INVARIANT_B(address _rewarded, address _reward, address _target) internal { 22 | (uint256 totalEligible,,) = BaseRewardStreamsHarness(_target).getDistributionTotals(_rewarded, _reward); 23 | if (totalEligible == 0) { 24 | try BaseRewardStreamsHarness(_target).updateReward(_rewarded, _reward, address(0)) {} 25 | catch { 26 | assertTrue(false, BASE_INVARIANT_B); 27 | } 28 | } 29 | } 30 | 31 | function assert_BASE_INVARIANT_E(address _rewarded, address _reward, address _target) internal { 32 | assertGe( 33 | BaseRewardStreamsHarness(_target).totalRewardRegistered(_rewarded, _reward), 34 | BaseRewardStreamsHarness(_target).totalRewardClaimed(_rewarded, _reward), 35 | BASE_INVARIANT_E 36 | ); 37 | } 38 | 39 | /////////////////////////////////////////////////////////////////////////////////////////////// 40 | // UPDATE REWARDS // 41 | /////////////////////////////////////////////////////////////////////////////////////////////// 42 | 43 | function assert_UPDATE_REWARDS_INVARIANT_B(address _rewarded, address _reward, address _target) internal { 44 | BaseRewardStreamsHarness target_ = BaseRewardStreamsHarness(_target); 45 | (uint48 lastUpdated,,,,) = target_.getDistributionData(_rewarded, _reward); 46 | assertGe(target_.currentEpoch(), target_.getEpoch(lastUpdated), UPDATE_REWARDS_INVARIANT_B); 47 | } 48 | 49 | function assert_UPDATE_REWARDS_INVARIANT_C( 50 | address _rewarded, 51 | address _reward, 52 | address _target, 53 | address _user 54 | ) internal { 55 | BaseRewardStreamsHarness target_ = BaseRewardStreamsHarness(_target); 56 | BaseRewardStreamsHarness.EarnStorage memory earnStorage = 57 | target_.getAccountEarnedData(_user, _rewarded, _reward); 58 | (, uint208 accumulator,,,) = target_.getDistributionData(_rewarded, _reward); 59 | assertGe(accumulator, earnStorage.accumulator, UPDATE_REWARDS_INVARIANT_C); 60 | } 61 | 62 | function assert_UPDATE_REWARDS_INVARIANT_D(address _rewarded, address _reward, address _target) internal { 63 | BaseRewardStreamsHarness target_ = BaseRewardStreamsHarness(_target); 64 | BaseRewardStreamsHarness.EarnStorage memory earnStorage = 65 | target_.getAccountEarnedData(address(0), _rewarded, _reward); 66 | assertEq(earnStorage.accumulator, 0, UPDATE_REWARDS_INVARIANT_D); 67 | } 68 | 69 | /////////////////////////////////////////////////////////////////////////////////////////////// 70 | // DISTRIBUTION // 71 | /////////////////////////////////////////////////////////////////////////////////////////////// 72 | 73 | function assert_DISTRIBUTION_INVARIANT_C(address _rewarded, address _reward, address _target) internal { 74 | IERC20 rewardToken = IERC20(_reward); 75 | BaseRewardStreamsHarness target_ = BaseRewardStreamsHarness(_target); 76 | (,,, uint128 totalRegistered, uint128 totalClaimed) = target_.getDistributionData(_rewarded, _reward); 77 | assertGe(rewardToken.balanceOf(_target), totalRegistered - totalClaimed, DISTRIBUTION_INVARIANT_C); 78 | } 79 | 80 | function assert_DISTRIBUTION_INVARIANT_D(address _rewarded, address _reward, address _target) internal { 81 | BaseRewardStreamsHarness target_ = BaseRewardStreamsHarness(_target); 82 | (,,,, uint128 totalClaimed) = target_.getDistributionData(_rewarded, _reward); 83 | 84 | assertEq(totalClaimed, ghost_claims[_target][_rewarded][_reward], DISTRIBUTION_INVARIANT_D); 85 | } 86 | 87 | function assert_DISTRIBUTION_INVARIANT_E(address rewarded, address _reward, address _target) internal { 88 | BaseRewardStreamsHarness target_ = BaseRewardStreamsHarness(_target); 89 | uint48 currentEpoch = target_.currentEpoch(); 90 | assertEq( 91 | target_.getEpochData(rewarded, _reward, currentEpoch + MAX_EPOCHS_AHEAD_END), 0, DISTRIBUTION_INVARIANT_E 92 | ); 93 | } 94 | 95 | function assert_DISTRIBUTION_INVARIANT_I( 96 | address _rewarded, 97 | address _reward, 98 | address _target, 99 | uint256 _sumBalances 100 | ) internal { 101 | assertEq( 102 | BaseRewardStreamsHarness(_target).totalRewardedEligible(_rewarded, _reward), 103 | _sumBalances, 104 | DISTRIBUTION_INVARIANT_I 105 | ); 106 | } 107 | 108 | /////////////////////////////////////////////////////////////////////////////////////////////// 109 | // VIEW // 110 | /////////////////////////////////////////////////////////////////////////////////////////////// 111 | 112 | /////////////////////////////////////////////////////////////////////////////////////////////// 113 | // ACCOUNT STORAGE // 114 | /////////////////////////////////////////////////////////////////////////////////////////////// 115 | 116 | ////////////////////////////////////////////////////////////////////////////////////////////// 117 | // HELPERS // 118 | ////////////////////////////////////////////////////////////////////////////////////////////// 119 | 120 | function _getTotalAmountAcrossEpochs( 121 | address _rewarded, 122 | address _reward, 123 | BaseRewardStreamsHarness _target 124 | ) internal view returns (uint256 totalAmountAcrossEpochs) { 125 | (uint48 lastUpdated,,,,) = _target.getDistributionData(_rewarded, _reward); 126 | 127 | uint48 startEpoch = _target.getEpoch(lastUpdated); 128 | uint48 endEpoch = _target.currentEpoch() + MAX_EPOCHS_AHEAD_END; 129 | 130 | for (uint48 i = startEpoch; i <= endEpoch; i++) { 131 | totalAmountAcrossEpochs += _target.getEpochData(_rewarded, _reward, i); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /test/invariants/invariants/StakingInvariants.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Contracts 5 | import {HandlerAggregator} from "../HandlerAggregator.t.sol"; 6 | 7 | /// @title StakingInvariants 8 | /// @notice Implements Invariants for the protocol 9 | /// @dev Inherits HandlerAggregator to check actions in assertion testing mode 10 | abstract contract StakingInvariants is HandlerAggregator { 11 | function assert_STAKING_INVARIANT_A(address user) internal { 12 | assertEq( 13 | stakingDistributor.balanceOf(user, stakingRewarded), 14 | ghost_deposits[user][stakingRewarded], 15 | STAKING_INVARIANT_A 16 | ); 17 | } 18 | 19 | ////////////////////////////////////////////////////////////////////////////////////////////// 20 | // HELPERS // 21 | ////////////////////////////////////////////////////////////////////////////////////////////// 22 | } 23 | -------------------------------------------------------------------------------- /test/invariants/invariants/TrackingInvariants.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Contracts 5 | import {HandlerAggregator} from "../HandlerAggregator.t.sol"; 6 | 7 | /// @title TrackingInvariants 8 | /// @notice Implements Invariants for the protocol 9 | /// @dev Inherits HandlerAggregator to check actions in assertion testing mode 10 | abstract contract TrackingInvariants is HandlerAggregator { 11 | ////////////////////////////////////////////////////////////////////////////////////////////// 12 | // HELPERS // 13 | ////////////////////////////////////////////////////////////////////////////////////////////// 14 | } 15 | -------------------------------------------------------------------------------- /test/invariants/utils/Actor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | // Interfaces 5 | import {IERC20} from "forge-std/interfaces/IERC20.sol"; 6 | 7 | /// @title Actor 8 | /// @notice Proxy contract for invariant suite actors to avoid Tester calling contracts 9 | /// @dev This expands the flexibility of the invariant suite 10 | contract Actor { 11 | /// @notice list of tokens to approve 12 | address[] internal tokens; 13 | /// @notice list of callers to approve tokens to 14 | address[] internal callers; 15 | 16 | constructor(address[] memory _tokens, address[] memory _callers) payable { 17 | tokens = _tokens; 18 | callers = _callers; 19 | for (uint256 i = 0; i < tokens.length; i++) { 20 | for (uint256 j = 0; j < callers.length; j++) { 21 | IERC20(tokens[i]).approve(callers[j], type(uint256).max); 22 | } 23 | } 24 | } 25 | 26 | /// @notice Helper function to proxy a call to a target contract, used to avoid Tester calling contracts 27 | function proxy(address _target, bytes memory _calldata) public returns (bool success, bytes memory returnData) { 28 | (success, returnData) = address(_target).call(_calldata); 29 | } 30 | 31 | /// @notice Helper function to proxy a call and value to a target contract, used to avoid Tester calling contracts 32 | function proxy( 33 | address _target, 34 | bytes memory _calldata, 35 | uint256 value 36 | ) public returns (bool success, bytes memory returnData) { 37 | (success, returnData) = address(_target).call{value: value}(_calldata); 38 | } 39 | 40 | receive() external payable {} 41 | } 42 | -------------------------------------------------------------------------------- /test/invariants/utils/Pretty.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.19; 3 | 4 | ///@notice https://github.com/one-hundred-proof/kyberswap-exploit/blob/main/lib/helpers/Pretty.sol 5 | library Strings { 6 | function concat(string memory _base, string memory _value) internal pure returns (string memory) { 7 | bytes memory _baseBytes = bytes(_base); 8 | bytes memory _valueBytes = bytes(_value); 9 | 10 | string memory _tmpValue = new string(_baseBytes.length + _valueBytes.length); 11 | bytes memory _newValue = bytes(_tmpValue); 12 | 13 | uint256 i; 14 | uint256 j; 15 | 16 | for (i = 0; i < _baseBytes.length; i++) { 17 | _newValue[j++] = _baseBytes[i]; 18 | } 19 | 20 | for (i = 0; i < _valueBytes.length; i++) { 21 | _newValue[j++] = _valueBytes[i]; 22 | } 23 | 24 | return string(_newValue); 25 | } 26 | } 27 | 28 | library Pretty { 29 | uint8 constant DEFAULT_DECIMALS = 18; 30 | 31 | function toBitString(uint256 n) external pure returns (string memory) { 32 | return uintToBitString(n, 256); 33 | } 34 | 35 | function toBitString(uint256 n, uint8 decimals) external pure returns (string memory) { 36 | return uintToBitString(n, decimals); 37 | } 38 | 39 | function pretty(uint256 n) external pure returns (string memory) { 40 | return n == type(uint256).max 41 | ? "type(uint256).max" 42 | : n == type(uint128).max ? "type(uint128).max" : _pretty(n, DEFAULT_DECIMALS); 43 | } 44 | 45 | function pretty(bool value) external pure returns (string memory) { 46 | return value ? "true" : "false"; 47 | } 48 | 49 | function pretty(uint256 n, uint8 decimals) external pure returns (string memory) { 50 | return _pretty(n, decimals); 51 | } 52 | 53 | function pretty(int256 n) external pure returns (string memory) { 54 | return _prettyInt(n, DEFAULT_DECIMALS); 55 | } 56 | 57 | function pretty(int256 n, uint8 decimals) external pure returns (string memory) { 58 | return _prettyInt(n, decimals); 59 | } 60 | 61 | function _pretty(uint256 n, uint8 decimals) internal pure returns (string memory) { 62 | bool pastDecimals = decimals == 0; 63 | uint256 place = 0; 64 | uint256 r; // remainder 65 | string memory s = ""; 66 | 67 | while (n != 0) { 68 | r = n % 10; 69 | n /= 10; 70 | place++; 71 | s = Strings.concat(toDigit(r), s); 72 | if (pastDecimals && place % 3 == 0 && n != 0) { 73 | s = Strings.concat("_", s); 74 | } 75 | if (!pastDecimals && place == decimals) { 76 | pastDecimals = true; 77 | place = 0; 78 | s = Strings.concat("_", s); 79 | } 80 | } 81 | if (pastDecimals && place == 0) { 82 | s = Strings.concat("0", s); 83 | } 84 | if (!pastDecimals) { 85 | uint256 i; 86 | uint256 upper = (decimals >= place ? decimals - place : 0); 87 | for (i = 0; i < upper; ++i) { 88 | s = Strings.concat("0", s); 89 | } 90 | s = Strings.concat("0_", s); 91 | } 92 | return s; 93 | } 94 | 95 | function _prettyInt(int256 n, uint8 decimals) internal pure returns (string memory) { 96 | bool isNegative = n < 0; 97 | string memory s = ""; 98 | if (isNegative) { 99 | s = "-"; 100 | } 101 | return Strings.concat(s, _pretty(uint256(isNegative ? -n : n), decimals)); 102 | } 103 | 104 | function toDigit(uint256 n) internal pure returns (string memory) { 105 | if (n == 0) { 106 | return "0"; 107 | } else if (n == 1) { 108 | return "1"; 109 | } else if (n == 2) { 110 | return "2"; 111 | } else if (n == 3) { 112 | return "3"; 113 | } else if (n == 4) { 114 | return "4"; 115 | } else if (n == 5) { 116 | return "5"; 117 | } else if (n == 6) { 118 | return "6"; 119 | } else if (n == 7) { 120 | return "7"; 121 | } else if (n == 8) { 122 | return "8"; 123 | } else if (n == 9) { 124 | return "9"; 125 | } else { 126 | revert("Not in range 0 to 10"); 127 | } 128 | } 129 | 130 | function uintToBitString(uint256 n, uint16 bits) internal pure returns (string memory) { 131 | string memory s = ""; 132 | for (uint256 i; i < bits; i++) { 133 | if (n % 2 == 0) { 134 | s = Strings.concat("0", s); 135 | } else { 136 | s = Strings.concat("1", s); 137 | } 138 | n = n / 2; 139 | } 140 | return s; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /test/invariants/utils/PropertiesConstants.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | abstract contract PropertiesConstants { 5 | // Constant echidna addresses 6 | address constant USER1 = address(0x10000); 7 | address constant USER2 = address(0x20000); 8 | address constant USER3 = address(0x30000); 9 | uint256 constant INITIAL_BALANCE = 1000e18; 10 | 11 | // Protocol constants 12 | uint256 constant MAX_EPOCHS_AHEAD = 5; 13 | uint256 constant MAX_DISTRIBUTION_LENGTH = 25; 14 | 15 | uint48 constant MAX_EPOCHS_AHEAD_END = uint48(MAX_EPOCHS_AHEAD) + uint48(MAX_DISTRIBUTION_LENGTH); 16 | uint256 constant SCALER = 2e19; 17 | } 18 | -------------------------------------------------------------------------------- /test/scripts/echidna-assert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echidna test/invariants/Tester.t.sol --test-mode assertion --contract Tester --config ./test/invariants/_config/echidna_config.yaml --corpus-dir ./test/invariants/_corpus/echidna/default/_data/corpus -------------------------------------------------------------------------------- /test/scripts/echidna.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echidna test/invariants/Tester.t.sol --contract Tester --config ./test/invariants/_config/echidna_config.yaml --corpus-dir ./test/invariants/_corpus/echidna/default/_data/corpus -------------------------------------------------------------------------------- /test/scripts/medusa.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | medusa fuzz -------------------------------------------------------------------------------- /test/unit/POC.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "forge-std/Test.sol"; 6 | import {EthereumVaultConnector} from "evc/EthereumVaultConnector.sol"; 7 | import {StakingRewardStreams} from "../../src/StakingRewardStreams.sol"; 8 | import {TrackingRewardStreams} from "../../src/TrackingRewardStreams.sol"; 9 | import {MockERC20, MockERC20BalanceForwarder} from "../utils/MockERC20.sol"; 10 | 11 | contract POC_Test is Test { 12 | EthereumVaultConnector internal evc; 13 | StakingRewardStreams internal stakingDistributor; 14 | TrackingRewardStreams internal trackingDistributor; 15 | MockERC20 internal mockERC20; 16 | MockERC20BalanceForwarder internal mockERC20BalanceForwarder; 17 | 18 | function setUp() external { 19 | evc = new EthereumVaultConnector(); 20 | 21 | stakingDistributor = new StakingRewardStreams(address(evc), 10 days); 22 | mockERC20 = new MockERC20("Mock ERC20", "MOCK"); 23 | 24 | trackingDistributor = new TrackingRewardStreams(address(evc), 10 days); 25 | mockERC20BalanceForwarder = new MockERC20BalanceForwarder(evc, trackingDistributor, "Mock ERC20 BT", "MOCK_BT"); 26 | } 27 | 28 | function test_POC() external {} 29 | } 30 | -------------------------------------------------------------------------------- /test/unit/RegisterReward.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "forge-std/Test.sol"; 6 | import "evc/EthereumVaultConnector.sol"; 7 | import "../harness/BaseRewardStreamsHarness.sol"; 8 | import {MockERC20, MockERC20Malicious} from "../utils/MockERC20.sol"; 9 | 10 | contract RegisterRewardTest is Test { 11 | EthereumVaultConnector internal evc; 12 | BaseRewardStreamsHarness internal distributor; 13 | mapping(address rewarded => mapping(address reward => mapping(uint256 epoch => uint256 amount))) internal 14 | distributionAmounts; 15 | address internal rewarded; 16 | address internal reward; 17 | address internal seeder; 18 | 19 | function setUp() external { 20 | evc = new EthereumVaultConnector(); 21 | 22 | distributor = new BaseRewardStreamsHarness(address(evc), 10 days); 23 | 24 | rewarded = address(new MockERC20("Rewarded", "RWDD")); 25 | vm.label(rewarded, "REWARDED"); 26 | 27 | reward = address(new MockERC20("Reward", "RWD")); 28 | vm.label(reward, "REWARD"); 29 | 30 | seeder = vm.addr(0xabcdef); 31 | vm.label(seeder, "SEEDER"); 32 | 33 | MockERC20(reward).mint(seeder, 100e18); 34 | 35 | vm.prank(seeder); 36 | MockERC20(reward).approve(address(distributor), type(uint256).max); 37 | } 38 | 39 | function updateDistributionAmounts( 40 | address _rewarded, 41 | address _reward, 42 | uint48 _startEpoch, 43 | uint128[] memory _amounts 44 | ) internal { 45 | for (uint256 i; i < _amounts.length; ++i) { 46 | distributionAmounts[_rewarded][_reward][_startEpoch + i] += _amounts[i]; 47 | } 48 | } 49 | 50 | function test_RevertIfInvalidEpochDuration_Constructor(uint48 epochDuration) external { 51 | if (epochDuration < 7 days || epochDuration > 10 * 7 days) { 52 | vm.expectRevert(BaseRewardStreams.InvalidEpoch.selector); 53 | } 54 | 55 | new BaseRewardStreamsHarness(address(1), epochDuration); 56 | } 57 | 58 | function test_RegisterReward( 59 | uint48 epochDuration, 60 | uint48 blockTimestamp, 61 | uint48 startEpoch, 62 | uint8 amountsLength0, 63 | uint8 amountsLength1, 64 | uint8 amountsLength2, 65 | uint256 seed 66 | ) external { 67 | epochDuration = uint48(bound(epochDuration, 7 days, 10 * 7 days)); 68 | blockTimestamp = uint48(bound(blockTimestamp, 1, type(uint48).max - 50 * epochDuration)); 69 | amountsLength0 = uint8(bound(amountsLength0, 1, 25)); 70 | amountsLength1 = uint8(bound(amountsLength1, 1, 25)); 71 | amountsLength2 = uint8(bound(amountsLength2, 1, 25)); 72 | 73 | vm.warp(blockTimestamp); 74 | distributor = new BaseRewardStreamsHarness(address(evc), epochDuration); 75 | 76 | vm.startPrank(seeder); 77 | MockERC20(reward).approve(address(distributor), type(uint256).max); 78 | 79 | // ------------------ 1st call ------------------ 80 | // prepare the start epoch 81 | startEpoch = uint48( 82 | bound( 83 | startEpoch, distributor.currentEpoch() + 1, distributor.currentEpoch() + distributor.MAX_EPOCHS_AHEAD() 84 | ) 85 | ); 86 | 87 | // prepare the amounts 88 | uint128[] memory amounts = new uint128[](amountsLength0); 89 | uint128 totalAmount = 0; 90 | for (uint256 i; i < amounts.length; ++i) { 91 | amounts[i] = uint128(uint256(keccak256(abi.encode(seed, i)))) % 1e18; 92 | totalAmount += amounts[i]; 93 | } 94 | 95 | vm.expectEmit(true, true, true, true, address(distributor)); 96 | emit BaseRewardStreams.RewardRegistered(seeder, rewarded, reward, startEpoch, amounts); 97 | distributor.registerReward(rewarded, reward, startEpoch, amounts); 98 | 99 | // verify that the total amount was properly transferred 100 | assertEq(MockERC20(reward).balanceOf(address(distributor)), totalAmount); 101 | 102 | // verify that the distribution and totals storage were properly initialized 103 | { 104 | ( 105 | uint48 lastUpdated, 106 | uint208 accumulator, 107 | uint256 totalEligible, 108 | uint128 totalRegistered, 109 | uint128 totalClaimed 110 | ) = distributor.getDistributionData(rewarded, reward); 111 | assertEq(lastUpdated, block.timestamp); 112 | assertEq(accumulator, 0); 113 | assertEq(totalEligible, 0); 114 | assertEq(totalRegistered, totalAmount); 115 | assertEq(totalClaimed, 0); 116 | } 117 | 118 | // verify that the distribution amounts storage was properly updated 119 | updateDistributionAmounts(rewarded, reward, startEpoch, amounts); 120 | 121 | for (uint48 i; i <= distributor.MAX_EPOCHS_AHEAD(); ++i) { 122 | assertEq( 123 | distributor.rewardAmount(rewarded, reward, startEpoch + i), 124 | distributionAmounts[rewarded][reward][startEpoch + i] 125 | ); 126 | } 127 | 128 | // ------------------ 2nd call ------------------ 129 | // prepare the start epoch 130 | startEpoch = 0; 131 | 132 | // prepare the amounts 133 | seed = uint256(keccak256(abi.encode(seed))); 134 | amounts = new uint128[](amountsLength1); 135 | totalAmount = 0; 136 | for (uint256 i; i < amounts.length; ++i) { 137 | amounts[i] = uint128(uint256(keccak256(abi.encode(seed, i)))) % 1e18; 138 | totalAmount += amounts[i]; 139 | } 140 | 141 | uint256 preBalance = MockERC20(reward).balanceOf(address(distributor)); 142 | vm.expectEmit(true, true, true, true, address(distributor)); 143 | emit BaseRewardStreams.RewardRegistered(seeder, rewarded, reward, distributor.currentEpoch() + 1, amounts); 144 | distributor.registerReward(rewarded, reward, startEpoch, amounts); 145 | 146 | // verify that the total amount was properly transferred 147 | assertEq(MockERC20(reward).balanceOf(address(distributor)), preBalance + totalAmount); 148 | 149 | // verify that the totals storage was properly updated (no time elapsed) 150 | { 151 | ( 152 | uint48 lastUpdated, 153 | uint208 accumulator, 154 | uint256 totalEligible, 155 | uint128 totalRegistered, 156 | uint128 totalClaimed 157 | ) = distributor.getDistributionData(rewarded, reward); 158 | assertEq(lastUpdated, block.timestamp); 159 | assertEq(accumulator, 0); 160 | assertEq(totalEligible, 0); 161 | assertEq(totalRegistered, uint128(preBalance) + totalAmount); 162 | assertEq(totalClaimed, 0); 163 | } 164 | 165 | // verify that the distribution amounts storage was properly updated 166 | startEpoch = distributor.currentEpoch() + 1; 167 | updateDistributionAmounts(rewarded, reward, startEpoch, amounts); 168 | 169 | for (uint48 i; i <= distributor.MAX_EPOCHS_AHEAD(); ++i) { 170 | assertEq( 171 | distributor.rewardAmount(rewarded, reward, startEpoch + i), 172 | distributionAmounts[rewarded][reward][startEpoch + i] 173 | ); 174 | } 175 | 176 | // ------------------ 3rd call ------------------ 177 | // elapse some random amount of time 178 | vm.warp(blockTimestamp + epochDuration * amountsLength0 + amountsLength1 + amountsLength2); 179 | 180 | // prepare the start epoch 181 | startEpoch = uint48( 182 | bound( 183 | startEpoch, distributor.currentEpoch() + 1, distributor.currentEpoch() + distributor.MAX_EPOCHS_AHEAD() 184 | ) 185 | ); 186 | 187 | // prepare the amounts 188 | seed = uint256(keccak256(abi.encode(seed))); 189 | amounts = new uint128[](amountsLength2); 190 | totalAmount = 0; 191 | for (uint256 i; i < amounts.length; ++i) { 192 | amounts[i] = uint128(uint256(keccak256(abi.encode(seed, i)))) % 1e18; 193 | totalAmount += amounts[i]; 194 | } 195 | 196 | preBalance = MockERC20(reward).balanceOf(address(distributor)); 197 | vm.expectEmit(true, true, true, true, address(distributor)); 198 | emit BaseRewardStreams.RewardRegistered(seeder, rewarded, reward, startEpoch, amounts); 199 | distributor.registerReward(rewarded, reward, startEpoch, amounts); 200 | 201 | // verify that the total amount was properly transferred 202 | assertEq(MockERC20(reward).balanceOf(address(distributor)), preBalance + totalAmount); 203 | 204 | // verify that the totals storage was properly updated (considering that some has time elapsed) 205 | { 206 | ( 207 | uint48 lastUpdated, 208 | uint208 accumulator, 209 | uint256 totalEligible, 210 | uint128 totalRegistered, 211 | uint128 totalClaimed 212 | ) = distributor.getDistributionData(rewarded, reward); 213 | assertEq(lastUpdated, block.timestamp); 214 | assertEq(accumulator, 0); 215 | assertEq(totalEligible, 0); 216 | assertEq(totalRegistered, uint128(preBalance) + totalAmount); 217 | assertEq(totalClaimed, 0); 218 | } 219 | 220 | // verify that the distribution amounts storage was properly updated 221 | updateDistributionAmounts(rewarded, reward, startEpoch, amounts); 222 | 223 | for (uint48 i; i <= distributor.MAX_EPOCHS_AHEAD(); ++i) { 224 | assertEq( 225 | distributor.rewardAmount(rewarded, reward, startEpoch + i), 226 | distributionAmounts[rewarded][reward][startEpoch + i] 227 | ); 228 | } 229 | } 230 | 231 | function test_RevertIfInvalidEpoch_RegisterReward(uint48 blockTimestamp) external { 232 | blockTimestamp = uint48( 233 | bound(blockTimestamp, distributor.EPOCH_DURATION() + 1, type(uint48).max - distributor.EPOCH_DURATION()) 234 | ); 235 | vm.warp(blockTimestamp); 236 | 237 | uint128[] memory amounts = new uint128[](1); 238 | amounts[0] = 1; 239 | 240 | vm.startPrank(seeder); 241 | uint48 startEpoch = distributor.currentEpoch(); 242 | vm.expectRevert(BaseRewardStreams.InvalidEpoch.selector); 243 | distributor.registerReward(rewarded, reward, startEpoch, amounts); 244 | vm.stopPrank(); 245 | 246 | vm.startPrank(seeder); 247 | startEpoch = uint48(distributor.currentEpoch() + distributor.MAX_EPOCHS_AHEAD() + 1); 248 | vm.expectRevert(BaseRewardStreams.InvalidEpoch.selector); 249 | distributor.registerReward(rewarded, reward, startEpoch, amounts); 250 | vm.stopPrank(); 251 | 252 | // succeeds if the epoch is valid 253 | vm.startPrank(seeder); 254 | startEpoch = 0; 255 | distributor.registerReward(rewarded, reward, startEpoch, amounts); 256 | vm.stopPrank(); 257 | 258 | vm.startPrank(seeder); 259 | startEpoch = distributor.currentEpoch() + 1; 260 | distributor.registerReward(rewarded, reward, startEpoch, amounts); 261 | vm.stopPrank(); 262 | 263 | vm.startPrank(seeder); 264 | startEpoch = uint48(distributor.currentEpoch() + distributor.MAX_EPOCHS_AHEAD()); 265 | distributor.registerReward(rewarded, reward, startEpoch, amounts); 266 | vm.stopPrank(); 267 | } 268 | 269 | function test_RevertIfInvalidAmounts_RegisterReward(uint8 numberOfEpochs) external { 270 | uint128[] memory amounts = new uint128[](numberOfEpochs); 271 | 272 | if (amounts.length > distributor.MAX_DISTRIBUTION_LENGTH()) { 273 | vm.expectRevert(BaseRewardStreams.InvalidDistribution.selector); 274 | distributor.registerReward(rewarded, reward, 0, amounts); 275 | } else { 276 | vm.expectRevert(BaseRewardStreams.InvalidAmount.selector); 277 | distributor.registerReward(rewarded, reward, 0, amounts); 278 | } 279 | } 280 | 281 | function test_RevertIfAccumulatorOverflows_RegisterReward() external { 282 | uint128[] memory amounts = new uint128[](1); 283 | amounts[0] = 1; 284 | 285 | uint128 maxRegistered = uint128(type(uint160).max / 2e19); 286 | 287 | // initialize the distribution data and set the total registered amount to the max value 288 | distributor.setDistributionData(rewarded, reward, uint48(1), 0, 0, maxRegistered, 0); 289 | 290 | vm.startPrank(seeder); 291 | vm.expectRevert(BaseRewardStreams.AccumulatorOverflow.selector); 292 | distributor.registerReward(rewarded, reward, 0, amounts); 293 | vm.stopPrank(); 294 | 295 | // accumulator doesn't overflow if the total registered amount is less than the max value 296 | distributor.setDistributionData(rewarded, reward, uint48(1), 0, 0, maxRegistered - 1, 0); 297 | 298 | vm.startPrank(seeder); 299 | distributor.registerReward(rewarded, reward, 0, amounts); 300 | vm.stopPrank(); 301 | } 302 | 303 | function test_RevertIfMaliciousToken_RegisterReward(uint16[] calldata _amounts) external { 304 | vm.assume(_amounts.length > 0 && _amounts.length <= distributor.MAX_DISTRIBUTION_LENGTH() && _amounts[0] > 0); 305 | 306 | uint128[] memory amounts = new uint128[](_amounts.length); 307 | for (uint256 i; i < amounts.length; ++i) { 308 | amounts[i] = uint128(_amounts[i]); 309 | } 310 | 311 | address malicious = address(new MockERC20Malicious("Malicious", "MAL")); 312 | MockERC20(malicious).mint(seeder, type(uint256).max); 313 | 314 | vm.prank(seeder); 315 | MockERC20(malicious).approve(address(distributor), type(uint256).max); 316 | 317 | vm.startPrank(seeder); 318 | vm.expectRevert(BaseRewardStreams.InvalidAmount.selector); 319 | distributor.registerReward(rewarded, malicious, 0, amounts); 320 | vm.stopPrank(); 321 | 322 | // succeeds if the token is not malicious 323 | vm.startPrank(seeder); 324 | distributor.registerReward(rewarded, reward, 0, amounts); 325 | vm.stopPrank(); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /test/unit/Staking.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "forge-std/Test.sol"; 6 | import "evc/EthereumVaultConnector.sol"; 7 | import "../harness/StakingRewardStreamsHarness.sol"; 8 | import {MockERC20, MockERC20Malicious} from "../utils/MockERC20.sol"; 9 | import {MockController} from "../utils/MockController.sol"; 10 | 11 | contract StakingTest is Test { 12 | EthereumVaultConnector internal evc; 13 | StakingRewardStreamsHarness internal distributor; 14 | address internal rewarded; 15 | address internal rewardedMalicious; 16 | 17 | function setUp() external { 18 | evc = new EthereumVaultConnector(); 19 | distributor = new StakingRewardStreamsHarness(address(evc), 10 days); 20 | rewarded = address(new MockERC20("Rewarded", "RWDD")); 21 | rewardedMalicious = address(new MockERC20Malicious("RewardedMalicious", "RWDMLC")); 22 | } 23 | 24 | function test_StakeAndUnstake(address participant, uint64 amount, address recipient) external { 25 | vm.assume( 26 | uint160(participant) > 255 && participant != address(evc) && participant != address(distributor) 27 | && participant != rewarded 28 | ); 29 | vm.assume( 30 | uint160(recipient) > 255 && recipient != address(evc) 31 | && !evc.haveCommonOwner(recipient, address(distributor)) && recipient != rewarded 32 | ); 33 | vm.assume(amount > 0); 34 | 35 | // mint tokens and approve 36 | vm.startPrank(participant); 37 | MockERC20(rewarded).mint(participant, 10 * uint256(amount)); 38 | MockERC20(rewarded).approve(address(distributor), type(uint256).max); 39 | MockERC20(rewardedMalicious).mint(participant, 10 * uint256(amount)); 40 | MockERC20(rewardedMalicious).approve(address(distributor), type(uint256).max); 41 | 42 | // stake 0 amount 43 | vm.expectRevert(BaseRewardStreams.InvalidAmount.selector); 44 | distributor.stake(rewarded, 0); 45 | 46 | // stake 47 | uint256 preBalanceParticipant = MockERC20(rewarded).balanceOf(participant); 48 | uint256 preBalanceDistributor = MockERC20(rewarded).balanceOf(address(distributor)); 49 | distributor.stake(rewarded, amount); 50 | assertEq(MockERC20(rewarded).balanceOf(participant), preBalanceParticipant - amount); 51 | assertEq(MockERC20(rewarded).balanceOf(address(distributor)), preBalanceDistributor + amount); 52 | 53 | // unstake 0 amount 54 | vm.expectRevert(BaseRewardStreams.InvalidAmount.selector); 55 | distributor.unstake(rewarded, 0, participant, false); 56 | 57 | // unstake greater than staked amount 58 | vm.expectRevert(BaseRewardStreams.InvalidAmount.selector); 59 | distributor.unstake(rewarded, uint256(amount) + 1, participant, false); 60 | 61 | // unstake 62 | uint256 preBalanceRecipient = MockERC20(rewarded).balanceOf(recipient); 63 | preBalanceDistributor = MockERC20(rewarded).balanceOf(address(distributor)); 64 | distributor.unstake(rewarded, amount, recipient, false); 65 | assertEq(MockERC20(rewarded).balanceOf(recipient), preBalanceRecipient + amount); 66 | assertEq(MockERC20(rewarded).balanceOf(address(distributor)), preBalanceDistributor - amount); 67 | 68 | // stake max 69 | preBalanceParticipant = MockERC20(rewarded).balanceOf(participant); 70 | preBalanceDistributor = MockERC20(rewarded).balanceOf(address(distributor)); 71 | distributor.stake(rewarded, type(uint256).max); 72 | assertEq(MockERC20(rewarded).balanceOf(participant), 0); 73 | assertEq(MockERC20(rewarded).balanceOf(address(distributor)), preBalanceDistributor + preBalanceParticipant); 74 | 75 | // unstake max 76 | preBalanceRecipient = MockERC20(rewarded).balanceOf(recipient); 77 | preBalanceDistributor = MockERC20(rewarded).balanceOf(address(distributor)); 78 | distributor.unstake(rewarded, type(uint256).max, recipient, false); 79 | assertEq(MockERC20(rewarded).balanceOf(recipient), preBalanceRecipient + preBalanceDistributor); 80 | assertEq(MockERC20(rewarded).balanceOf(address(distributor)), 0); 81 | 82 | // stake malicious 83 | vm.expectRevert(BaseRewardStreams.InvalidAmount.selector); 84 | distributor.stake(rewardedMalicious, amount); 85 | vm.stopPrank(); 86 | 87 | // stake max from recipient 88 | vm.startPrank(recipient); 89 | MockERC20(rewarded).approve(address(distributor), type(uint256).max); 90 | distributor.stake(rewarded, type(uint256).max); 91 | 92 | // unstake to zero address 93 | vm.expectRevert(BaseRewardStreams.InvalidRecipient.selector); 94 | distributor.unstake(rewarded, type(uint256).max, address(0), false); 95 | 96 | // register the receiver as the owner on the EVC 97 | assertEq(evc.getAccountOwner(recipient), address(0)); 98 | evc.call(address(0), recipient, 0, ""); 99 | assertEq(evc.getAccountOwner(recipient), recipient); 100 | 101 | for (uint160 i = 1; i < 256; ++i) { 102 | address _recipient = address(uint160(recipient) ^ i); 103 | 104 | // if known non-owner is the recipient, revert 105 | vm.expectRevert(BaseRewardStreams.InvalidRecipient.selector); 106 | distributor.unstake(rewarded, type(uint256).max, _recipient, false); 107 | } 108 | 109 | // but if owner is the recipient, it should work 110 | uint256 snapshot = vm.snapshotState(); 111 | preBalanceRecipient = MockERC20(rewarded).balanceOf(recipient); 112 | preBalanceDistributor = MockERC20(rewarded).balanceOf(address(distributor)); 113 | distributor.unstake(rewarded, type(uint256).max, recipient, false); 114 | assertEq(MockERC20(rewarded).balanceOf(recipient), preBalanceRecipient + preBalanceDistributor); 115 | assertEq(MockERC20(rewarded).balanceOf(address(distributor)), 0); 116 | 117 | // it should also work if the rewarded token is EVC-compatible 118 | vm.revertToState(snapshot); 119 | vm.mockCall(rewarded, abi.encodeWithSignature("EVC()"), abi.encode(address(evc))); 120 | 121 | for (uint160 i = 1; i < 256; ++i) { 122 | snapshot = vm.snapshotState(); 123 | 124 | address _recipient = address(uint160(recipient) ^ i); 125 | 126 | // if known non-owner is the recipient, but the rewarded token is EVC-compatible, proceed 127 | preBalanceRecipient = MockERC20(rewarded).balanceOf(_recipient); 128 | preBalanceDistributor = MockERC20(rewarded).balanceOf(address(distributor)); 129 | distributor.unstake(rewarded, type(uint256).max, _recipient, false); 130 | assertEq(MockERC20(rewarded).balanceOf(_recipient), preBalanceRecipient + preBalanceDistributor); 131 | assertEq(MockERC20(rewarded).balanceOf(address(distributor)), 0); 132 | 133 | vm.revertToState(snapshot); 134 | } 135 | 136 | vm.stopPrank(); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /test/unit/View.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "forge-std/Test.sol"; 6 | import "evc/EthereumVaultConnector.sol"; 7 | import "../harness/BaseRewardStreamsHarness.sol"; 8 | import {boundAddr} from "../utils/TestUtils.sol"; 9 | 10 | contract ViewTest is Test { 11 | EthereumVaultConnector internal evc; 12 | BaseRewardStreamsHarness internal distributor; 13 | 14 | function setUp() external { 15 | evc = new EthereumVaultConnector(); 16 | distributor = new BaseRewardStreamsHarness(address(evc), 10 days); 17 | } 18 | 19 | function test_EnabledRewards(address account, address rewarded, uint8 n, uint256 seed) external { 20 | account = boundAddr(account); 21 | rewarded = boundAddr(rewarded); 22 | n = uint8(bound(n, 1, 5)); 23 | 24 | vm.startPrank(account); 25 | for (uint8 i = 0; i < n; i++) { 26 | address reward = address(uint160(uint256(keccak256(abi.encode(seed, i))))); 27 | distributor.enableReward(rewarded, reward); 28 | 29 | address[] memory enabledRewards = distributor.enabledRewards(account, rewarded); 30 | assertEq(enabledRewards.length, i + 1); 31 | assertEq(enabledRewards[i], reward); 32 | } 33 | } 34 | 35 | function test_IsRewardEnabled(address account, address rewarded, uint8 n, uint256 index, uint256 seed) external { 36 | account = boundAddr(account); 37 | rewarded = boundAddr(rewarded); 38 | n = uint8(bound(n, 1, 5)); 39 | index = uint8(bound(index, 0, n - 1)); 40 | 41 | vm.startPrank(account); 42 | for (uint8 i = 0; i < n; i++) { 43 | address reward = address(uint160(uint256(keccak256(abi.encode(seed, i))))); 44 | bool wasEnabled = distributor.enableReward(rewarded, reward); 45 | 46 | assertTrue(distributor.isRewardEnabled(account, rewarded, reward)); 47 | assertTrue(wasEnabled); 48 | assertFalse(distributor.enableReward(rewarded, reward)); 49 | } 50 | 51 | address[] memory enabledRewards = distributor.enabledRewards(account, rewarded); 52 | assertEq(enabledRewards.length, n); 53 | 54 | bool wasDisabled = distributor.disableReward(rewarded, enabledRewards[index], false); 55 | 56 | assertFalse(distributor.isRewardEnabled(account, rewarded, enabledRewards[index])); 57 | assertTrue(wasDisabled); 58 | assertFalse(distributor.disableReward(rewarded, enabledRewards[index], false)); 59 | } 60 | 61 | function test_BalanceOf(address account, address rewarded, uint256 balance) external { 62 | distributor.setAccountBalance(account, rewarded, balance); 63 | assertEq(distributor.balanceOf(account, rewarded), balance); 64 | } 65 | 66 | function test_RewardAmountCurrent( 67 | address rewarded, 68 | address reward, 69 | uint48 blockTimestamp, 70 | uint128 amount 71 | ) external { 72 | uint48 epoch = distributor.getEpoch(blockTimestamp); 73 | distributor.setDistributionAmount(rewarded, reward, epoch, amount); 74 | vm.warp(blockTimestamp); 75 | assertEq(distributor.rewardAmount(rewarded, reward), amount); 76 | } 77 | 78 | function test_RewardAmount(address rewarded, address reward, uint48 epoch, uint128 amount) external { 79 | distributor.setDistributionAmount(rewarded, reward, epoch, amount); 80 | assertEq(distributor.rewardAmount(rewarded, reward, epoch), amount); 81 | } 82 | 83 | function test_totalRewardedEligible(address rewarded, address reward, uint256 totalEligible) external { 84 | distributor.setDistributionTotals(rewarded, reward, totalEligible, 0, 0); 85 | assertEq(distributor.totalRewardedEligible(rewarded, reward), totalEligible); 86 | } 87 | 88 | function test_totalRewardRegistered(address rewarded, address reward, uint128 totalRegistered) external { 89 | distributor.setDistributionTotals(rewarded, reward, 0, totalRegistered, 0); 90 | assertEq(distributor.totalRewardRegistered(rewarded, reward), totalRegistered); 91 | } 92 | 93 | function test_totalRewardClaimed(address rewarded, address reward, uint128 totalClaimed) external { 94 | distributor.setDistributionTotals(rewarded, reward, 0, 0, totalClaimed); 95 | assertEq(distributor.totalRewardClaimed(rewarded, reward), totalClaimed); 96 | } 97 | 98 | function test_Epoch(uint48 timestamp) external { 99 | timestamp = uint48(bound(timestamp, 0, type(uint48).max - distributor.EPOCH_DURATION() - 1)); 100 | vm.warp(timestamp); 101 | 102 | assertEq(distributor.getEpoch(timestamp), distributor.currentEpoch()); 103 | assertEq(distributor.currentEpoch(), timestamp / distributor.EPOCH_DURATION()); 104 | assertEq( 105 | distributor.getEpochStartTimestamp(distributor.currentEpoch()), 106 | distributor.currentEpoch() * distributor.EPOCH_DURATION() 107 | ); 108 | assertEq( 109 | distributor.getEpochEndTimestamp(distributor.currentEpoch()), 110 | distributor.getEpochStartTimestamp(distributor.currentEpoch()) + distributor.EPOCH_DURATION() 111 | ); 112 | } 113 | 114 | function test_EpochHasntStarted_TimeElapsedInEpoch( 115 | uint48 epoch, 116 | uint48 lastUpdated, 117 | uint256 blockTimestamp 118 | ) external { 119 | epoch = uint48(bound(epoch, 1, type(uint48).max / distributor.EPOCH_DURATION())); 120 | blockTimestamp = bound(blockTimestamp, 0, distributor.getEpochStartTimestamp(epoch) - 1); 121 | lastUpdated = uint48(bound(lastUpdated, 0, blockTimestamp)); 122 | 123 | vm.warp(blockTimestamp); 124 | assertEq(distributor.getTimeElapsedInEpoch(epoch, lastUpdated), 0); 125 | } 126 | 127 | function test_EpochIsOngoing_TimeElapsedInEpoch( 128 | uint48 epoch, 129 | uint48 lastUpdated, 130 | uint256 blockTimestamp 131 | ) external { 132 | epoch = uint48(bound(epoch, 1, type(uint48).max / distributor.EPOCH_DURATION())); 133 | blockTimestamp = bound( 134 | blockTimestamp, distributor.getEpochStartTimestamp(epoch), distributor.getEpochEndTimestamp(epoch) - 1 135 | ); 136 | lastUpdated = uint48(bound(lastUpdated, 0, blockTimestamp)); 137 | 138 | vm.warp(blockTimestamp); 139 | 140 | if (lastUpdated > distributor.getEpochStartTimestamp(epoch)) { 141 | assertEq(distributor.getTimeElapsedInEpoch(epoch, lastUpdated), block.timestamp - lastUpdated); 142 | } else { 143 | assertEq( 144 | distributor.getTimeElapsedInEpoch(epoch, lastUpdated), 145 | block.timestamp - distributor.getEpochStartTimestamp(epoch) 146 | ); 147 | } 148 | } 149 | 150 | function test_EpochHasEnded_TimeElapsedInEpoch(uint48 epoch, uint48 lastUpdated, uint256 blockTimestamp) external { 151 | epoch = uint48(bound(epoch, 1, type(uint48).max / distributor.EPOCH_DURATION())); 152 | blockTimestamp = bound(blockTimestamp, distributor.getEpochEndTimestamp(epoch), type(uint48).max); 153 | lastUpdated = uint48(bound(lastUpdated, 0, distributor.getEpochEndTimestamp(epoch))); 154 | 155 | vm.warp(blockTimestamp); 156 | 157 | if (lastUpdated > distributor.getEpochStartTimestamp(epoch)) { 158 | assertEq( 159 | distributor.getTimeElapsedInEpoch(epoch, lastUpdated), 160 | distributor.getEpochEndTimestamp(epoch) - lastUpdated 161 | ); 162 | } else { 163 | assertEq(distributor.getTimeElapsedInEpoch(epoch, lastUpdated), distributor.EPOCH_DURATION()); 164 | } 165 | } 166 | 167 | function test_msgSender(address caller) external { 168 | vm.assume(caller != address(0) && caller != address(evc)); 169 | 170 | vm.startPrank(caller); 171 | assertEq(distributor.msgSender(), caller); 172 | 173 | vm.startPrank(caller); 174 | bytes memory result = 175 | evc.call(address(distributor), caller, 0, abi.encodeWithSelector(distributor.msgSender.selector)); 176 | assertEq(abi.decode(result, (address)), caller); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /test/utils/MockController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "openzeppelin-contracts/token/ERC20/ERC20.sol"; 5 | import "evc/interfaces/IEthereumVaultConnector.sol"; 6 | import "evc/interfaces/IVault.sol"; 7 | 8 | contract MockController { 9 | IEVC public immutable evc; 10 | 11 | constructor(IEVC _evc) { 12 | evc = _evc; 13 | } 14 | 15 | function checkAccountStatus(address, address[] calldata) external pure returns (bytes4) { 16 | return IVault.checkAccountStatus.selector; 17 | } 18 | 19 | function liquidateCollateralShares( 20 | address vault, 21 | address liquidated, 22 | address liquidator, 23 | uint256 shares 24 | ) external { 25 | // Control the collateral in order to transfer shares from the violator's vault to the liquidator. 26 | bytes memory result = 27 | evc.controlCollateral(vault, liquidated, 0, abi.encodeCall(ERC20.transfer, (liquidator, shares))); 28 | 29 | require(result.length == 0 || abi.decode(result, (bool)), "MockController: liquidateCollateralShares failed"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/utils/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "openzeppelin-contracts/token/ERC20/ERC20.sol"; 5 | import "evc/interfaces/IEthereumVaultConnector.sol"; 6 | import "../../src/interfaces/IBalanceTracker.sol"; 7 | 8 | /// @title IBalanceForwarder 9 | /// @author Euler Labs (https://www.eulerlabs.com/) 10 | /// @notice This interface defines the functions for enabling and disabling balance forwarding. 11 | interface IBalanceForwarder { 12 | /// @notice Enables balance forwarding for the msg.sender 13 | /// @dev Only the msg.sender can enable balance forwarding for itself 14 | /// @dev Should call the IBalanceTracker hook with the current account's balance 15 | function enableBalanceForwarding() external; 16 | 17 | /// @notice Disables balance forwarding for the msg.sender 18 | /// @dev Only the msg.sender can disable balance forwarding for itself 19 | /// @dev Should call the IBalanceTracker hook with the account's balance of 0 20 | function disableBalanceForwarding() external; 21 | } 22 | 23 | contract MockERC20 is ERC20 { 24 | constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) {} 25 | 26 | function mint(address account, uint256 amount) external { 27 | _mint(account, amount); 28 | } 29 | } 30 | 31 | contract MockERC20Malicious is MockERC20 { 32 | constructor(string memory _name, string memory _symbol) MockERC20(_name, _symbol) {} 33 | 34 | function transferFrom(address from, address to, uint256 amount) public override returns (bool) { 35 | return super.transferFrom(from, to, amount - 1); 36 | } 37 | } 38 | 39 | contract MockERC20BalanceForwarder is MockERC20, IBalanceForwarder { 40 | IEVC public immutable evc; 41 | IBalanceTracker public immutable balanceTracker; 42 | 43 | mapping(address account => bool enabled) internal forwardingEnabled; 44 | 45 | constructor( 46 | IEVC _evc, 47 | IBalanceTracker _balanceTracker, 48 | string memory _name, 49 | string memory _symbol 50 | ) MockERC20(_name, _symbol) { 51 | evc = _evc; 52 | balanceTracker = _balanceTracker; 53 | } 54 | 55 | function enableBalanceForwarding() external { 56 | address account = _msgSender(); 57 | forwardingEnabled[account] = true; 58 | balanceTracker.balanceTrackerHook(account, balanceOf(account), false); 59 | } 60 | 61 | function disableBalanceForwarding() external { 62 | address account = _msgSender(); 63 | forwardingEnabled[account] = false; 64 | balanceTracker.balanceTrackerHook(account, 0, false); 65 | } 66 | 67 | function _msgSender() internal view virtual override returns (address msgSender) { 68 | msgSender = msg.sender; 69 | 70 | if (msgSender == address(evc)) { 71 | (msgSender,) = evc.getCurrentOnBehalfOfAccount(address(0)); 72 | } 73 | 74 | return msgSender; 75 | } 76 | 77 | function _update(address from, address to, uint256 value) internal virtual override { 78 | super._update(from, to, value); 79 | 80 | if (forwardingEnabled[from]) { 81 | balanceTracker.balanceTrackerHook(from, balanceOf(from), evc.isControlCollateralInProgress()); 82 | } 83 | 84 | if (from != to && forwardingEnabled[to]) { 85 | balanceTracker.balanceTrackerHook(to, balanceOf(to), false); 86 | } 87 | } 88 | 89 | function balanceTrackerHookSimulator( 90 | address account, 91 | uint256 newAccountBalance, 92 | bool forfeitRecentReward 93 | ) external { 94 | balanceTracker.balanceTrackerHook(account, newAccountBalance, forfeitRecentReward); 95 | } 96 | } 97 | 98 | contract MockERC20BalanceForwarderMessedUp is MockERC20BalanceForwarder { 99 | constructor( 100 | IEVC _evc, 101 | IBalanceTracker _balanceTracker, 102 | string memory _name, 103 | string memory _symbol 104 | ) MockERC20BalanceForwarder(_evc, _balanceTracker, _name, _symbol) {} 105 | 106 | function _update(address from, address to, uint256 value) internal virtual override { 107 | ERC20._update(from, to, value); 108 | 109 | if (forwardingEnabled[from]) { 110 | balanceTracker.balanceTrackerHook(from, balanceOf(from), false); 111 | } 112 | 113 | // always pass forfeitRecentReward = true when increasing balance to mess up the accounting 114 | if (from != to && forwardingEnabled[to]) { 115 | balanceTracker.balanceTrackerHook(to, balanceOf(to), true); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /test/utils/TestUtils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.24; 4 | 5 | address constant VM_ADDRESS = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D; 6 | address constant CONSOLE = 0x000000000000000000636F6e736F6c652e6c6f67; 7 | address constant CREATE2_FACTORY = 0x4e59b44847b379578588920cA78FbF26c0B4956C; 8 | address constant DEFAULT_TEST_CONTRACT = 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f; 9 | address constant MULTICALL3_ADDRESS = 0xcA11bde05977b3631167028862bE2a173976CA11; 10 | address constant FIRST_DEPLOYED_CONTRACT = 0x2e234DAe75C793f67A35089C9d99245E1C58470b; 11 | 12 | /// @dev Exclude Foundry precompiles, predeploys and addresses with already deployed code. 13 | /// These addresses can make certain test cases that call/mockCall to them fail. 14 | /// List of Foundry precompiles: https://book.getfoundry.sh/misc/precompile-registry 15 | function boundAddr(address addr) view returns (address) { 16 | if ( 17 | uint160(addr) < 10 || addr == VM_ADDRESS || addr == CONSOLE || addr == CREATE2_FACTORY 18 | || addr == DEFAULT_TEST_CONTRACT || addr == MULTICALL3_ADDRESS || addr == FIRST_DEPLOYED_CONTRACT 19 | || addr.code.length != 0 20 | ) { 21 | return address(uint160(addr) + 10); 22 | } 23 | 24 | return addr; 25 | } 26 | --------------------------------------------------------------------------------