diff --git a/.github/workflows/check-fast-forward.yml b/.github/workflows/check-fast-forward.yml index bd529a3..95e5fb0 100644 --- a/.github/workflows/check-fast-forward.yml +++ b/.github/workflows/check-fast-forward.yml @@ -37,7 +37,7 @@ jobs: fi - name: Fast forwarding - uses: sequoia-pgp/fast-forward@v1 + uses: sequoia-pgp/fast-forward@ea7628bedcb0b0b96e94383ada458d812fca4979 # v1.0.0 with: merge: false comment: on-error # always | never | on-error diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aecb7fa..d740db7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,9 @@ on: - devel - bepolia +permissions: + contents: read + jobs: ci: runs-on: @@ -25,17 +28,21 @@ jobs: command: forge coverage --fuzz-runs 256 --report lcov --report-file ./test-forge-cover.txt summary: "## Coverage result" name: ${{ matrix.job_name }} + env: + BERACHAIN_RPC_URL: ${{ secrets.BERACHAIN_RPC_URL }} steps: - name: Check out the repo - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: recursive - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 + uses: foundry-rs/foundry-toolchain@c7450ba673e133f5ee30098b3b54f444d3a2ca2d # v1.8.0 - name: Install Bun - uses: oven-sh/setup-bun@v1 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: "1.3.14" - name: Install the Node.js dependencies run: bun install @@ -50,7 +57,7 @@ jobs: - if: ${{ matrix.job_name == 'coverage' }} name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./test-forge-cover.txt diff --git a/.github/workflows/fast-forward.yml b/.github/workflows/fast-forward.yml index ebfe1d5..884d089 100644 --- a/.github/workflows/fast-forward.yml +++ b/.github/workflows/fast-forward.yml @@ -41,7 +41,7 @@ jobs: exit 1 fi - name: Fast forwarding - uses: sequoia-pgp/fast-forward@v1 + uses: sequoia-pgp/fast-forward@ea7628bedcb0b0b96e94383ada458d812fca4979 # v1.0.0 with: merge: true comment: on-error # always | never | on-error diff --git a/.github/workflows/slither.yaml b/.github/workflows/slither.yaml index d720fa7..a843510 100644 --- a/.github/workflows/slither.yaml +++ b/.github/workflows/slither.yaml @@ -3,21 +3,24 @@ name: Slither Analysis on: pull_request: +permissions: + contents: read + jobs: analyze: runs-on: ubuntu-latest steps: # Add Docker Hub authentication with rate limiting configuration - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_PASSWORD }} registry: docker.io - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: crytic/slither-action@v0.4.1 + - uses: crytic/slither-action@4fd765aeef19915d04ddf0be90c2930036a774d8 # v0.4.1 with: fail-on: none diff --git a/.github/workflows/tests-merge.yml b/.github/workflows/tests-merge.yml index 40ba446..2d8cc6d 100644 --- a/.github/workflows/tests-merge.yml +++ b/.github/workflows/tests-merge.yml @@ -7,22 +7,29 @@ on: - bepolia - devel +permissions: + contents: read + jobs: test: runs-on: labels: ubuntu-latest timeout-minutes: 30 + env: + BERACHAIN_RPC_URL: ${{ secrets.BERACHAIN_RPC_URL }} steps: - name: Check out the repo - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: recursive - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 + uses: foundry-rs/foundry-toolchain@c7450ba673e133f5ee30098b3b54f444d3a2ca2d # v1.8.0 - name: Install Bun - uses: oven-sh/setup-bun@v1 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: "1.3.14" - name: Install the Node.js dependencies run: bun install diff --git a/.github/workflows/tests-pr.yml b/.github/workflows/tests-pr.yml index 460e22a..b438ebb 100644 --- a/.github/workflows/tests-pr.yml +++ b/.github/workflows/tests-pr.yml @@ -3,22 +3,29 @@ name: Tests-PR on: pull_request: +permissions: + contents: read + jobs: test: runs-on: labels: ubuntu-latest timeout-minutes: 30 + env: + BERACHAIN_RPC_URL: ${{ secrets.BERACHAIN_RPC_URL }} steps: - name: Check out the repo - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: recursive - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 + uses: foundry-rs/foundry-toolchain@c7450ba673e133f5ee30098b3b54f444d3a2ca2d # v1.8.0 - name: Install Bun - uses: oven-sh/setup-bun@v1 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: "1.3.14" - name: Install the Node.js dependencies run: bun install diff --git a/foundry.toml b/foundry.toml index a60f27b..35d2f27 100644 --- a/foundry.toml +++ b/foundry.toml @@ -59,7 +59,7 @@ mainnet = "https://eth-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}" optimism = "https://optimism-mainnet.infura.io/v3/${API_KEY_INFURA}" polygon = "https://polygon-mainnet.infura.io/v3/${API_KEY_INFURA}" sepolia = "https://sepolia.infura.io/v3/${API_KEY_INFURA}" -berachain = "https://rpc.berachain.com" +berachain = "${BERACHAIN_RPC_URL}" bepolia = "https://bepolia.rpc.berachain.com/" [lint] diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..0e76fde --- /dev/null +++ b/renovate.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["local>berachain/renovate-config"], + "baseBranches": ["devel"] +} diff --git a/script/base/BaseUpgrade.s.sol b/script/base/BaseUpgrade.s.sol new file mode 100644 index 0000000..800a3c5 --- /dev/null +++ b/script/base/BaseUpgrade.s.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { console2 } from "forge-std/Script.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { UpgradeableBeacon } from "solady/src/utils/UpgradeableBeacon.sol"; + +import { BaseDeployScript } from "./BaseDeploy.s.sol"; +import { AddressBook } from "./AddressBook.sol"; + +abstract contract BaseUpgradeScript is BaseDeployScript, AddressBook { + function run() public pure { + console2.log("Please run specific function."); + } + + function deployNewImplementation() public broadcast { + address impl = _deployNewImplementation(); + console2.log("New implementation address:", impl); + } + + function _deployNewImplementation() internal virtual returns (address); +} + +abstract contract BaseERC1967UpgradeScript is BaseUpgradeScript { + /// @dev Only for testnet or test purposes. + function upgradeToAndCallTestnet(bytes memory callSignature) public broadcast { + address newImpl = _deployNewImplementation(); + console2.log("New implementation address:", newImpl); + UUPSUpgradeable(_proxyAddress()).upgradeToAndCall(newImpl, callSignature); + console2.log("Contract upgraded successfully"); + } + + function _proxyAddress() internal view virtual returns (address); +} + +abstract contract BaseBeaconUpgradeScript is BaseUpgradeScript { + /// @dev Only for testnet or test purposes. + function upgradeToTestnet() public broadcast { + address newImpl = _deployNewImplementation(); + console2.log("New implementation address:", newImpl); + UpgradeableBeacon(_beaconAddress()).upgradeTo(newImpl); + console2.log("Contract upgraded successfully"); + } + + function _beaconAddress() internal view virtual returns (address); +} diff --git a/script/base/Storage.sol b/script/base/Storage.sol index 8ec5cb1..da27d07 100644 --- a/script/base/Storage.sol +++ b/script/base/Storage.sol @@ -19,11 +19,12 @@ import { BeaconDeposit } from "src/pol/BeaconDeposit.sol"; import { BGTIncentiveDistributor } from "src/pol/rewards/BGTIncentiveDistributor.sol"; import { WBERAStakerVault } from "src/pol/WBERAStakerVault.sol"; import { WBERAStakerVaultWithdrawalRequest } from "src/pol/WBERAStakerVaultWithdrawalRequest.sol"; -import { BGTIncentiveFeeCollector } from "src/pol/BGTIncentiveFeeCollector.sol"; +import { IncentivesCollector } from "src/pol/IncentivesCollector.sol"; import { LSTStakerVaultFactory } from "src/pol/lst/LSTStakerVaultFactory.sol"; import { LSTStakerVault } from "src/pol/lst/LSTStakerVault.sol"; import { LSTStakerVaultWithdrawalRequest } from "src/pol/lst/LSTStakerVaultWithdrawalRequest.sol"; import { DedicatedEmissionStreamManager } from "src/pol/rewards/DedicatedEmissionStreamManager.sol"; +import { RewardVaultHelper } from "src/pol/rewards/RewardVaultHelper.sol"; abstract contract Storage { BGT internal bgt; @@ -44,9 +45,10 @@ abstract contract Storage { BGTIncentiveDistributor internal bgtIncentiveDistributor; WBERAStakerVault internal wberaStakerVault; WBERAStakerVaultWithdrawalRequest internal wberaStakerVaultWithdrawalRequest; - BGTIncentiveFeeCollector internal bgtIncentiveFeeCollector; + IncentivesCollector internal incentivesCollector; LSTStakerVaultFactory internal lstStakerVaultFactory; LSTStakerVault internal lstStakerVault; LSTStakerVaultWithdrawalRequest internal lstStakerVaultWithdrawalRequest; DedicatedEmissionStreamManager internal dedicatedEmissionStreamManager; + RewardVaultHelper internal rewardVaultHelper; } diff --git a/script/devnet/DeployDevnet.s.sol b/script/devnet/DeployDevnet.s.sol new file mode 100644 index 0000000..3e6ec3a --- /dev/null +++ b/script/devnet/DeployDevnet.s.sol @@ -0,0 +1,895 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { console2 } from "forge-std/Script.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { UpgradeableBeacon } from "solady/src/utils/UpgradeableBeacon.sol"; + +import { BaseDeployScript } from "script/base/BaseDeploy.s.sol"; +import { RBAC } from "script/base/RBAC.sol"; +import { Storage } from "script/base/Storage.sol"; +import { AddressBook } from "script/base/AddressBook.sol"; +import { ConfigPOL } from "script/pol/logic/ConfigPOL.sol"; + +import { BGT } from "src/pol/BGT.sol"; +import { BGTDeployer } from "src/pol/BGTDeployer.sol"; +import { POLDeployer } from "src/pol/POLDeployer.sol"; +import { BGTFeeDeployer } from "src/pol/BGTFeeDeployer.sol"; +import { BGTIncentiveDistributor } from "src/pol/rewards/BGTIncentiveDistributor.sol"; +import { BGTIncentiveDistributorDeployer } from "src/pol/BGTIncentiveDistributorDeployer.sol"; +import { IncentivesCollectorDeployer } from "src/pol/IncentivesCollectorDeployer.sol"; +import { WBERAStakerVault } from "src/pol/WBERAStakerVault.sol"; +import { WBERAStakerVaultWithdrawalRequest } from "src/pol/WBERAStakerVaultWithdrawalRequest.sol"; +import { WBERAStakerWithdrawReqDeployer } from "src/pol/WBERAStakerWithdrawReqDeployer.sol"; +import { IncentivesCollector } from "src/pol/IncentivesCollector.sol"; +import { DedicatedEmissionStreamManagerDeployer } from "src/pol/DedicatedEmissionStreamManagerDeployer.sol"; +import { DedicatedEmissionStreamManager } from "src/pol/rewards/DedicatedEmissionStreamManager.sol"; +import { RewardVaultHelperDeployer } from "src/pol/RewardVaultHelperDeployer.sol"; +import { RewardVaultHelper } from "src/pol/rewards/RewardVaultHelper.sol"; +import { BeraChef } from "src/pol/rewards/BeraChef.sol"; +import { BlockRewardController } from "src/pol/rewards/BlockRewardController.sol"; +import { Distributor } from "src/pol/rewards/Distributor.sol"; +import { RewardVaultFactory } from "src/pol/rewards/RewardVaultFactory.sol"; +import { RewardVault } from "src/pol/rewards/RewardVault.sol"; +import { BGTStaker } from "src/pol/BGTStaker.sol"; +import { FeeCollector } from "src/pol/FeeCollector.sol"; +import { WBERA } from "src/WBERA.sol"; + +import { HoneyDeployer } from "src/honey/HoneyDeployer.sol"; +import { Honey } from "src/honey/Honey.sol"; +import { HoneyFactory } from "src/honey/HoneyFactory.sol"; +import { HoneyFactoryReader } from "src/honey/HoneyFactoryReader.sol"; + +import { PeggedPriceOracle } from "src/extras/PeggedPriceOracle.sol"; +import { PythPriceOracle } from "src/extras/PythPriceOracle.sol"; +import { PythPriceOracleDeployer } from "src/extras/PythPriceOracleDeployer.sol"; +import { RootPriceOracle } from "src/extras/RootPriceOracle.sol"; +import { RootPriceOracleDeployer } from "src/extras/RootPriceOracleDeployer.sol"; + +/// @notice Single-shot devnet deployment script. +/// @dev Deploys all system contracts, configures them, and transfers all ownership to `owner`. +/// Governance contracts (BerachainGovernance, TimeLock) are intentionally omitted. +/// +/// Prerequisites: +/// - Broadcaster must have enough BERA to wrap 10 BERA → WBERA for the WBERAStakerVault initial deposit. +/// - WBERA (0x6969...) and BeaconDeposit (0x4242...) must be live at their genesis addresses. +/// +/// Usage: +/// forge script script/devnet/DeployDevnet.s.sol \ +/// --rpc-url $DEVNET_RPC \ +/// --sig "run(address)" \ +/// --sender \ +/// --broadcast +contract DeployDevnetScript is BaseDeployScript, RBAC, Storage, AddressBook, ConfigPOL { + // ─── Genesis addresses ─── + + /// @dev Wrapped BERA genesis address (same on all Berachain networks). + address internal constant WBERA_ADDRESS = 0x6969696969696969696969696969696969696969; + + /// @dev BeaconDeposit genesis address. + address internal constant BEACON_DEPOSIT_ADDRESS = 0x4242424242424242424242424242424242424242; + + // ─── Deployment constants ─── + + /// @dev Payout amount for FeeCollector (WBERA per claim). + uint256 internal constant PAYOUT_AMOUNT_FEE = 5000 ether; + + /// @dev Payout amount for IncentivesCollector (WBERA per claim). + uint256 internal constant PAYOUT_AMOUNT_INCENTIVE = 50_000 ether; + + /// @dev Initial WBERA deposit to WBERAStakerVault to prevent inflation attacks. + uint256 internal constant INITIAL_DEPOSIT_AMOUNT = 10 ether; + + /// @dev Block delay before a queued reward allocation becomes active in BeraChef. + uint64 internal constant REWARD_ALLOCATION_BLOCK_DELAY = 8191; + + // ─── Additional storage not in base Storage + // ─────────────────────────────── + + PeggedPriceOracle internal peggedPriceOracle; + PythPriceOracle internal pythPriceOracle; + RootPriceOracle internal rootPriceOracle; + + // ─── Entry point ─── + + /// @notice Deploy all system contracts and transfer ownership to `owner`. + /// @param owner The address that will own all deployed contracts. May equal msg.sender. + function run(address owner) external broadcast { + require(owner != address(0), "DeployDevnet: owner cannot be zero address"); + + console2.log("\n========== DeployDevnet =========="); + console2.log("Deployer (msg.sender):", msg.sender); + console2.log("Owner (target):", owner); + + _validateCode("BeaconDeposit", BEACON_DEPOSIT_ADDRESS); + _validateCode("WBERA", WBERA_ADDRESS); + + // ── Phase 1: BGT ── + _deployBGT(); + + // ── Phase 2: POL system ── + _deployPOL(); + _deployBGTFees(); + _deployBGTIncentiveDistributor(); + _deployBGTIncentiveFees(); + _deployWBERAStakerWithdrawReq(); + _deployDedicatedEmissionStreamManager(); + _deployRewardVaultHelper(); + + // ── Phase 3: Oracles ── + _deployOracles(); + + // ── Phase 4: Honey ── + _deployHoney(); + + // ── Phase 5: Configure setters ── + _configure(); + + // ── Phase 6: Transfer ownership to owner ── + _transferAllOwnership(owner); + + console2.log("\n========== Deployment complete =========="); + _logAddresses(); + } + + // ─── Phase 1: BGT ─── + + function _deployBGT() internal { + console2.log("\n--- [1/4] BGT ---"); + + // msg.sender is the initial owner so it can call setMinter/setStaker/whitelistSender later. + BGTDeployer bgtDeployer = new BGTDeployer(msg.sender, _salt(type(BGT).creationCode)); + bgt = bgtDeployer.bgt(); + _checkDeploymentAddress("BGT", address(bgt), _polAddresses.bgt); + } + + // ─── Phase 2: POL system ─── + + function _deployPOL() internal { + console2.log("\n--- [2/4] PoL ---"); + + // msg.sender passed as governance so it holds DEFAULT_ADMIN_ROLE for later setters. + POLDeployer polDeployer = new POLDeployer( + address(bgt), + msg.sender, + _saltsForProxy(type(BeraChef).creationCode), + _saltsForProxy(type(BlockRewardController).creationCode), + _saltsForProxy(type(Distributor).creationCode), + _saltsForProxy(type(RewardVaultFactory).creationCode), + _salt(type(RewardVault).creationCode) + ); + + beraChef = polDeployer.beraChef(); + _checkDeploymentAddress("BeraChef", address(beraChef), _polAddresses.beraChef); + + blockRewardController = polDeployer.blockRewardController(); + _checkDeploymentAddress( + "BlockRewardController", address(blockRewardController), _polAddresses.blockRewardController + ); + + distributor = polDeployer.distributor(); + _checkDeploymentAddress("Distributor", address(distributor), _polAddresses.distributor); + + rewardVaultFactory = polDeployer.rewardVaultFactory(); + _checkDeploymentAddress("RewardVaultFactory", address(rewardVaultFactory), _polAddresses.rewardVaultFactory); + + // Grant operational roles to deployer. + // VAULT_PAUSER_ROLE's admin is VAULT_MANAGER_ROLE, so we must hold VAULT_MANAGER_ROLE + // to be able to transfer VAULT_PAUSER_ROLE in the ownership phase. + RBAC.AccountDescription memory deployer = RBAC.AccountDescription({ name: "deployer", addr: msg.sender }); + _grantRole( + RBAC.RoleDescription({ + contractName: "RewardVaultFactory", + contractAddr: address(rewardVaultFactory), + name: "VAULT_MANAGER_ROLE", + role: rewardVaultFactory.VAULT_MANAGER_ROLE() + }), + deployer + ); + _grantRole( + RBAC.RoleDescription({ + contractName: "RewardVaultFactory", + contractAddr: address(rewardVaultFactory), + name: "VAULT_PAUSER_ROLE", + role: rewardVaultFactory.VAULT_PAUSER_ROLE() + }), + deployer + ); + // MANAGER_ROLE on Distributor (needed to transfer it in the ownership phase). + _grantRole( + RBAC.RoleDescription({ + contractName: "Distributor", + contractAddr: address(distributor), + name: "MANAGER_ROLE", + role: distributor.MANAGER_ROLE() + }), + deployer + ); + } + + function _deployBGTFees() internal { + console2.log("\n--- [2/4] BGT fees ---"); + + BGTFeeDeployer feeDeployer = new BGTFeeDeployer( + address(bgt), + msg.sender, + WBERA_ADDRESS, + _saltsForProxy(type(BGTStaker).creationCode), + _saltsForProxy(type(FeeCollector).creationCode), + PAYOUT_AMOUNT_FEE + ); + + bgtStaker = feeDeployer.bgtStaker(); + _checkDeploymentAddress("BGTStaker", address(bgtStaker), _polAddresses.bgtStaker); + + feeCollector = feeDeployer.feeCollector(); + _checkDeploymentAddress("FeeCollector", address(feeCollector), _polAddresses.feeCollector); + + // PAUSER_ROLE's admin is MANAGER_ROLE on FeeCollector. + RBAC.AccountDescription memory deployer = RBAC.AccountDescription({ name: "deployer", addr: msg.sender }); + _grantRole( + RBAC.RoleDescription({ + contractName: "FeeCollector", + contractAddr: address(feeCollector), + name: "MANAGER_ROLE", + role: feeCollector.MANAGER_ROLE() + }), + deployer + ); + _grantRole( + RBAC.RoleDescription({ + contractName: "FeeCollector", + contractAddr: address(feeCollector), + name: "PAUSER_ROLE", + role: feeCollector.PAUSER_ROLE() + }), + deployer + ); + } + + function _deployBGTIncentiveDistributor() internal { + console2.log("\n--- [2/4] BGTIncentiveDistributor ---"); + + BGTIncentiveDistributorDeployer bgtIncDistDeployer = new BGTIncentiveDistributorDeployer( + msg.sender, _saltsForProxy(type(BGTIncentiveDistributor).creationCode) + ); + + bgtIncentiveDistributor = bgtIncDistDeployer.bgtIncentiveDistributor(); + _checkDeploymentAddress( + "BGTIncentiveDistributor", address(bgtIncentiveDistributor), _polAddresses.bgtIncentiveDistributor + ); + + // PAUSER_ROLE's admin is MANAGER_ROLE on BGTIncentiveDistributor. + RBAC.AccountDescription memory deployer = RBAC.AccountDescription({ name: "deployer", addr: msg.sender }); + _grantRole( + RBAC.RoleDescription({ + contractName: "BGTIncentiveDistributor", + contractAddr: address(bgtIncentiveDistributor), + name: "MANAGER_ROLE", + role: bgtIncentiveDistributor.MANAGER_ROLE() + }), + deployer + ); + _grantRole( + RBAC.RoleDescription({ + contractName: "BGTIncentiveDistributor", + contractAddr: address(bgtIncentiveDistributor), + name: "PAUSER_ROLE", + role: bgtIncentiveDistributor.PAUSER_ROLE() + }), + deployer + ); + } + + function _deployBGTIncentiveFees() internal { + console2.log("\n--- [2/4] BGTIncentiveFees ---"); + + // Wrap BERA → WBERA; the IncentivesCollectorDeployer will pull INITIAL_DEPOSIT_AMOUNT via transferFrom. + WBERA(payable(WBERA_ADDRESS)).deposit{ value: INITIAL_DEPOSIT_AMOUNT }(); + + bytes memory args = abi.encode( + msg.sender, // governance (DEFAULT_ADMIN_ROLE on both vaults) + msg.sender, // tokenProvider (will transferFrom this address) + PAYOUT_AMOUNT_INCENTIVE, + _saltsForProxy(type(WBERAStakerVault).creationCode), + _saltsForProxy(type(IncentivesCollector).creationCode) + ); + + // Must approve before deploying because the constructor calls transferFrom(tokenProvider). + address predictedDeployer = _predictAddressWithArgs(type(IncentivesCollectorDeployer).creationCode, args); + IERC20(WBERA_ADDRESS).approve(predictedDeployer, INITIAL_DEPOSIT_AMOUNT); + + IncentivesCollectorDeployer bgtIncFeeDeployer = IncentivesCollectorDeployer( + _deployWithArgs( + "IncentivesCollectorDeployer", type(IncentivesCollectorDeployer).creationCode, args, predictedDeployer + ) + ); + + wberaStakerVault = bgtIncFeeDeployer.wberaStakerVault(); + _checkDeploymentAddress("WBERAStakerVault", address(wberaStakerVault), _polAddresses.wberaStakerVault); + + incentivesCollector = bgtIncFeeDeployer.incentivesCollector(); + _checkDeploymentAddress("IncentivesCollector", address(incentivesCollector), _polAddresses.incentivesCollector); + + // PAUSER_ROLE's admin is MANAGER_ROLE on both contracts. + RBAC.AccountDescription memory deployer = RBAC.AccountDescription({ name: "deployer", addr: msg.sender }); + _grantRole( + RBAC.RoleDescription({ + contractName: "WBERAStakerVault", + contractAddr: address(wberaStakerVault), + name: "MANAGER_ROLE", + role: wberaStakerVault.MANAGER_ROLE() + }), + deployer + ); + _grantRole( + RBAC.RoleDescription({ + contractName: "WBERAStakerVault", + contractAddr: address(wberaStakerVault), + name: "PAUSER_ROLE", + role: wberaStakerVault.PAUSER_ROLE() + }), + deployer + ); + _grantRole( + RBAC.RoleDescription({ + contractName: "IncentivesCollector", + contractAddr: address(incentivesCollector), + name: "MANAGER_ROLE", + role: incentivesCollector.MANAGER_ROLE() + }), + deployer + ); + _grantRole( + RBAC.RoleDescription({ + contractName: "IncentivesCollector", + contractAddr: address(incentivesCollector), + name: "PAUSER_ROLE", + role: incentivesCollector.PAUSER_ROLE() + }), + deployer + ); + } + + function _deployWBERAStakerWithdrawReq() internal { + console2.log("\n--- [2/4] WBERAStakerVaultWithdrawalRequest ---"); + + WBERAStakerWithdrawReqDeployer wsrDeployer = new WBERAStakerWithdrawReqDeployer( + msg.sender, address(wberaStakerVault), _saltsForProxy(type(WBERAStakerVaultWithdrawalRequest).creationCode) + ); + + wberaStakerVaultWithdrawalRequest = wsrDeployer.wberaStakerVaultWithdrawalRequest(); + _checkDeploymentAddress( + "WBERAStakerVaultWithdrawalRequest", + address(wberaStakerVaultWithdrawalRequest), + _polAddresses.wberaStakerVaultWithdrawalRequest + ); + + // Wire the NFT withdrawal-request contract on the staker vault. + wberaStakerVault.setWithdrawalRequests721(address(wberaStakerVaultWithdrawalRequest)); + require( + address(wberaStakerVault.withdrawalRequests721()) == address(wberaStakerVaultWithdrawalRequest), + "DeployDevnet: withdrawalRequests721 not set on WBERAStakerVault" + ); + console2.log("Wired WBERAStakerVaultWithdrawalRequest on WBERAStakerVault"); + } + + function _deployDedicatedEmissionStreamManager() internal { + console2.log("\n--- [2/4] DedicatedEmissionStreamManager ---"); + + DedicatedEmissionStreamManagerDeployer desDeployer = new DedicatedEmissionStreamManagerDeployer( + msg.sender, + address(distributor), + address(beraChef), + _saltsForProxy(type(DedicatedEmissionStreamManager).creationCode) + ); + + dedicatedEmissionStreamManager = desDeployer.dedicatedEmissionStreamManager(); + _checkDeploymentAddress( + "DedicatedEmissionStreamManager", + address(dedicatedEmissionStreamManager), + _polAddresses.dedicatedEmissionStreamManager + ); + } + + function _deployRewardVaultHelper() internal { + console2.log("\n--- [2/4] RewardVaultHelper ---"); + + RewardVaultHelperDeployer rvhDeployer = + new RewardVaultHelperDeployer(msg.sender, _saltsForProxy(type(RewardVaultHelper).creationCode)); + + rewardVaultHelper = rvhDeployer.rewardVaultHelper(); + _checkDeploymentAddress("RewardVaultHelper", address(rewardVaultHelper), _polAddresses.rewardVaultHelper); + } + + // ─── Phase 3: Oracles ─── + + function _deployOracles() internal { + console2.log("\n--- [3/4] Oracles ---"); + + // PeggedPriceOracle: stateless, no proxy, pure CREATE2. + _predictAddress(type(PeggedPriceOracle).creationCode); + peggedPriceOracle = PeggedPriceOracle( + _deploy("PeggedPriceOracle", type(PeggedPriceOracle).creationCode, _oraclesAddresses.peggedPriceOracle) + ); + + // PythPriceOracle: proxied, msg.sender gets DEFAULT_ADMIN_ROLE. + PythPriceOracleDeployer pythDeployer = + new PythPriceOracleDeployer(msg.sender, _saltsForProxy(type(PythPriceOracle).creationCode)); + pythPriceOracle = pythDeployer.oracle(); + _checkDeploymentAddress("PythPriceOracle", address(pythPriceOracle), _oraclesAddresses.pythPriceOracle); + + // RootPriceOracle: no proxy, CREATE2, msg.sender gets DEFAULT_ADMIN_ROLE. + RootPriceOracleDeployer rootDeployer = + new RootPriceOracleDeployer(msg.sender, _salt(type(RootPriceOracle).creationCode)); + rootPriceOracle = rootDeployer.rootPriceOracle(); + _checkDeploymentAddress("RootPriceOracle", address(rootPriceOracle), _oraclesAddresses.rootPriceOracle); + + // Grant MANAGER_ROLE to msg.sender on both oracles so it can call their restricted setters in _configure(). + RBAC.AccountDescription memory deployer = RBAC.AccountDescription({ name: "deployer", addr: msg.sender }); + _grantRole( + RBAC.RoleDescription({ + contractName: "PythPriceOracle", + contractAddr: address(pythPriceOracle), + name: "MANAGER_ROLE", + role: pythPriceOracle.MANAGER_ROLE() + }), + deployer + ); + _grantRole( + RBAC.RoleDescription({ + contractName: "RootPriceOracle", + contractAddr: address(rootPriceOracle), + name: "MANAGER_ROLE", + role: rootPriceOracle.MANAGER_ROLE() + }), + deployer + ); + } + + // ─── Phase 4: Honey ─── + + function _deployHoney() internal { + console2.log("\n--- [4/6] Honey ---"); + + HoneyDeployer honeyDeployer = new HoneyDeployer( + msg.sender, + address(feeCollector), + address(feeCollector), + _saltsForProxy(type(Honey).creationCode), + _saltsForProxy(type(HoneyFactory).creationCode), + _saltsForProxy(type(HoneyFactoryReader).creationCode), + address(peggedPriceOracle) + ); + + honey = honeyDeployer.honey(); + _checkDeploymentAddress("Honey", address(honey), _honeyAddresses.honey); + + honeyFactory = honeyDeployer.honeyFactory(); + _checkDeploymentAddress("HoneyFactory", address(honeyFactory), _honeyAddresses.honeyFactory); + + honeyFactoryReader = honeyDeployer.honeyFactoryReader(); + _checkDeploymentAddress("HoneyFactoryReader", address(honeyFactoryReader), _honeyAddresses.honeyFactoryReader); + + require(honeyFactory.feeReceiver() == address(feeCollector), "DeployDevnet: fee receiver not set"); + require(honeyFactory.polFeeCollector() == address(feeCollector), "DeployDevnet: pol fee collector not set"); + + // PAUSER_ROLE's admin is MANAGER_ROLE on HoneyFactory. + RBAC.AccountDescription memory deployer = RBAC.AccountDescription({ name: "deployer", addr: msg.sender }); + _grantRole( + RBAC.RoleDescription({ + contractName: "HoneyFactory", + contractAddr: address(honeyFactory), + name: "MANAGER_ROLE", + role: honeyFactory.MANAGER_ROLE() + }), + deployer + ); + _grantRole( + RBAC.RoleDescription({ + contractName: "HoneyFactory", + contractAddr: address(honeyFactory), + name: "PAUSER_ROLE", + role: honeyFactory.PAUSER_ROLE() + }), + deployer + ); + } + + // ─── Phase 5: Configure setters ─── + + function _configure() internal { + console2.log("\n--- [5/6] Configure ---"); + + // BGT: set staker, whitelist distributor as sender, set minter. + _setBGTAddresses(address(bgtStaker), address(distributor), address(blockRewardController)); + + // BlockRewardController V2 reinitializer: wires WBERA and clears deprecated rate/boost slots. + // POLDeployer ran the V1 initializer; rates are now constants and WBERA must be set explicitly. + blockRewardController.initialize(); + require( + address(blockRewardController.wbera()) == WBERA_ADDRESS, + "DeployDevnet: WBERA not set on BlockRewardController" + ); + console2.log("Ran BlockRewardController V2 reinitializer"); + + // BeraChef: set the block delay for reward allocation activation. + _setRewardAllocationBlockDelay(REWARD_ALLOCATION_BLOCK_DELAY); + + // Distributor: link to DedicatedEmissionStreamManager. + distributor.setDedicatedEmissionStreamManager(address(dedicatedEmissionStreamManager)); + require( + address(distributor.dedicatedEmissionStreamManager()) == address(dedicatedEmissionStreamManager), + "DeployDevnet: dedicatedEmissionStreamManager not set on Distributor" + ); + console2.log("Set DedicatedEmissionStreamManager on Distributor"); + + // Distributor: switch emission token from BGT (set by V1 init in POLDeployer) to WBERA. + distributor.setEmissionToken(); + require(distributor.emissionToken() == WBERA_ADDRESS, "DeployDevnet: emission token not WBERA"); + console2.log("Set emission token on Distributor to WBERA"); + + // RewardVaultFactory: incentive tokens collector + helper. + rewardVaultFactory.setIncentiveTokensCollector(address(incentivesCollector)); + rewardVaultFactory.setRewardVaultHelper(address(rewardVaultHelper)); + console2.log("Configured RewardVaultFactory incentive settings"); + + // RewardVaultHelper: wire sWBERA (the WBERAStakerVault is the sWBERA ERC4626 vault). + rewardVaultHelper.setSWBERA(address(wberaStakerVault)); + require( + rewardVaultHelper.sWBERA() == address(wberaStakerVault), + "DeployDevnet: sWBERA not set on RewardVaultHelper" + ); + console2.log("Set sWBERA on RewardVaultHelper"); + + require( + rewardVaultFactory.rewardVaultHelper() == address(rewardVaultHelper), + "DeployDevnet: rewardVaultHelper not wired on RewardVaultFactory" + ); + console2.log("Wired RewardVaultHelper on RewardVaultFactory"); + + // RootPriceOracle: use PeggedPriceOracle as spot oracle (all assets pegged 1:1 on devnet). + rootPriceOracle.setSpotOracle(address(peggedPriceOracle)); + console2.log("Set PeggedPriceOracle as spot oracle on RootPriceOracle"); + } + + // ─── Phase 6: Transfer ownership ─── + + function _transferAllOwnership(address owner) internal { + console2.log("\n--- [6/6] Transfer ownership to:", owner, "---"); + + RBAC.AccountDescription memory deployer = RBAC.AccountDescription({ name: "deployer", addr: msg.sender }); + RBAC.AccountDescription memory ownerDesc = RBAC.AccountDescription({ name: "owner", addr: owner }); + + // ── BGT (Ownable) ─── + bgt.transferOwnership(owner); + require(bgt.owner() == owner, "DeployDevnet: BGT ownership transfer failed"); + console2.log("BGT ownership transferred"); + + // ── BeraChef (Ownable) ─── + beraChef.transferOwnership(owner); + require(beraChef.owner() == owner, "DeployDevnet: BeraChef ownership transfer failed"); + console2.log("BeraChef ownership transferred"); + + // ── BlockRewardController (Ownable) ─── + blockRewardController.transferOwnership(owner); + require( + blockRewardController.owner() == owner, "DeployDevnet: BlockRewardController ownership transfer failed" + ); + console2.log("BlockRewardController ownership transferred"); + + // ── BGTStaker (Ownable) ─── + bgtStaker.transferOwnership(owner); + require(bgtStaker.owner() == owner, "DeployDevnet: BGTStaker ownership transfer failed"); + console2.log("BGTStaker ownership transferred"); + + // ── RewardVaultFactory (AccessControl) ── + { + RBAC.RoleDescription memory pauserRole = RBAC.RoleDescription({ + contractName: "RewardVaultFactory", + contractAddr: address(rewardVaultFactory), + name: "VAULT_PAUSER_ROLE", + role: rewardVaultFactory.VAULT_PAUSER_ROLE() + }); + RBAC.RoleDescription memory managerRole = RBAC.RoleDescription({ + contractName: "RewardVaultFactory", + contractAddr: address(rewardVaultFactory), + name: "VAULT_MANAGER_ROLE", + role: rewardVaultFactory.VAULT_MANAGER_ROLE() + }); + RBAC.RoleDescription memory adminRole = RBAC.RoleDescription({ + contractName: "RewardVaultFactory", + contractAddr: address(rewardVaultFactory), + name: "DEFAULT_ADMIN_ROLE", + role: rewardVaultFactory.DEFAULT_ADMIN_ROLE() + }); + _transferRole(pauserRole, deployer, ownerDesc); + _transferRole(managerRole, deployer, ownerDesc); + _transferRole(adminRole, deployer, ownerDesc); + + UpgradeableBeacon rewardVaultBeacon = UpgradeableBeacon(rewardVaultFactory.beacon()); + rewardVaultBeacon.transferOwnership(owner); + console2.log("RewardVaultFactory roles + beacon transferred"); + } + + // ── Distributor (AccessControl) ── + { + RBAC.RoleDescription memory managerRole = RBAC.RoleDescription({ + contractName: "Distributor", + contractAddr: address(distributor), + name: "MANAGER_ROLE", + role: distributor.MANAGER_ROLE() + }); + RBAC.RoleDescription memory adminRole = RBAC.RoleDescription({ + contractName: "Distributor", + contractAddr: address(distributor), + name: "DEFAULT_ADMIN_ROLE", + role: distributor.DEFAULT_ADMIN_ROLE() + }); + _transferRole(managerRole, deployer, ownerDesc); + _transferRole(adminRole, deployer, ownerDesc); + console2.log("Distributor roles transferred"); + } + + // ── FeeCollector (AccessControl) ── + { + RBAC.RoleDescription memory pauserRole = RBAC.RoleDescription({ + contractName: "FeeCollector", + contractAddr: address(feeCollector), + name: "PAUSER_ROLE", + role: feeCollector.PAUSER_ROLE() + }); + RBAC.RoleDescription memory managerRole = RBAC.RoleDescription({ + contractName: "FeeCollector", + contractAddr: address(feeCollector), + name: "MANAGER_ROLE", + role: feeCollector.MANAGER_ROLE() + }); + RBAC.RoleDescription memory adminRole = RBAC.RoleDescription({ + contractName: "FeeCollector", + contractAddr: address(feeCollector), + name: "DEFAULT_ADMIN_ROLE", + role: feeCollector.DEFAULT_ADMIN_ROLE() + }); + _transferRole(pauserRole, deployer, ownerDesc); + _transferRole(managerRole, deployer, ownerDesc); + _transferRole(adminRole, deployer, ownerDesc); + console2.log("FeeCollector roles transferred"); + } + + // ── BGTIncentiveDistributor (AccessControl) ── + { + RBAC.RoleDescription memory pauserRole = RBAC.RoleDescription({ + contractName: "BGTIncentiveDistributor", + contractAddr: address(bgtIncentiveDistributor), + name: "PAUSER_ROLE", + role: bgtIncentiveDistributor.PAUSER_ROLE() + }); + RBAC.RoleDescription memory managerRole = RBAC.RoleDescription({ + contractName: "BGTIncentiveDistributor", + contractAddr: address(bgtIncentiveDistributor), + name: "MANAGER_ROLE", + role: bgtIncentiveDistributor.MANAGER_ROLE() + }); + RBAC.RoleDescription memory adminRole = RBAC.RoleDescription({ + contractName: "BGTIncentiveDistributor", + contractAddr: address(bgtIncentiveDistributor), + name: "DEFAULT_ADMIN_ROLE", + role: bgtIncentiveDistributor.DEFAULT_ADMIN_ROLE() + }); + _transferRole(pauserRole, deployer, ownerDesc); + _transferRole(managerRole, deployer, ownerDesc); + _transferRole(adminRole, deployer, ownerDesc); + console2.log("BGTIncentiveDistributor roles transferred"); + } + + // ── WBERAStakerVault (AccessControl) ── + { + RBAC.RoleDescription memory pauserRole = RBAC.RoleDescription({ + contractName: "WBERAStakerVault", + contractAddr: address(wberaStakerVault), + name: "PAUSER_ROLE", + role: wberaStakerVault.PAUSER_ROLE() + }); + RBAC.RoleDescription memory managerRole = RBAC.RoleDescription({ + contractName: "WBERAStakerVault", + contractAddr: address(wberaStakerVault), + name: "MANAGER_ROLE", + role: wberaStakerVault.MANAGER_ROLE() + }); + RBAC.RoleDescription memory adminRole = RBAC.RoleDescription({ + contractName: "WBERAStakerVault", + contractAddr: address(wberaStakerVault), + name: "DEFAULT_ADMIN_ROLE", + role: wberaStakerVault.DEFAULT_ADMIN_ROLE() + }); + _transferRole(pauserRole, deployer, ownerDesc); + _transferRole(managerRole, deployer, ownerDesc); + _transferRole(adminRole, deployer, ownerDesc); + console2.log("WBERAStakerVault roles transferred"); + } + + // ── IncentivesCollector (AccessControl) ── + { + RBAC.RoleDescription memory pauserRole = RBAC.RoleDescription({ + contractName: "IncentivesCollector", + contractAddr: address(incentivesCollector), + name: "PAUSER_ROLE", + role: incentivesCollector.PAUSER_ROLE() + }); + RBAC.RoleDescription memory managerRole = RBAC.RoleDescription({ + contractName: "IncentivesCollector", + contractAddr: address(incentivesCollector), + name: "MANAGER_ROLE", + role: incentivesCollector.MANAGER_ROLE() + }); + RBAC.RoleDescription memory adminRole = RBAC.RoleDescription({ + contractName: "IncentivesCollector", + contractAddr: address(incentivesCollector), + name: "DEFAULT_ADMIN_ROLE", + role: incentivesCollector.DEFAULT_ADMIN_ROLE() + }); + _transferRole(pauserRole, deployer, ownerDesc); + _transferRole(managerRole, deployer, ownerDesc); + _transferRole(adminRole, deployer, ownerDesc); + console2.log("IncentivesCollector roles transferred"); + } + + // ── WBERAStakerVaultWithdrawalRequest (Ownable) ── + wberaStakerVaultWithdrawalRequest.transferOwnership(owner); + require( + wberaStakerVaultWithdrawalRequest.owner() == owner, + "DeployDevnet: WBERAStakerVaultWithdrawalRequest ownership transfer failed" + ); + console2.log("WBERAStakerVaultWithdrawalRequest ownership transferred"); + + // ── DedicatedEmissionStreamManager (AccessControl) ── + { + RBAC.RoleDescription memory allocationManagerRole = RBAC.RoleDescription({ + contractName: "DedicatedEmissionStreamManager", + contractAddr: address(dedicatedEmissionStreamManager), + name: "ALLOCATION_MANAGER_ROLE", + role: dedicatedEmissionStreamManager.ALLOCATION_MANAGER_ROLE() + }); + RBAC.RoleDescription memory adminRole = RBAC.RoleDescription({ + contractName: "DedicatedEmissionStreamManager", + contractAddr: address(dedicatedEmissionStreamManager), + name: "DEFAULT_ADMIN_ROLE", + role: dedicatedEmissionStreamManager.DEFAULT_ADMIN_ROLE() + }); + _grantRole(allocationManagerRole, ownerDesc); + _transferRole(adminRole, deployer, ownerDesc); + console2.log("DedicatedEmissionStreamManager roles transferred"); + } + + // ── RewardVaultHelper (AccessControl) ── + { + RBAC.RoleDescription memory adminRole = RBAC.RoleDescription({ + contractName: "RewardVaultHelper", + contractAddr: address(rewardVaultHelper), + name: "DEFAULT_ADMIN_ROLE", + role: rewardVaultHelper.DEFAULT_ADMIN_ROLE() + }); + _transferRole(adminRole, deployer, ownerDesc); + console2.log("RewardVaultHelper roles transferred"); + } + + // ── Honey (AccessControl) ── + { + RBAC.RoleDescription memory adminRole = RBAC.RoleDescription({ + contractName: "Honey", + contractAddr: address(honey), + name: "DEFAULT_ADMIN_ROLE", + role: honey.DEFAULT_ADMIN_ROLE() + }); + _transferRole(adminRole, deployer, ownerDesc); + console2.log("Honey roles transferred"); + } + + // ── HoneyFactory (AccessControl) ── + { + RBAC.RoleDescription memory pauserRole = RBAC.RoleDescription({ + contractName: "HoneyFactory", + contractAddr: address(honeyFactory), + name: "PAUSER_ROLE", + role: honeyFactory.PAUSER_ROLE() + }); + RBAC.RoleDescription memory managerRole = RBAC.RoleDescription({ + contractName: "HoneyFactory", + contractAddr: address(honeyFactory), + name: "MANAGER_ROLE", + role: honeyFactory.MANAGER_ROLE() + }); + RBAC.RoleDescription memory adminRole = RBAC.RoleDescription({ + contractName: "HoneyFactory", + contractAddr: address(honeyFactory), + name: "DEFAULT_ADMIN_ROLE", + role: honeyFactory.DEFAULT_ADMIN_ROLE() + }); + _transferRole(pauserRole, deployer, ownerDesc); + _transferRole(managerRole, deployer, ownerDesc); + _transferRole(adminRole, deployer, ownerDesc); + + UpgradeableBeacon honeyFactoryBeacon = UpgradeableBeacon(honeyFactory.beacon()); + honeyFactoryBeacon.transferOwnership(owner); + console2.log("HoneyFactory roles + beacon transferred"); + } + + // ── HoneyFactoryReader (AccessControl) ── + { + RBAC.RoleDescription memory adminRole = RBAC.RoleDescription({ + contractName: "HoneyFactoryReader", + contractAddr: address(honeyFactoryReader), + name: "DEFAULT_ADMIN_ROLE", + role: honeyFactoryReader.DEFAULT_ADMIN_ROLE() + }); + _transferRole(adminRole, deployer, ownerDesc); + console2.log("HoneyFactoryReader roles transferred"); + } + + // ── PythPriceOracle (AccessControl) ── + { + RBAC.RoleDescription memory managerRole = RBAC.RoleDescription({ + contractName: "PythPriceOracle", + contractAddr: address(pythPriceOracle), + name: "MANAGER_ROLE", + role: pythPriceOracle.MANAGER_ROLE() + }); + RBAC.RoleDescription memory adminRole = RBAC.RoleDescription({ + contractName: "PythPriceOracle", + contractAddr: address(pythPriceOracle), + name: "DEFAULT_ADMIN_ROLE", + role: pythPriceOracle.DEFAULT_ADMIN_ROLE() + }); + _transferRole(managerRole, deployer, ownerDesc); + _transferRole(adminRole, deployer, ownerDesc); + console2.log("PythPriceOracle roles transferred"); + } + + // ── RootPriceOracle (AccessControl) ── + { + RBAC.RoleDescription memory managerRole = RBAC.RoleDescription({ + contractName: "RootPriceOracle", + contractAddr: address(rootPriceOracle), + name: "MANAGER_ROLE", + role: rootPriceOracle.MANAGER_ROLE() + }); + RBAC.RoleDescription memory adminRole = RBAC.RoleDescription({ + contractName: "RootPriceOracle", + contractAddr: address(rootPriceOracle), + name: "DEFAULT_ADMIN_ROLE", + role: rootPriceOracle.DEFAULT_ADMIN_ROLE() + }); + _transferRole(managerRole, deployer, ownerDesc); + _transferRole(adminRole, deployer, ownerDesc); + console2.log("RootPriceOracle roles transferred"); + } + + console2.log("\nAll ownership transferred to:", owner); + } + + // ─── Helpers ─── + + function _logAddresses() internal view { + console2.log("\n--- Deployed addresses ---"); + console2.log("BGT: ", address(bgt)); + console2.log("BeraChef: ", address(beraChef)); + console2.log("BlockRewardController: ", address(blockRewardController)); + console2.log("Distributor: ", address(distributor)); + console2.log("RewardVaultFactory: ", address(rewardVaultFactory)); + console2.log("BGTStaker: ", address(bgtStaker)); + console2.log("FeeCollector: ", address(feeCollector)); + console2.log("BGTIncentiveDistributor: ", address(bgtIncentiveDistributor)); + console2.log("WBERAStakerVault: ", address(wberaStakerVault)); + console2.log("WBERAStakerVaultWithdrawalRequest:", address(wberaStakerVaultWithdrawalRequest)); + console2.log("IncentivesCollector: ", address(incentivesCollector)); + console2.log("DedicatedEmissionStreamManager: ", address(dedicatedEmissionStreamManager)); + console2.log("RewardVaultHelper: ", address(rewardVaultHelper)); + console2.log("PeggedPriceOracle: ", address(peggedPriceOracle)); + console2.log("PythPriceOracle: ", address(pythPriceOracle)); + console2.log("RootPriceOracle: ", address(rootPriceOracle)); + console2.log("Honey: ", address(honey)); + console2.log("HoneyFactory: ", address(honeyFactory)); + console2.log("HoneyFactoryReader: ", address(honeyFactoryReader)); + } +} diff --git a/script/devnet/PoLUpgrader.sol b/script/devnet/PoLUpgrader.sol new file mode 100644 index 0000000..01063de --- /dev/null +++ b/script/devnet/PoLUpgrader.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { UpgradeableBeacon } from "solady/src/utils/UpgradeableBeacon.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { RewardVaultFactory } from "src/pol/rewards/RewardVaultFactory.sol"; +import { Ownable } from "solady/src/auth/Ownable.sol"; +import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; + +/// @title PoLUpgrader +/// @notice Deploys new PoL implementations on construction, then upgrades all proxies and returns +/// all authorities to the caller in a single `upgrade()` transaction. +/// @dev Usage: +/// 1. Deploy this contract — new implementations are deployed in the constructor. +/// 2. Transfer upgrade authority for every proxy to this contract: +/// - `transferOwnership(address(this))` for BlockRewardController, BeraChef, and the RewardVault beacon. +/// - `grantRole(DEFAULT_ADMIN_ROLE, address(this))` for Distributor, RewardVaultFactory, RewardVaultHelper. +/// 3. Call `upgrade(...)` — upgrades all proxies and returns all authorities to `msg.sender`. +contract PoLUpgrader { + struct Proxies { + address payable blockRewardController; + address beraChef; + address distributor; + address rewardVaultFactory; + address payable rewardVaultHelper; + } + + struct Impls { + address blockRewardController; + address beraChef; + address distributor; + address rewardVaultFactory; + address rewardVault; + address rewardVaultHelper; + } + + /// @dev Only the deployer may call `upgrade()`. + // solhint-disable-next-line immutable-vars-naming + address private immutable _deployer; + + constructor() { + _deployer = msg.sender; + } + + /// @notice Upgrades all PoL proxies and returns all authorities to `msg.sender`. + /// @dev Must be called by the deployer. Caller must have transferred all upgrade authorities + /// to this contract before calling (see contract-level NatSpec). + function upgrade(Proxies calldata proxies, Impls calldata impls, address _swbera) external { + require(msg.sender == _deployer, "PoLUpgrader: not deployer"); + + // ── Upgrade all proxies + // ────────────────────────────────────────────────── + + // BlockRewardController: upgrade with V2 reinitializer that wires WBERA and clears deprecated state. + // Use encodeWithSignature to disambiguate from the V1 initializer overload. + UUPSUpgradeable(proxies.blockRewardController) + .upgradeToAndCall(impls.blockRewardController, abi.encodeWithSignature("initialize()")); + + // Distributor: upgrade then update emission token to WBERA. + UUPSUpgradeable(proxies.distributor) + .upgradeToAndCall(impls.distributor, abi.encodeWithSignature("setEmissionToken()")); + + // BeraChef: plain upgrade, no reinitializer needed. + UUPSUpgradeable(proxies.beraChef).upgradeToAndCall(impls.beraChef, ""); + + // RewardVault: beacon upgrade — all vault proxies automatically use the new implementation. + // Beacon address is read before upgrading the factory proxy. + address beacon = RewardVaultFactory(proxies.rewardVaultFactory).beacon(); + UpgradeableBeacon(beacon).upgradeTo(impls.rewardVault); + + // RewardVaultFactory: plain upgrade. + UUPSUpgradeable(proxies.rewardVaultFactory).upgradeToAndCall(impls.rewardVaultFactory, ""); + + // RewardVaultHelper: upgrade then set sWBERA. + UUPSUpgradeable(proxies.rewardVaultHelper) + .upgradeToAndCall(impls.rewardVaultHelper, abi.encodeWithSignature("setSWBERA(address)", _swbera)); + + // ── Return all authorities to msg.sender + // ────────────────────────────────────────────────── + + // Ownable contracts + Ownable(proxies.blockRewardController).transferOwnership(msg.sender); + Ownable(proxies.beraChef).transferOwnership(msg.sender); + UpgradeableBeacon(beacon).transferOwnership(msg.sender); + + // AccessControl contracts: grant first, then revoke to avoid losing admin role + bytes32 adminRole = 0x00; + IAccessControl(proxies.distributor).grantRole(adminRole, msg.sender); + IAccessControl(proxies.distributor).revokeRole(adminRole, address(this)); + IAccessControl(proxies.rewardVaultFactory).grantRole(adminRole, msg.sender); + IAccessControl(proxies.rewardVaultFactory).revokeRole(adminRole, address(this)); + IAccessControl(proxies.rewardVaultHelper).grantRole(adminRole, msg.sender); + IAccessControl(proxies.rewardVaultHelper).revokeRole(adminRole, address(this)); + } +} diff --git a/script/devnet/UpgradePoL.s.sol b/script/devnet/UpgradePoL.s.sol new file mode 100644 index 0000000..d0e13f6 --- /dev/null +++ b/script/devnet/UpgradePoL.s.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { console2 } from "forge-std/Script.sol"; + +import { UpgradeableBeacon } from "solady/src/utils/UpgradeableBeacon.sol"; + +import { BaseDeployScript } from "../base/BaseDeploy.s.sol"; +import { AddressBook } from "../base/AddressBook.sol"; +import { PoLUpgrader } from "./PoLUpgrader.sol"; +import { BlockRewardController } from "src/pol/rewards/BlockRewardController.sol"; +import { BeraChef } from "src/pol/rewards/BeraChef.sol"; +import { Distributor } from "src/pol/rewards/Distributor.sol"; +import { RewardVault } from "src/pol/rewards/RewardVault.sol"; +import { RewardVaultFactory } from "src/pol/rewards/RewardVaultFactory.sol"; +import { RewardVaultHelper } from "src/pol/rewards/RewardVaultHelper.sol"; + +contract UpgradePoLScript is BaseDeployScript, AddressBook { + address blockRewardControllerImpl; + address beraChefImpl; + address distributorImpl; + address rewardVaultImpl; + address rewardVaultFactoryImpl; + address rewardVaultHelperImpl; + + function run() public pure { + console2.log("Please run specific function."); + } + + /// @notice Deploys new implementations for all PoL contracts without upgrading the proxies. + function deployNewImplementations() public broadcast { + _deployNewImplementations(); + } + + /// @notice Deploys PoLUpgrader, transfers all authorities to it, executes the upgrade, and + /// verifies that all authorities have been returned to the broadcaster. + /// @dev The broadcaster must already hold upgrade authority for every proxy (owner for + /// BlockRewardController / BeraChef; DEFAULT_ADMIN_ROLE for Distributor, + /// RewardVaultFactory, and RewardVaultHelper) and must own the RewardVault beacon. + /// @param _blockRewardController Proxy address of BlockRewardController. + /// @param _beraChef Proxy address of BeraChef. + /// @param _distributor Proxy address of Distributor. + /// @param _rewardVaultFactory Proxy address of RewardVaultFactory. + /// @param _rewardVaultHelper Proxy address of RewardVaultHelper. + /// @param _swbera sWBERA token address. + function upgrade( + address payable _blockRewardController, + address _beraChef, + address _distributor, + address _rewardVaultFactory, + address payable _rewardVaultHelper, + address _swbera + ) + public + broadcast + { + // ── 0. Deploy new implementations + ( + blockRewardControllerImpl, + beraChefImpl, + distributorImpl, + rewardVaultImpl, + rewardVaultFactoryImpl, + rewardVaultHelperImpl + ) = _deployNewImplementations(); + + // ── 1. Deploy PoLUpgrader (deploys all new implementations in its constructor) + PoLUpgrader upgrader = new PoLUpgrader(); + console2.log("PoLUpgrader: ", address(upgrader)); + + // ── 2. Transfer all authorities to the upgrader + + // Ownable contracts + BlockRewardController(_blockRewardController).transferOwnership(address(upgrader)); + BeraChef(_beraChef).transferOwnership(address(upgrader)); + + // Beacon (read before any factory upgrade so the address is still accessible) + address beacon = RewardVaultFactory(_rewardVaultFactory).beacon(); + UpgradeableBeacon(beacon).transferOwnership(address(upgrader)); + + // AccessControl contracts: grant to upgrader (broadcaster retains its own role) + Distributor(_distributor).grantRole(bytes32(0), address(upgrader)); + RewardVaultFactory(_rewardVaultFactory).grantRole(bytes32(0), address(upgrader)); + RewardVaultHelper(_rewardVaultHelper).grantRole(bytes32(0), address(upgrader)); + + // ── 3. Execute upgrade (upgrader returns all authorities to msg.sender = broadcaster) + upgrader.upgrade( + PoLUpgrader.Proxies({ + blockRewardController: _blockRewardController, + beraChef: _beraChef, + distributor: _distributor, + rewardVaultFactory: _rewardVaultFactory, + rewardVaultHelper: _rewardVaultHelper + }), + PoLUpgrader.Impls({ + blockRewardController: blockRewardControllerImpl, + beraChef: beraChefImpl, + distributor: distributorImpl, + rewardVaultFactory: rewardVaultFactoryImpl, + rewardVault: rewardVaultImpl, + rewardVaultHelper: rewardVaultHelperImpl + }), + _swbera + ); + + // ── 4. Verify all authorities have been returned to the broadcaster + require(BlockRewardController(_blockRewardController).owner() == msg.sender, "BRC: ownership not returned"); + require(BeraChef(_beraChef).owner() == msg.sender, "BeraChef: ownership not returned"); + require(UpgradeableBeacon(beacon).owner() == msg.sender, "Beacon: ownership not returned"); + require(Distributor(_distributor).hasRole(bytes32(0), msg.sender), "Distributor: admin not returned"); + require( + RewardVaultFactory(_rewardVaultFactory).hasRole(bytes32(0), msg.sender), + "RewardVaultFactory: admin not returned" + ); + require( + RewardVaultHelper(_rewardVaultHelper).hasRole(bytes32(0), msg.sender), + "RewardVaultHelper: admin not returned" + ); + + console2.log("All authorities returned to:", msg.sender); + } + + function _deployNewImplementations() internal returns (address, address, address, address, address, address) { + address _blockRewardControllerImpl = address(new BlockRewardController()); + address _beraChefImpl = address(new BeraChef()); + address _distributorImpl = address(new Distributor()); + address _rewardVaultImpl = address(new RewardVault()); + address _rewardVaultFactoryImpl = address(new RewardVaultFactory()); + address _rewardVaultHelperImpl = address(new RewardVaultHelper()); + + console2.log("BlockRewardController impl:", _blockRewardControllerImpl); + console2.log("BeraChef impl:", _beraChefImpl); + console2.log("Distributor impl:", _distributorImpl); + console2.log("RewardVault impl:", _rewardVaultImpl); + console2.log("RewardVaultFactory impl:", _rewardVaultFactoryImpl); + console2.log("RewardVaultHelper impl:", _rewardVaultHelperImpl); + + return ( + _blockRewardControllerImpl, + _beraChefImpl, + _distributorImpl, + _rewardVaultImpl, + _rewardVaultFactoryImpl, + _rewardVaultHelperImpl + ); + } +} diff --git a/script/honey/HoneyAddresses.sol b/script/honey/HoneyAddresses.sol index 01f70ec..742000b 100644 --- a/script/honey/HoneyAddresses.sol +++ b/script/honey/HoneyAddresses.sol @@ -64,14 +64,14 @@ abstract contract HoneyAddressBook { /// @notice Devnet addresses function _getDevnetHoneyAddresses() private pure returns (HoneyAddresses memory) { return HoneyAddresses({ - honey: 0x60Cd2DB29edf02c6514821498Ceabc61A845BbC2, - honeyImpl: 0xbd5e55e69c3Fd0c89d153DF60C8B1afb2D72E500, - honeyFactory: 0x6DFf70A6327b343801997f7fE20d192849863c5e, + honey: 0x4475bdcd6F2Ded26Ea5074beb862271f2141f696, + honeyImpl: 0x4D4Ff71C50D47a5A45682F42E1E96bdED8fE7516, + honeyFactory: 0x2AA7F988284fD04cE83b27d017B89731c67d8F67, honeyFactoryReader: 0xf1CF3467C9508dfa6D1197F5359419856B3A3300, - honeyFactoryImpl: 0xec5e1a0B097BD27B1C244Ab5557a52160200dB3A, + honeyFactoryImpl: 0xd6E9728eefa526F9B6Cf3c9ba5CbC060F937cDb0, honeyFactoryReaderImpl: 0x52d5848Ab7A304369cd2879DAfd9Ed3349E8ebB1, collateralVaultImpl: 0x5DeDB0F5587F83798245a53189c1A52437A52475, - honeyFactoryPythWrapper: 0x7C4d33b026F44E5E6589BaAf7FF38bd93633d58f + honeyFactoryPythWrapper: 0xA55F4a5D0415bFAaaE8207F77Ab2dc406e7594A2 }); } diff --git a/script/pol/POLAddresses.sol b/script/pol/POLAddresses.sol index 2648852..f2385a6 100644 --- a/script/pol/POLAddresses.sol +++ b/script/pol/POLAddresses.sol @@ -22,8 +22,8 @@ struct POLAddresses { address feeCollectorImpl; address bgtIncentiveDistributor; address bgtIncentiveDistributorImpl; - address bgtIncentiveFeeCollector; - address bgtIncentiveFeeCollectorImpl; + address incentivesCollector; + address incentivesCollectorImpl; address wberaStakerVault; address wberaStakerVaultImpl; address wberaStakerVaultWithdrawalRequest; @@ -66,30 +66,30 @@ abstract contract POLAddressBook { beraChef: 0xdf960E8F3F19C481dDE769edEDD439ea1a63426a, beraChefImpl: 0x7BE46e21Af81E432228E7ae15DfAA409E4ea211e, blockRewardController: 0x1AE7dD7AE06F6C58B4524d9c1f816094B1bcCD8e, - blockRewardControllerImpl: 0x971aF0c15E15F1F31ff60C6Ed2c2234B88D5d25a, + blockRewardControllerImpl: 0x6341356004D27821CBBd9b8554F0aF53FB39776C, distributor: 0xD2f19a79b026Fb636A7c300bF5947df113940761, - distributorImpl: 0x7fFC63Db4fddCC40C3FfBAC88b71Fd9330b80C3b, + distributorImpl: 0xABc4e807C2664eBBa530D2A4348b3151A4E05b8f, rewardVaultFactory: 0x94Ad6Ac84f6C6FbA8b8CCbD71d9f4f101def52a8, - rewardVaultFactoryImpl: 0x6b75ab5860B9129C58bdb01b8194dd1CBc428cac, - rewardVaultImpl: 0x68DFD1c244Fc65Ca57967E269BD0112aF0eC2184, + rewardVaultFactoryImpl: 0xF96dB70f2F87dB141beEbCF4eDE97a3f13153E31, + rewardVaultImpl: 0x40CF72ec06B66b0454682e2ee07Ab84022ea00d5, bgtStaker: 0x44F07Ce5AfeCbCC406e6beFD40cc2998eEb8c7C6, bgtStakerImpl: 0xDD7FA46a1a735DBD7E7eD4B1928176D28002e205, feeCollector: 0x7Bb8DdaC7FbE3FFC0f4B3c73C4F158B06CF82650, feeCollectorImpl: 0x0fE7B2A78f8c239569ec22cdbdb472694afc289c, bgtIncentiveDistributor: 0x77DA09bC82652f9A14d1b170a001e759640298e6, - bgtIncentiveDistributorImpl: 0xa0170DBDe24E92F0d3140CC119E8c51b85BE2DC6, - bgtIncentiveFeeCollector: 0x1984Baf659607Cc5f206c55BB3B00eb3E180190B, - bgtIncentiveFeeCollectorImpl: 0x56808698929c56D72851E18E5d8E1859B8E6FaCC, + bgtIncentiveDistributorImpl: 0x5c5BfeFddB6f6A51F1D24A6a99F3BeA53eE59F29, + incentivesCollector: 0x1984Baf659607Cc5f206c55BB3B00eb3E180190B, + incentivesCollectorImpl: 0x16565CCEc1b6782cf06C9fab051a1299036d1ddB, wberaStakerVault: 0x118D2cEeE9785eaf70C15Cd74CD84c9f8c3EeC9a, wberaStakerVaultImpl: 0x657EC58fDc6CebBDB78d74f814b1C5fA3C0423B1, wberaStakerVaultWithdrawalRequest: 0x30e47fd0452a14Caf18A0444cb6f35eaCaC899DA, wberaStakerVaultWithdrawalRequestImpl: 0x9d77351A50eba1D50A77B1b86b94b7bD9f42f216, rewardVaultHelper: 0xEe233a69A36Db7fC10E03e921D90DEC52Cdce6e2, - rewardVaultHelperImpl: 0xa64951392198b4c9739d336Da22F853d8a66a8C5, + rewardVaultHelperImpl: 0x8cA1678AA2eC74bA62a3B9a0f4335846fd32D3c8, rewardAllocatorFactory: 0xc8FD9a3fB3Dad4C22c9F8Cfa7cecC318A667A791, rewardAllocatorFactoryImpl: 0x7e80F890Ac3752711BC40fE18FDbbe23BEB88f2B, lstStakerVaultFactory: 0xc41bbD6695AB6bdc6D04701b15f4CE5EbA2e2500, - lstStakerVaultFactoryImpl: 0x330FB93c4DB234E81Bbeda943D5eD42a2039bE8c, + lstStakerVaultFactoryImpl: 0x4ad7D50440370E277F523330458e7eFFF0C6cfb1, lstStakerVaultImpl: 0x805c3BB9f74fF0d14eF401f0Fd986713fA521C68, lstStakerVaultWithdrawalRequestImpl: 0x5Df9799bd804E0f0001Df62d34c0026CFeb5890c, dedicatedEmissionStreamManager: 0x813dCdBa9197947792985c866cE98D6739cA821A, @@ -108,30 +108,30 @@ abstract contract POLAddressBook { beraChef: 0xdf960E8F3F19C481dDE769edEDD439ea1a63426a, beraChefImpl: 0xb0857802D9B91ffD797562627f4801BA080c512b, blockRewardController: 0x1AE7dD7AE06F6C58B4524d9c1f816094B1bcCD8e, - blockRewardControllerImpl: 0x401479c852286F702536613dA8De237401621161, + blockRewardControllerImpl: 0x3e6286bEeB457fBDc6C1218215be12c2B1a6D9B2, distributor: 0xD2f19a79b026Fb636A7c300bF5947df113940761, - distributorImpl: 0xbD95CAd473Adc21d6b1Ea7EbB674bD8b6Af5e1d1, + distributorImpl: 0x65Ccba6221503173Ff984Bd326F412Ef584AFf13, rewardVaultFactory: 0x94Ad6Ac84f6C6FbA8b8CCbD71d9f4f101def52a8, - rewardVaultFactoryImpl: 0xa6f4899209302f363E863ED8E7aCD76b1d8998E1, - rewardVaultImpl: 0xC87Bb594Cd9d80Dc9E5ab26582913EE5f6eB2BB6, + rewardVaultFactoryImpl: 0x16E955dc6e8d1B379853a25142658077477c9E1f, + rewardVaultImpl: 0xa1a85743d500D5976e511062E62f2cB1E7E40A99, bgtStaker: 0x44F07Ce5AfeCbCC406e6beFD40cc2998eEb8c7C6, bgtStakerImpl: 0x66B872cC8B01269E20E5E5aB05C2F7A1198A67Ce, feeCollector: 0x7Bb8DdaC7FbE3FFC0f4B3c73C4F158B06CF82650, feeCollectorImpl: 0x6ca4930Efc5cb995D83e2607571A3b2060532f75, - bgtIncentiveDistributor: 0x77DA09bC82652f9A14d1b170a001e759640298e6, - bgtIncentiveDistributorImpl: 0x7E71C51F367f5f4A9D08151f7C24a2503Fa1A844, - bgtIncentiveFeeCollector: 0x1984Baf659607Cc5f206c55BB3B00eb3E180190B, - bgtIncentiveFeeCollectorImpl: 0xd4013ce734d58AE0B20215c356B5DF4a89D46Cd3, + bgtIncentiveDistributor: 0xb0d005Fe83E3F1ec876C1a64700c5F0d6265d9E3, + bgtIncentiveDistributorImpl: 0x4AA432E9a3FD5dC58146Ef231ff346364E36Cc6D, + incentivesCollector: 0x1984Baf659607Cc5f206c55BB3B00eb3E180190B, + incentivesCollectorImpl: 0xf79936BFF041a489CE7bE62cc46bbbdf86003689, wberaStakerVault: 0x118D2cEeE9785eaf70C15Cd74CD84c9f8c3EeC9a, wberaStakerVaultImpl: 0x68348D7c5973bB932c108F03C04C16900827Fc14, wberaStakerVaultWithdrawalRequest: 0x30e47fd0452a14Caf18A0444cb6f35eaCaC899DA, wberaStakerVaultWithdrawalRequestImpl: 0x1a1b50F511feb89a92DA0ACB2732cfebfB66B096, rewardVaultHelper: 0xEe233a69A36Db7fC10E03e921D90DEC52Cdce6e2, - rewardVaultHelperImpl: 0x3026AD38f797B311F9B0d35891eD1D2C35b4F40C, + rewardVaultHelperImpl: 0x9104D6f5201EC22fa3DBb3CCEEd6eABBcD1D306d, rewardAllocatorFactory: 0x7f09Cf6958631513aF0400488F65c7B5c0313F52, rewardAllocatorFactoryImpl: 0xA3b40aB9c6f7B45625cBD81a1F05027f5507Ee0d, lstStakerVaultFactory: 0xAf10B532cCC25B26a8e28913D5C4056a77e7a178, - lstStakerVaultFactoryImpl: 0x04ABa70C118990534B2D37e8AC46cEeA1B5967B9, + lstStakerVaultFactoryImpl: 0x211acBf8a0241F7671909cc645314010fC7F6aAe, lstStakerVaultImpl: 0x49CA7e596d5F1B96d1B8274B2e6eFFe92ffD53ec, lstStakerVaultWithdrawalRequestImpl: 0x78e151F4e599eC1EebDa2563536BDa14498E2f21, dedicatedEmissionStreamManager: 0xfe83d31669b52B7a619119Bc71805fD29eeEB9Dd, @@ -145,33 +145,33 @@ abstract contract POLAddressBook { beaconDeposit: 0x4242424242424242424242424242424242424242, // From genesis files wbera: 0x6969696969696969696969696969696969696969, // From genesis files bgt: 0xEE0BD9569e41fA26A79305Fc31a663986Deb79FB, - beraChef: 0x11D327E93F251e6cCE267e392CdA9eEF8Ff9099B, + beraChef: 0xD93EB81ff6d6D6a67b60edaE2cf8B5E95Ec47467, beraChefImpl: 0xaBE258a826B1fbD00eA0ea3D766a891133B3d93c, - blockRewardController: 0x4A2452Fd7e9FCA389d98063c5C3A8FC63838E451, - blockRewardControllerImpl: 0xf1774D66392999268a40c8c9Df1708f220662aba, - distributor: 0x9dD639638B46899CED46ef58b3A3c21E9feF9d7c, - distributorImpl: 0x823114F59C2D226e7e76aa0e374739afe937728A, - rewardVaultFactory: 0xb6C6e3A4aBf3777ffccB01d0a8581daAc07CaAEf, - rewardVaultFactoryImpl: 0x3199Ea83F2731b74d77F7A989cE3bDF785779304, - rewardVaultImpl: 0xB9a8323d504994EC9dD864887D29F21e6eDbaDf2, + blockRewardController: 0xe96aD3b5Ea4763B66979d1D76028227bb5CF1951, + blockRewardControllerImpl: 0x68Ee183142A289a1D1059647fFA905256BE45C7E, + distributor: 0xEFBA19B83712c6FF15e8bDeB624aB223A1b89af6, + distributorImpl: 0xa2F64452D137c50A85E78b6dE610a427bf659322, + rewardVaultFactory: 0xcd47e10A495920C45c12964E7A0d1dc78F0eFfA1, + rewardVaultFactoryImpl: 0x9c045882Ecd359c9B8e0E707E94Aa13CE797a594, + rewardVaultImpl: 0xD4718Aaf0a3C341961b67660175D0a0EE68E32dE, bgtStaker: 0xb3EFeD697e5A10568E65452d5fAd4CFcF057e457, bgtStakerImpl: 0xE2fC2F9AC9e4988187f7A37B161fd042E3E0A4F8, feeCollector: 0x750791868bcf30654543165bfc9BD1da1E071870, feeCollectorImpl: 0xca68B6742c78Fac8276082eb74E4532B8E24887d, - bgtIncentiveDistributor: 0x6DC1E455571937a1A579090c8f879A4431E169b8, - bgtIncentiveDistributorImpl: 0x507B0b5781c747E73Fd0dd670D77166a5cc232f3, - bgtIncentiveFeeCollector: 0xc3322E5886CdA15b51b1cbe5A8b5668F9C6Ad72E, - bgtIncentiveFeeCollectorImpl: 0x6eD35D56914822D487f968397CCf0acB0dCacfC8, - wberaStakerVault: 0x39091E2a8472Bc3364F59dFF620c9163AA27F397, + bgtIncentiveDistributor: 0x20CA52119499531EF4ac7e83a35Bf1C505538E74, + bgtIncentiveDistributorImpl: 0x2b9308e4a09F8BEEab2860D255A1a635B8E9FBCF, + incentivesCollector: 0x2F375FcEa0C162b22a70099D36A01263B681f42b, + incentivesCollectorImpl: 0xe0aFf6182F9A6E97D423C197fCF2D21b304ac015, + wberaStakerVault: 0x0651f7834678e19BAf01de086864240DE4FfBE45, wberaStakerVaultImpl: 0x7571c17da478022fa3C4C8eD646B282E930F4C67, wberaStakerVaultWithdrawalRequest: 0xa48b32DE980349893de3C2Eb6cC2C5505E8A53c6, wberaStakerVaultWithdrawalRequestImpl: 0xC99dbe0679AAa9c95B1C1d00d8D25a3EA5Bf552a, - rewardVaultHelper: 0x41BE38f22F6D04D2D8A2e6b13fB71B8a4b8B4bD3, - rewardVaultHelperImpl: 0xA88C548a1160fc28e9DA6F91cfA2Ef76dEBFAE70, + rewardVaultHelper: 0xd1D259eb84A04df03a32aF7BA6609939D41e10b3, + rewardVaultHelperImpl: 0x515163711122a14b58f9bba67AEC941BdA00f964, rewardAllocatorFactory: 0xF9451D2Ca42C703bc86Ca8aE76336527EAA5d63A, rewardAllocatorFactoryImpl: 0xf6503F1c149bB6c12f1F25c500c580335578A520, - lstStakerVaultFactory: 0x124BC8af306345db060aCD04D87B6f5C79C80027, - lstStakerVaultFactoryImpl: 0x401F34e0dBAd8E53cF0D3e62574BF13841Ad8EcB, + lstStakerVaultFactory: 0x0C9a4C3B7557bb1bED8Cc93Ee11A460220D209e3, + lstStakerVaultFactoryImpl: 0x3522D0aA63813F5b2987FeEA8Be045E4d399c1aF, lstStakerVaultImpl: 0xBADD53A592FC22125D82dC8252D7F7C834fbDAf7, lstStakerVaultWithdrawalRequestImpl: 0xED868a9F16b8F715A9fBfE3b9ff4e096B35C7E74, dedicatedEmissionStreamManager: 0x469a8410f1417Df9114C7bA7F7846FBE184f9f21, @@ -188,30 +188,30 @@ abstract contract POLAddressBook { beraChef: 0x4898c5fb3af0Be5E709e35E75800a5E313BF6e8a, beraChefImpl: 0xa8399eA9bb56B02838294003cddF8e6933fC3B57, blockRewardController: 0xf1aDf7a50773FF65c7cE8662A309F8e277Cd7Ec6, - blockRewardControllerImpl: 0x147585c9E870f1CE7F3320A30ff2E43410463e58, + blockRewardControllerImpl: 0xE16dc304d1aF660A03e618F4948f84042aaEE03c, distributor: 0x046e3BeED5090A8f6EF88eeFD1a1877360560F71, - distributorImpl: 0xab708f5D33495718a44CA7835Be048A683E55F41, + distributorImpl: 0x7C1810322A453074A1Eb3cd7Ebeb79dE66598378, rewardVaultFactory: 0x5D280c8F2227A594De61902fE4154Ea669163742, - rewardVaultFactoryImpl: 0x589e95A46B566d2d011314b4f209716f266f774D, - rewardVaultImpl: 0x62C114DD46829ADc7ACf98D9ef5ecaC91b2f53E2, + rewardVaultFactoryImpl: 0xe4CaF94A74916cc59E99d4258ae59D2766Abb3b5, + rewardVaultImpl: 0x0eEBC5ef4AEbB2752E69aFd8e201B1751B13EB46, bgtStaker: 0x57C4b599Ef3D476cC2bc9eb494542db546F764f0, bgtStakerImpl: 0x409aCA0227Dc0B097c05E97c46499011EdB6F48b, feeCollector: 0x2B7686Aff4595Ca1EbF9Ff6168C039b6A980222E, feeCollectorImpl: 0xa5D7a877297B31da1A3D0CcfdfC41D1C27428d36, - bgtIncentiveDistributor: 0x4200d596bE35b7AB8aD0c17E04b11c60F7AC2938, - bgtIncentiveDistributorImpl: 0xE6B7C8391eeB8b82EEdb2d015fcEaef4eC16c7D8, - bgtIncentiveFeeCollector: 0xBE4f441CcE02268Ca29C85DeBB558002E1133b25, - bgtIncentiveFeeCollectorImpl: 0x22256464F89582B65A6FaAdf8d562B6E89a6BA66, + bgtIncentiveDistributor: 0xf015eeC023E2Db26D2aa99D84b372E215bd59B65, + bgtIncentiveDistributorImpl: 0x36a91B80a4f74FE7cfeddF0fc24959d04b89203A, + incentivesCollector: 0xF158F72596415078803dcC0B8BF7723b0dA5Fcf6, + incentivesCollectorImpl: 0x319dE2A3Ab9D8F42548F439ABbb4CC4c20F3B489, wberaStakerVault: 0x806A948acc78DA018b76aE8afabB6B71Ab95D3DB, wberaStakerVaultImpl: 0xEBf7759047f1027B4cC9de0211d611e23841C1e1, wberaStakerVaultWithdrawalRequest: 0x8bbFF3F485B1263CFb1960e7505FC6456dC14D5B, wberaStakerVaultWithdrawalRequestImpl: 0x2C7231a59EeC62658D7ca01f1d4A557bda1029A3, rewardVaultHelper: 0x3dD313F3d08fAD4220CA0f153A0b984932567716, - rewardVaultHelperImpl: 0x19DA32A86BFfee117527c34243ccba7B3d155b65, + rewardVaultHelperImpl: 0xF1F828f1AdE86896bD77e18833581fC52fc2982A, rewardAllocatorFactory: 0x36886B62Cbfd2d7278C3F045B44f29E42153Ea89, rewardAllocatorFactoryImpl: 0x9aC5cB10145085cf84a8291243F5e832C62534A2, lstStakerVaultFactory: 0x31b5Cf9a4F89cEE50a779E95B6b8e6a1D7E4E058, - lstStakerVaultFactoryImpl: 0x203b42Cdc6216253f6576aa5cdB9BeB74bbFB963, + lstStakerVaultFactoryImpl: 0xC7acFFe09e857170aFAbBAF27B982dC4f0F5eaBe, lstStakerVaultImpl: 0xC891E5dfE7982c99F1eF5aD36f08FCE03652300c, lstStakerVaultWithdrawalRequestImpl: 0xAe88Db65a31E4D23Ddb75b6c64F24Ae26ef098E7, dedicatedEmissionStreamManager: 0x8a3EB29D2E634FA10F70496BDA230b65f73f1dF1, diff --git a/script/pol/POLPredictAddresses.s.sol b/script/pol/POLPredictAddresses.s.sol index ea4a8b0..6760518 100644 --- a/script/pol/POLPredictAddresses.s.sol +++ b/script/pol/POLPredictAddresses.s.sol @@ -12,7 +12,7 @@ import { Distributor } from "src/pol/rewards/Distributor.sol"; import { BGTStaker } from "src/pol/BGTStaker.sol"; import { FeeCollector } from "src/pol/FeeCollector.sol"; import { WBERAStakerVault } from "src/pol/WBERAStakerVault.sol"; -import { BGTIncentiveFeeCollector } from "src/pol/BGTIncentiveFeeCollector.sol"; +import { IncentivesCollector } from "src/pol/IncentivesCollector.sol"; import { BGTIncentiveDistributor } from "src/pol/rewards/BGTIncentiveDistributor.sol"; import { WBERAStakerVaultWithdrawalRequest } from "src/pol/WBERAStakerVaultWithdrawalRequest.sol"; import { RewardVaultHelper } from "src/pol/rewards/RewardVaultHelper.sol"; @@ -42,8 +42,8 @@ contract POLPredictAddressesScript is BasePredictScript, AddressBook { _predictAddress("FeeCollector Impl", type(FeeCollector).creationCode); _predictProxyAddress("BGTIncentiveDistributor", type(BGTIncentiveDistributor).creationCode); _predictAddress("BGTIncentiveDistributor Impl", type(BGTIncentiveDistributor).creationCode); - _predictProxyAddress("BGT Incentive Fee Collector", type(BGTIncentiveFeeCollector).creationCode); - _predictAddress("BGT Incentive Fee Collector Impl", type(BGTIncentiveFeeCollector).creationCode); + _predictProxyAddress("Incentives Collector", type(IncentivesCollector).creationCode); + _predictAddress("Incentives Collector Impl", type(IncentivesCollector).creationCode); _predictProxyAddress("WBERA Staker Vault", type(WBERAStakerVault).creationCode); _predictAddress("WBERA Staker Vault Impl", type(WBERAStakerVault).creationCode); _predictProxyAddress( diff --git a/script/pol/actions/ChangePOLParameters.s.sol b/script/pol/actions/ChangePOLParameters.s.sol deleted file mode 100644 index e47c39a..0000000 --- a/script/pol/actions/ChangePOLParameters.s.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import { BaseScript } from "../../base/Base.s.sol"; -import { BlockRewardController } from "../../../src/pol/rewards/BlockRewardController.sol"; -import { ConfigPOL } from "../logic/ConfigPOL.sol"; -import { Storage } from "../../base/Storage.sol"; -import { AddressBook } from "../../base/AddressBook.sol"; - -/// @dev Sender must have permission to update parameters. -contract ChangePOLParametersScript is BaseScript, Storage, ConfigPOL, AddressBook { - // POL - BlockRewardController params - // The constant base rate for BGT. - uint256 internal constant BASE_RATE = 0.5e18; - // The reward rate for BGT. - uint256 internal constant REWARD_RATE = 1.5e18; - // The minimum reward rate for BGT after accounting for validator boosts. - uint256 internal constant MIN_BOOSTED_REWARD_RATE = 0; - // The boost mutliplier param in the function, determines the inflation cap, 18 dec. - uint256 internal constant BOOST_MULTIPLIER = 3.5e18; - // The reward convexity param in the function, determines how fast it converges to its max, 18 dec. - uint256 internal constant REWARD_CONVEXITY = 0.4e18; - - function run() public virtual broadcast { - blockRewardController = BlockRewardController(_polAddresses.blockRewardController); - _setPOLParams(BASE_RATE, REWARD_RATE, MIN_BOOSTED_REWARD_RATE, BOOST_MULTIPLIER, REWARD_CONVEXITY); - } -} diff --git a/script/pol/actions/ChangePOLParametersProposal.s.sol b/script/pol/actions/ChangePOLParametersProposal.s.sol deleted file mode 100644 index 63081ba..0000000 --- a/script/pol/actions/ChangePOLParametersProposal.s.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import { console2 } from "forge-std/console2.sol"; -import { BaseScript } from "../../base/Base.s.sol"; -import { BerachainGovernance } from "../../../src/gov/BerachainGovernance.sol"; -import { BlockRewardController } from "../../../src/pol/rewards/BlockRewardController.sol"; -import { AddressBook } from "../../base/AddressBook.sol"; - -/// @dev Create a proposal to change POL parameters -contract ChangePOLParametersProposalScript is BaseScript, AddressBook { - // POL - BlockRewardController params - // The constant base rate for BGT. - uint256 internal constant BASE_RATE = 0.5e18; - // The reward rate for BGT. - uint256 internal constant REWARD_RATE = 1.5e18; - // The minimum reward rate for BGT after accounting for validator boosts. - uint256 internal constant MIN_BOOSTED_REWARD_RATE = 0; - // The boost mutliplier param in the function, determines the inflation cap, 18 dec. - uint256 internal constant BOOST_MULTIPLIER = 3.5e18; - // The reward convexity param in the function, determines how fast it converges to its max, 18 dec. - uint256 internal constant REWARD_CONVEXITY = 0.4e18; - - string internal constant PROPOSAL_DESCRIPTION = "Update POL parameters"; - - function run() public virtual broadcast { - console2.log("Creating proposal to change POL parameters..."); - BerachainGovernance gov = BerachainGovernance(payable(_governanceAddresses.governance)); - - address[] memory _targets = new address[](5); - for (uint256 i = 0; i < _targets.length; i++) { - _targets[i] = _polAddresses.blockRewardController; - } - - uint256[] memory _values = new uint256[](5); - for (uint256 i = 0; i < _values.length; i++) { - _values[i] = 0; - } - - bytes[] memory _calldatas = new bytes[](5); - // set base rate - _calldatas[0] = abi.encodeWithSelector(BlockRewardController.setBaseRate.selector, BASE_RATE, bytes("")); - // set reward rate - _calldatas[1] = abi.encodeWithSelector(BlockRewardController.setRewardRate.selector, REWARD_RATE, bytes("")); - // set min boosted reward rate - _calldatas[2] = abi.encodeWithSelector( - BlockRewardController.setMinBoostedRewardRate.selector, MIN_BOOSTED_REWARD_RATE, bytes("") - ); - // set boost multiplier - _calldatas[3] = - abi.encodeWithSelector(BlockRewardController.setBoostMultiplier.selector, BOOST_MULTIPLIER, bytes("")); - // set reward convexity - _calldatas[4] = - abi.encodeWithSelector(BlockRewardController.setRewardConvexity.selector, REWARD_CONVEXITY, bytes("")); - - uint256 proposalId = gov.propose(_targets, _values, _calldatas, PROPOSAL_DESCRIPTION); - console2.log("Proposal ID:", proposalId); - } -} diff --git a/script/pol/actions/WhitelistIncentiveToken.s.sol b/script/pol/actions/WhitelistIncentiveToken.s.sol index 1c21d7d..c7c98ef 100644 --- a/script/pol/actions/WhitelistIncentiveToken.s.sol +++ b/script/pol/actions/WhitelistIncentiveToken.s.sol @@ -42,7 +42,7 @@ contract WhitelistIncentiveTokenScript is BaseScript { internal { // Reverts if the token is already whitelisted. - RewardVault(vault).whitelistIncentiveToken(token, minIncentiveRate, manager); + RewardVault(payable(vault)).whitelistIncentiveToken(token, minIncentiveRate, manager); console2.log("Whitelisted incentive token %s for reward vault %s with manager %s", token, vault, manager); console2.log("Minimum incentive rate: %d", minIncentiveRate); } diff --git a/script/pol/deployment/4_TransferPOLOwnership.s.sol b/script/pol/deployment/4_TransferPOLOwnership.s.sol index da004c6..c34504e 100644 --- a/script/pol/deployment/4_TransferPOLOwnership.s.sol +++ b/script/pol/deployment/4_TransferPOLOwnership.s.sol @@ -155,7 +155,7 @@ contract TransferPOLOwnershipScript is RBAC, BaseScript, Storage, AddressBook { _validateCode("BeraChef", _polAddresses.beraChef); beraChef = BeraChef(_polAddresses.beraChef); _validateCode("BlockRewardController", _polAddresses.blockRewardController); - blockRewardController = BlockRewardController(_polAddresses.blockRewardController); + blockRewardController = BlockRewardController(payable(_polAddresses.blockRewardController)); _validateCode("Distributor", _polAddresses.distributor); distributor = Distributor(_polAddresses.distributor); _validateCode("RewardVaultFactory", _polAddresses.rewardVaultFactory); diff --git a/script/pol/deployment/7_DeployBGTIncentiveFee.s.sol b/script/pol/deployment/7_DeployBGTIncentiveFee.s.sol index 1a00e25..0688ada 100644 --- a/script/pol/deployment/7_DeployBGTIncentiveFee.s.sol +++ b/script/pol/deployment/7_DeployBGTIncentiveFee.s.sol @@ -6,9 +6,9 @@ import { BaseDeployScript } from "../../base/BaseDeploy.s.sol"; import { RBAC } from "../../base/RBAC.sol"; import { Storage } from "../../base/Storage.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { BGTIncentiveFeeDeployer } from "src/pol/BGTIncentiveFeeDeployer.sol"; +import { IncentivesCollectorDeployer } from "src/pol/IncentivesCollectorDeployer.sol"; import { WBERAStakerVault } from "src/pol/WBERAStakerVault.sol"; -import { BGTIncentiveFeeCollector } from "src/pol/BGTIncentiveFeeCollector.sol"; +import { IncentivesCollector } from "src/pol/IncentivesCollector.sol"; contract DeployBGTIncentiveFeeScript is BaseDeployScript, RBAC, Storage { // The amount to be paid out to the incentive fee collector in order to claim fees. @@ -21,7 +21,7 @@ contract DeployBGTIncentiveFeeScript is BaseDeployScript, RBAC, Storage { IERC20 public constant WBERA = IERC20(0x6969696969696969696969696969696969696969); function run() public broadcast { - console2.log("deploying BGTIncentiveFeeDeployer"); + console2.log("deploying IncentivesCollectorDeployer"); console2.log("Broadcaster address:", msg.sender); console2.log("WBERA balance of broadcaster:", WBERA.balanceOf(msg.sender)); @@ -30,45 +30,45 @@ contract DeployBGTIncentiveFeeScript is BaseDeployScript, RBAC, Storage { msg.sender, PAYOUT_AMOUNT, _saltsForProxy(type(WBERAStakerVault).creationCode), - _saltsForProxy(type(BGTIncentiveFeeCollector).creationCode) + _saltsForProxy(type(IncentivesCollector).creationCode) ); - address predictedAddress = _predictAddressWithArgs(type(BGTIncentiveFeeDeployer).creationCode, args); - console2.log("BGTIncentiveFeeDeployer predicted address:", predictedAddress); + address predictedAddress = _predictAddressWithArgs(type(IncentivesCollectorDeployer).creationCode, args); + console2.log("IncentivesCollectorDeployer predicted address:", predictedAddress); // approve the deployer to spend the tokens WBERA.approve(predictedAddress, INITIAL_DEPOSIT_AMOUNT); // log the allowance console2.log("WBERA allowance of the deployer:", WBERA.allowance(msg.sender, predictedAddress)); - // deploy the BGTIncentiveFeeDeployer - BGTIncentiveFeeDeployer bgtIncentiveFeeDeployer = BGTIncentiveFeeDeployer( + // deploy the IncentivesCollectorDeployer + IncentivesCollectorDeployer incentivesCollectorDeployer = IncentivesCollectorDeployer( _deployWithArgs( - "BGTIncentiveFeeDeployer", type(BGTIncentiveFeeDeployer).creationCode, args, predictedAddress + "IncentivesCollectorDeployer", type(IncentivesCollectorDeployer).creationCode, args, predictedAddress ) ); - wberaStakerVault = bgtIncentiveFeeDeployer.wberaStakerVault(); - bgtIncentiveFeeCollector = bgtIncentiveFeeDeployer.bgtIncentiveFeeCollector(); + wberaStakerVault = incentivesCollectorDeployer.wberaStakerVault(); + incentivesCollector = incentivesCollectorDeployer.incentivesCollector(); - console2.log("BGTIncentiveFeeDeployer deployed at", address(bgtIncentiveFeeDeployer)); + console2.log("IncentivesCollectorDeployer deployed at", address(incentivesCollectorDeployer)); console2.log("WBERAStakerVault deployed at", address(wberaStakerVault)); - console2.log("BGTIncentiveFeeCollector deployed at", address(bgtIncentiveFeeCollector)); + console2.log("IncentivesCollector deployed at", address(incentivesCollector)); // grant MANAGER and PAUSER roles to the deployer RBAC.AccountDescription memory deployer = RBAC.AccountDescription({ name: "deployer", addr: msg.sender }); RBAC.RoleDescription memory incentiveFeeCollectorManagerRole = RBAC.RoleDescription({ - contractName: "BGTIncentiveFeeCollector", - contractAddr: address(bgtIncentiveFeeCollector), + contractName: "IncentivesCollector", + contractAddr: address(incentivesCollector), name: "MANAGER_ROLE", - role: bgtIncentiveFeeCollector.MANAGER_ROLE() + role: incentivesCollector.MANAGER_ROLE() }); RBAC.RoleDescription memory incentiveFeeCollectorPauserRole = RBAC.RoleDescription({ - contractName: "BGTIncentiveFeeCollector", - contractAddr: address(bgtIncentiveFeeCollector), + contractName: "IncentivesCollector", + contractAddr: address(incentivesCollector), name: "PAUSER_ROLE", - role: bgtIncentiveFeeCollector.PAUSER_ROLE() + role: incentivesCollector.PAUSER_ROLE() }); RBAC.RoleDescription memory wberaStakerVaultManagerRole = RBAC.RoleDescription({ @@ -91,6 +91,6 @@ contract DeployBGTIncentiveFeeScript is BaseDeployScript, RBAC, Storage { _grantRole(incentiveFeeCollectorManagerRole, deployer); _grantRole(incentiveFeeCollectorPauserRole, deployer); - console2.log("Granted MANAGER and PAUSER roles to the deployer for BGTIncentiveFeeCollector"); + console2.log("Granted MANAGER and PAUSER roles to the deployer for IncentivesCollector"); } } diff --git a/script/pol/deployment/8_TransferPOLV2Ownership.s.sol b/script/pol/deployment/8_TransferPOLV2Ownership.s.sol index 6178efe..d225654 100644 --- a/script/pol/deployment/8_TransferPOLV2Ownership.s.sol +++ b/script/pol/deployment/8_TransferPOLV2Ownership.s.sol @@ -28,7 +28,7 @@ contract TransferPOLV2OwnershipScript is RBAC, BaseScript, Storage, AddressBook RBAC.AccountDescription memory deployer = RBAC.AccountDescription({ name: "deployer", addr: msg.sender }); _transferWBERAStakerVaultOwnership(deployer, governance); - _transferBGTIncentiveFeeCollectorOwnership(deployer, governance); + _transferIncentivesCollectorOwnership(deployer, governance); } function _transferWBERAStakerVaultOwnership( @@ -61,40 +61,40 @@ contract TransferPOLV2OwnershipScript is RBAC, BaseScript, Storage, AddressBook _transferRole(wberaStakerVaultAdminRole, deployer, governance); } - function _transferBGTIncentiveFeeCollectorOwnership( + function _transferIncentivesCollectorOwnership( RBAC.AccountDescription memory deployer, RBAC.AccountDescription memory governance ) internal { - RBAC.RoleDescription memory bgtIncentiveFeeCollectorAdminRole = RBAC.RoleDescription({ - contractName: "BGTIncentiveFeeCollector", - contractAddr: _polAddresses.bgtIncentiveFeeCollector, + RBAC.RoleDescription memory incentivesCollectorAdminRole = RBAC.RoleDescription({ + contractName: "IncentivesCollector", + contractAddr: _polAddresses.incentivesCollector, name: "DEFAULT_ADMIN_ROLE", - role: bgtIncentiveFeeCollector.DEFAULT_ADMIN_ROLE() + role: incentivesCollector.DEFAULT_ADMIN_ROLE() }); - RBAC.RoleDescription memory bgtIncentiveFeeCollectorManagerRole = RBAC.RoleDescription({ - contractName: "BGTIncentiveFeeCollector", - contractAddr: _polAddresses.bgtIncentiveFeeCollector, + RBAC.RoleDescription memory incentivesCollectorManagerRole = RBAC.RoleDescription({ + contractName: "IncentivesCollector", + contractAddr: _polAddresses.incentivesCollector, name: "MANAGER_ROLE", - role: bgtIncentiveFeeCollector.MANAGER_ROLE() + role: incentivesCollector.MANAGER_ROLE() }); - RBAC.RoleDescription memory bgtIncentiveFeeCollectorPauserRole = RBAC.RoleDescription({ - contractName: "BGTIncentiveFeeCollector", - contractAddr: _polAddresses.bgtIncentiveFeeCollector, + RBAC.RoleDescription memory incentivesCollectorPauserRole = RBAC.RoleDescription({ + contractName: "IncentivesCollector", + contractAddr: _polAddresses.incentivesCollector, name: "PAUSER_ROLE", - role: bgtIncentiveFeeCollector.PAUSER_ROLE() + role: incentivesCollector.PAUSER_ROLE() }); - console2.log("Transferring ownership of BGTIncentiveFeeCollector contract..."); - _transferRole(bgtIncentiveFeeCollectorPauserRole, deployer, governance); - _transferRole(bgtIncentiveFeeCollectorManagerRole, deployer, governance); - _transferRole(bgtIncentiveFeeCollectorAdminRole, deployer, governance); + console2.log("Transferring ownership of IncentivesCollector contract..."); + _transferRole(incentivesCollectorPauserRole, deployer, governance); + _transferRole(incentivesCollectorManagerRole, deployer, governance); + _transferRole(incentivesCollectorAdminRole, deployer, governance); } function _loadStorageContracts() internal { _validateCode("WBERAStakerVault", _polAddresses.wberaStakerVault); - _validateCode("BGTIncentiveFeeCollector", _polAddresses.bgtIncentiveFeeCollector); + _validateCode("IncentivesCollector", _polAddresses.incentivesCollector); wberaStakerVault = WBERAStakerVault(payable(_polAddresses.wberaStakerVault)); - bgtIncentiveFeeCollector = BGTIncentiveFeeCollector(_polAddresses.bgtIncentiveFeeCollector); + incentivesCollector = IncentivesCollector(_polAddresses.incentivesCollector); } } diff --git a/script/pol/logic/ConfigPOL.sol b/script/pol/logic/ConfigPOL.sol index 267abdf..92004e5 100644 --- a/script/pol/logic/ConfigPOL.sol +++ b/script/pol/logic/ConfigPOL.sol @@ -6,52 +6,6 @@ import { Storage } from "../../base/Storage.sol"; /// @dev This contract is used to configure the POL contracts. abstract contract ConfigPOL is Storage { - /// @dev Set the POL params - function _setPOLParams( - uint256 baseRate, - uint256 rewardRate, - uint256 minBoostedRewardRate, - uint256 boostMultiplier, - uint256 rewardConvexity - ) - internal - { - console2.log("\n\nConfiguring POL contracts..."); - // Config BlockRewardController - // Set the base rate. - blockRewardController.setBaseRate(baseRate); - require(blockRewardController.baseRate() == baseRate, "ConfigPOL: failed to set base rate"); - console2.log("Set the base rate to be %d BGT per block", baseRate); - - // Set the reward rate. - blockRewardController.setRewardRate(rewardRate); - require(blockRewardController.rewardRate() == rewardRate, "ConfigPOL: failed to set reward rate"); - console2.log("Set the reward rate to be %d BGT per block", rewardRate); - - // Set the min boosted reward rate. - blockRewardController.setMinBoostedRewardRate(minBoostedRewardRate); - require( - blockRewardController.minBoostedRewardRate() == minBoostedRewardRate, - "ConfigPOL: failed to set min boosted reward rate" - ); - console2.log("Set the min boosted reward rate to be %d BGT per block", minBoostedRewardRate); - - // Set the boost multiplier parameter. - blockRewardController.setBoostMultiplier(boostMultiplier); - require( - blockRewardController.boostMultiplier() == boostMultiplier, "ConfigPOL: failed to set boost multiplier" - ); - console2.log("Set the boost multiplier param to be %d", boostMultiplier); - - // Set the reward convexity parameter. - blockRewardController.setRewardConvexity(rewardConvexity); - require( - blockRewardController.rewardConvexity() == int256(rewardConvexity), - "ConfigPOL: failed to set reward convexity" - ); - console2.log("Set the reward convexity param to be %d", rewardConvexity); - } - /// @dev Set the reward allocation block delay function _setRewardAllocationBlockDelay(uint64 rewardAllocationBlockDelay) internal { console2.log("\n\nSetting reward allocation block delay on BeraChef..."); diff --git a/script/pol/upgrade/UpgradeBGTIncentiveDistributor.s.sol b/script/pol/upgrade/UpgradeBGTIncentiveDistributor.s.sol new file mode 100644 index 0000000..2f1b555 --- /dev/null +++ b/script/pol/upgrade/UpgradeBGTIncentiveDistributor.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { BGTIncentiveDistributor } from "src/pol/rewards/BGTIncentiveDistributor.sol"; +import { BaseERC1967UpgradeScript } from "../../base/BaseUpgrade.s.sol"; + +contract UpgradeBGTIncentiveDistributorScript is BaseERC1967UpgradeScript { + function _proxyAddress() internal view override returns (address) { + return _polAddresses.bgtIncentiveDistributor; + } + + function _deployNewImplementation() internal override returns (address) { + return _deploy( + "BGTIncentiveDistributor", + type(BGTIncentiveDistributor).creationCode, + _polAddresses.bgtIncentiveDistributorImpl + ); + } +} diff --git a/script/pol/upgrade/UpgradeBGTIncentiveFeeCollector.s.sol b/script/pol/upgrade/UpgradeBGTIncentiveFeeCollector.s.sol deleted file mode 100644 index 29afec3..0000000 --- a/script/pol/upgrade/UpgradeBGTIncentiveFeeCollector.s.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import { console2 } from "forge-std/Script.sol"; -import { AddressBook } from "script/base/AddressBook.sol"; -import { BaseDeployScript } from "script/base/BaseDeploy.s.sol"; -import { ChainHelper } from "script/base/Chain.sol"; -import { BGTIncentiveFeeCollector } from "src/pol/BGTIncentiveFeeCollector.sol"; - -contract UpgradeBGTIncentiveFeeCollectorScript is BaseDeployScript, AddressBook { - function run() public pure { - console2.log("Please run specific function."); - } - - function deployNewImplementation() public broadcast { - address newBGTIncentiveFeeCollectorImpl = _deployNewImplementation(); - console2.log("New BGTIncentiveFeeCollector implementation address:", newBGTIncentiveFeeCollectorImpl); - } - - /// @dev This function is only for testnet or test purposes. - function upgradeToTestnet() public broadcast { - address newBGTIncentiveFeeCollectorImpl = _deployNewImplementation(); - console2.log("New BGTIncentiveFeeCollector implementation address:", newBGTIncentiveFeeCollectorImpl); - BGTIncentiveFeeCollector(_polAddresses.bgtIncentiveFeeCollector) - .upgradeToAndCall(newBGTIncentiveFeeCollectorImpl, bytes("")); - console2.log("BGTIncentiveFeeCollector upgraded successfully"); - } - - function _deployNewImplementation() internal returns (address) { - return _deploy( - "BGTIncentiveFeeCollector", - type(BGTIncentiveFeeCollector).creationCode, - _polAddresses.bgtIncentiveFeeCollectorImpl - ); - } -} diff --git a/script/pol/upgrade/UpgradeBeraChef.s.sol b/script/pol/upgrade/UpgradeBeraChef.s.sol index 1e14140..2e83992 100644 --- a/script/pol/upgrade/UpgradeBeraChef.s.sol +++ b/script/pol/upgrade/UpgradeBeraChef.s.sol @@ -2,25 +2,14 @@ pragma solidity 0.8.26; import { console2 } from "forge-std/Script.sol"; -import { BaseDeployScript } from "../../base/BaseDeploy.s.sol"; import { BeraChef } from "src/pol/rewards/BeraChef.sol"; -import { AddressBook } from "../../base/AddressBook.sol"; +import { BaseERC1967UpgradeScript } from "../../base/BaseUpgrade.s.sol"; -contract UpgradeBeraChefScript is BaseDeployScript, AddressBook { +contract UpgradeBeraChefScript is BaseERC1967UpgradeScript { // Equal to MAX_COMMISSION_CHANGE_DELAY uint64 constant COMMISSION_CHANGE_DELAY = 2 * 8191; - uint64 constant STARTING_VALUE_MAX_WEIGHT_PER_VAULT = 1e4; - function run() public pure { - console2.log("Please run specific function."); - } - - function deployNewImplementation() public broadcast { - address newBeraChefImpl = _deployNewImplementation(); - console2.log("New BeraChef implementation address:", newBeraChefImpl); - } - function printSetCommissionChangeDelayCallSignature() public pure { console2.logBytes(abi.encodeCall(BeraChef.setCommissionChangeDelay, (COMMISSION_CHANGE_DELAY))); } @@ -29,15 +18,11 @@ contract UpgradeBeraChefScript is BaseDeployScript, AddressBook { console2.logBytes(abi.encodeCall(BeraChef.setMaxWeightPerVault, (STARTING_VALUE_MAX_WEIGHT_PER_VAULT))); } - /// @dev This function is only for testnet or test purposes. - function upgradeToAndCallTestnet(bytes memory callSignature) public broadcast { - address newBeraChefImpl = _deployNewImplementation(); - console2.log("New BeraChef implementation address:", newBeraChefImpl); - BeraChef(_polAddresses.beraChef).upgradeToAndCall(newBeraChefImpl, callSignature); - console2.log("BeraChef upgraded successfully"); + function _proxyAddress() internal view override returns (address) { + return _polAddresses.beraChef; } - function _deployNewImplementation() internal returns (address) { + function _deployNewImplementation() internal override returns (address) { return _deploy("BeraChef", type(BeraChef).creationCode, _polAddresses.beraChefImpl); } } diff --git a/script/pol/upgrade/UpgradeBlockRewardController.s.sol b/script/pol/upgrade/UpgradeBlockRewardController.s.sol new file mode 100644 index 0000000..12ebde1 --- /dev/null +++ b/script/pol/upgrade/UpgradeBlockRewardController.s.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { BlockRewardController } from "src/pol/rewards/BlockRewardController.sol"; +import { BaseERC1967UpgradeScript } from "../../base/BaseUpgrade.s.sol"; + +contract UpgradeBlockRewardControllerScript is BaseERC1967UpgradeScript { + function _proxyAddress() internal view override returns (address) { + return _polAddresses.blockRewardController; + } + + function _deployNewImplementation() internal override returns (address) { + return _deploy( + "BlockRewardController", type(BlockRewardController).creationCode, _polAddresses.blockRewardControllerImpl + ); + } +} diff --git a/script/pol/upgrade/UpgradeDedicatedEmissionStreamManager.s.sol b/script/pol/upgrade/UpgradeDedicatedEmissionStreamManager.s.sol index 58551a4..33d5bc4 100644 --- a/script/pol/upgrade/UpgradeDedicatedEmissionStreamManager.s.sol +++ b/script/pol/upgrade/UpgradeDedicatedEmissionStreamManager.s.sol @@ -1,34 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import { console2 } from "forge-std/Script.sol"; -import { BaseDeployScript } from "../../base/BaseDeploy.s.sol"; import { DedicatedEmissionStreamManager } from "src/pol/rewards/DedicatedEmissionStreamManager.sol"; +import { BaseERC1967UpgradeScript } from "script/base/BaseUpgrade.s.sol"; -import { AddressBook } from "script/base/AddressBook.sol"; - -contract UpgradeDedicatedEmissionStreamManagerScript is BaseDeployScript, AddressBook { - function run() public pure { - console2.log("Please run specific function."); - } - - function deployNewDedicatedEmissionStreamManagerImplementation() public broadcast { - address newDedicatedEmissionStreamManagerImpl = _deployDedicatedEmissionStreamManagerNewImpl(); - console2.log( - "New DedicatedEmissionStreamManager implementation address:", newDedicatedEmissionStreamManagerImpl - ); - } - - /// @dev This function is only for testnet or test purposes. - function upgradeToAndCallTestnet(bytes memory callSignature) public broadcast { - address newImpl = _deployDedicatedEmissionStreamManagerNewImpl(); - console2.log("New DedicatedEmissionStreamManager implementation address:", newImpl); - DedicatedEmissionStreamManager(payable(_polAddresses.dedicatedEmissionStreamManager)) - .upgradeToAndCall(newImpl, callSignature); - console2.log("DedicatedEmissionStreamManager upgraded successfully"); +contract UpgradeDedicatedEmissionStreamManagerScript is BaseERC1967UpgradeScript { + function _proxyAddress() internal view override returns (address) { + return _polAddresses.dedicatedEmissionStreamManager; } - function _deployDedicatedEmissionStreamManagerNewImpl() internal returns (address) { + function _deployNewImplementation() internal override returns (address) { return _deploy( "DedicatedEmissionStreamManager", type(DedicatedEmissionStreamManager).creationCode, diff --git a/script/pol/upgrade/UpgradeDistributor.s.sol b/script/pol/upgrade/UpgradeDistributor.s.sol index 26f02e7..f609c53 100644 --- a/script/pol/upgrade/UpgradeDistributor.s.sol +++ b/script/pol/upgrade/UpgradeDistributor.s.sol @@ -1,31 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import { console2 } from "forge-std/Script.sol"; -import { BaseDeployScript } from "../../base/BaseDeploy.s.sol"; import { Distributor } from "src/pol/rewards/Distributor.sol"; +import { BaseERC1967UpgradeScript } from "../../base/BaseUpgrade.s.sol"; -import { AddressBook } from "../../base/AddressBook.sol"; - -contract UpgradeDistributorScript is BaseDeployScript, AddressBook { - function run() public pure { - console2.log("Please run specific function."); - } - - function deployNewImplementation() public broadcast { - address newDistributorImpl = _deployNewImplementation(); - console2.log("New Distributor implementation address:", newDistributorImpl); - } - - /// @dev This function is only for testnet or test purposes. - function upgradeToAndCallTestnet(bytes memory callSignature) public broadcast { - address newDistributorImpl = _deployNewImplementation(); - console2.log("New Distributor implementation address:", newDistributorImpl); - Distributor(_polAddresses.distributor).upgradeToAndCall(newDistributorImpl, callSignature); - console2.log("Distributor upgraded successfully"); +contract UpgradeDistributorScript is BaseERC1967UpgradeScript { + function _proxyAddress() internal view override returns (address) { + return _polAddresses.distributor; } - function _deployNewImplementation() internal returns (address) { + function _deployNewImplementation() internal override returns (address) { return _deploy("Distributor", type(Distributor).creationCode, _polAddresses.distributorImpl); } } diff --git a/script/pol/upgrade/UpgradeFeeCollector.s.sol b/script/pol/upgrade/UpgradeFeeCollector.s.sol new file mode 100644 index 0000000..fa4169a --- /dev/null +++ b/script/pol/upgrade/UpgradeFeeCollector.s.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { FeeCollector } from "src/pol/FeeCollector.sol"; +import { BaseERC1967UpgradeScript } from "script/base/BaseUpgrade.s.sol"; + +contract UpgradeFeeCollectorScript is BaseERC1967UpgradeScript { + function _proxyAddress() internal view override returns (address) { + return _polAddresses.feeCollector; + } + + function _deployNewImplementation() internal override returns (address) { + return _deploy("FeeCollector", type(FeeCollector).creationCode, _polAddresses.feeCollectorImpl); + } +} diff --git a/script/pol/upgrade/UpgradeIncentiveCollector.s.sol b/script/pol/upgrade/UpgradeIncentiveCollector.s.sol new file mode 100644 index 0000000..be576f1 --- /dev/null +++ b/script/pol/upgrade/UpgradeIncentiveCollector.s.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { IncentivesCollector } from "src/pol/IncentivesCollector.sol"; +import { BaseERC1967UpgradeScript } from "script/base/BaseUpgrade.s.sol"; + +contract UpgradeIncentivesCollectorScript is BaseERC1967UpgradeScript { + function _proxyAddress() internal view override returns (address) { + return _polAddresses.incentivesCollector; + } + + function _deployNewImplementation() internal override returns (address) { + return _deploy( + "IncentivesCollector", type(IncentivesCollector).creationCode, _polAddresses.incentivesCollectorImpl + ); + } +} diff --git a/script/pol/upgrade/UpgradeLSTStakerVault.s.sol b/script/pol/upgrade/UpgradeLSTStakerVault.s.sol index ab86959..0ed9f96 100644 --- a/script/pol/upgrade/UpgradeLSTStakerVault.s.sol +++ b/script/pol/upgrade/UpgradeLSTStakerVault.s.sol @@ -1,66 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import { console2 } from "forge-std/Script.sol"; -import { UpgradeableBeacon } from "solady/src/utils/UpgradeableBeacon.sol"; - -import { AddressBook } from "script/base/AddressBook.sol"; -import { BaseDeployScript } from "script/base/BaseDeploy.s.sol"; -import { ChainHelper } from "script/base/Chain.sol"; import { LSTStakerVault } from "src/pol/lst/LSTStakerVault.sol"; import { LSTStakerVaultFactory } from "src/pol/lst/LSTStakerVaultFactory.sol"; -import { LSTStakerVaultWithdrawalRequest } from "src/pol/lst/LSTStakerVaultWithdrawalRequest.sol"; - -contract UpgradeLSTStakerVaultScript is BaseDeployScript, AddressBook { - function run() public pure { - console2.log("Please run specific function."); - } +import { BaseBeaconUpgradeScript } from "script/base/BaseUpgrade.s.sol"; - function deployNewVaultImplementation() public broadcast { - address newLSTStakerVaultImpl = _deployVaultNewImpl(); - console2.log("New LSTStakerVault implementation address:", newLSTStakerVaultImpl); +contract UpgradeLSTStakerVaultScript is BaseBeaconUpgradeScript { + function _beaconAddress() internal view override returns (address) { + return LSTStakerVaultFactory(_polAddresses.lstStakerVaultFactory).vaultBeacon(); } - function deployNewWithdrawal721Implementation() public broadcast { - address newLSTStakerVaultWithdrawalRequestImpl = _deployWithdrawal721NewImpl(); - console2.log( - "New LSTStakerVaultWithdrawalRequest implementation address:", newLSTStakerVaultWithdrawalRequestImpl - ); - } - - /// @dev This function is only for testnet or test purposes. - function vaultUpgradeToTestnet() public broadcast { - LSTStakerVaultFactory factory = LSTStakerVaultFactory(_polAddresses.lstStakerVaultFactory); - address beacon = factory.vaultBeacon(); - console2.log("Upgrading beacon at:", beacon); - - address newImpl = _deployVaultNewImpl(); - console2.log("New LSTStakerVault implementation address:", newImpl); - UpgradeableBeacon(beacon).upgradeTo(newImpl); - console2.log("LSTStakerVault upgraded successfully"); - } - - /// @dev This function is only for testnet or test purposes. - function withdrawal721UpgradeToTestnet() public broadcast { - LSTStakerVaultFactory factory = LSTStakerVaultFactory(_polAddresses.lstStakerVaultFactory); - address beacon = factory.withdrawalBeacon(); - console2.log("Upgrading beacon at:", beacon); - - address newImpl = _deployWithdrawal721NewImpl(); - console2.log("New LSTStakerVaultWithdrawalRequest implementation address:", newImpl); - UpgradeableBeacon(beacon).upgradeTo(newImpl); - console2.log("LSTStakerVaultWithdrawalRequest upgraded successfully"); - } - - function _deployVaultNewImpl() internal returns (address) { + function _deployNewImplementation() internal override returns (address) { return _deploy("LSTStakerVault", type(LSTStakerVault).creationCode, _polAddresses.lstStakerVaultImpl); } - - function _deployWithdrawal721NewImpl() internal returns (address) { - return _deploy( - "LSTStakerVaultWithdrawalRequest", - type(LSTStakerVaultWithdrawalRequest).creationCode, - _polAddresses.lstStakerVaultWithdrawalRequestImpl - ); - } } diff --git a/script/pol/upgrade/UpgradeLSTStakerVaultFactory.s.sol b/script/pol/upgrade/UpgradeLSTStakerVaultFactory.s.sol new file mode 100644 index 0000000..ba2399a --- /dev/null +++ b/script/pol/upgrade/UpgradeLSTStakerVaultFactory.s.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { LSTStakerVaultFactory } from "src/pol/lst/LSTStakerVaultFactory.sol"; +import { BaseERC1967UpgradeScript } from "script/base/BaseUpgrade.s.sol"; + +contract UpgradeLSTStakerVaultFactoryScript is BaseERC1967UpgradeScript { + function _proxyAddress() internal view override returns (address) { + return _polAddresses.lstStakerVaultFactory; + } + + function _deployNewImplementation() internal override returns (address) { + return _deploy( + "LSTStakerVaultFactory", type(LSTStakerVaultFactory).creationCode, _polAddresses.lstStakerVaultFactoryImpl + ); + } +} diff --git a/script/pol/upgrade/UpgradeLSTStakerVaultWithdrawalRequest.s.sol b/script/pol/upgrade/UpgradeLSTStakerVaultWithdrawalRequest.s.sol new file mode 100644 index 0000000..00dcd9d --- /dev/null +++ b/script/pol/upgrade/UpgradeLSTStakerVaultWithdrawalRequest.s.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { LSTStakerVaultFactory } from "src/pol/lst/LSTStakerVaultFactory.sol"; +import { LSTStakerVaultWithdrawalRequest } from "src/pol/lst/LSTStakerVaultWithdrawalRequest.sol"; +import { BaseBeaconUpgradeScript } from "script/base/BaseUpgrade.s.sol"; + +contract UpgradeLSTStakerVaultWithdrawalRequestScript is BaseBeaconUpgradeScript { + function _beaconAddress() internal view override returns (address) { + return LSTStakerVaultFactory(_polAddresses.lstStakerVaultFactory).withdrawalBeacon(); + } + + function _deployNewImplementation() internal override returns (address) { + return _deploy( + "LSTStakerVaultWithdrawalRequest", + type(LSTStakerVaultWithdrawalRequest).creationCode, + _polAddresses.lstStakerVaultWithdrawalRequestImpl + ); + } +} diff --git a/script/pol/upgrade/UpgradeRewardVault.s.sol b/script/pol/upgrade/UpgradeRewardVault.s.sol index 868c452..d428752 100644 --- a/script/pol/upgrade/UpgradeRewardVault.s.sol +++ b/script/pol/upgrade/UpgradeRewardVault.s.sol @@ -1,35 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import { console2 } from "forge-std/Script.sol"; -import { UpgradeableBeacon } from "solady/src/utils/UpgradeableBeacon.sol"; - -import { BaseDeployScript } from "../../base/BaseDeploy.s.sol"; import { RewardVault } from "src/pol/rewards/RewardVault.sol"; import { RewardVaultFactory } from "src/pol/rewards/RewardVaultFactory.sol"; -import { AddressBook } from "../../base/AddressBook.sol"; - -contract UpgradeRewardVaultScript is BaseDeployScript, AddressBook { - function run() public pure { - console2.log("Please run specific function."); - } - - function deployNewImplementation() public broadcast { - address newRewardVaultImpl = _deployNewImplementation(); - console2.log("New rewardVault implementation address:", newRewardVaultImpl); - } - - /// @dev This function is only for testnet or test purposes. - function upgradeToTestnet() public broadcast { - address newRewardVaultImpl = _deployNewImplementation(); - console2.log("New rewardVault implementation address:", newRewardVaultImpl); +import { BaseBeaconUpgradeScript } from "../../base/BaseUpgrade.s.sol"; - address beacon = RewardVaultFactory(_polAddresses.rewardVaultFactory).beacon(); - UpgradeableBeacon(beacon).upgradeTo(newRewardVaultImpl); - console2.log("RewardVault upgraded successfully"); +contract UpgradeRewardVaultScript is BaseBeaconUpgradeScript { + function _beaconAddress() internal view override returns (address) { + return RewardVaultFactory(_polAddresses.rewardVaultFactory).beacon(); } - function _deployNewImplementation() internal returns (address) { + function _deployNewImplementation() internal override returns (address) { return _deploy("RewardVault", type(RewardVault).creationCode, _polAddresses.rewardVaultImpl); } } diff --git a/script/pol/upgrade/UpgradeRewardVaultFactory.s.sol b/script/pol/upgrade/UpgradeRewardVaultFactory.s.sol index ecb9465..4d365fd 100644 --- a/script/pol/upgrade/UpgradeRewardVaultFactory.s.sol +++ b/script/pol/upgrade/UpgradeRewardVaultFactory.s.sol @@ -1,37 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import { console2 } from "forge-std/Script.sol"; -import { BaseDeployScript } from "../../base/BaseDeploy.s.sol"; import { RewardVaultFactory } from "src/pol/rewards/RewardVaultFactory.sol"; +import { BaseERC1967UpgradeScript } from "../../base/BaseUpgrade.s.sol"; -import { AddressBook } from "../../base/AddressBook.sol"; - -contract UpgradeRewardVaultFactoryScript is BaseDeployScript, AddressBook { - function run() public pure { - console2.log("Please run specific function."); - } - - function deployNewImplementation() public broadcast { - address newRewardVaultFactoryImpl = _deployNewImplementation(); - console2.log("New RewardVaultFactory implementation address:", newRewardVaultFactoryImpl); - } - - function printSetBGTIncentiveDistributorCallSignature() public view { - console2.logBytes( - abi.encodeCall(RewardVaultFactory.setBGTIncentiveDistributor, (_polAddresses.bgtIncentiveDistributor)) - ); - } - - /// @dev This function is only for testnet or test purposes. - function upgradeToAndCallTestnet(bytes memory callSignature) public broadcast { - address newRewardVaultFactoryImpl = _deployNewImplementation(); - console2.log("New RewardVaultFactory implementation address:", newRewardVaultFactoryImpl); - RewardVaultFactory(_polAddresses.rewardVaultFactory).upgradeToAndCall(newRewardVaultFactoryImpl, callSignature); - console2.log("RewardVaultFactory upgraded successfully"); +contract UpgradeRewardVaultFactoryScript is BaseERC1967UpgradeScript { + function _proxyAddress() internal view override returns (address) { + return _polAddresses.rewardVaultFactory; } - function _deployNewImplementation() internal returns (address) { + function _deployNewImplementation() internal override returns (address) { return _deploy("RewardVaultFactory", type(RewardVaultFactory).creationCode, _polAddresses.rewardVaultFactoryImpl); } diff --git a/script/pol/upgrade/UpgradeRewardVaultHelper.s.sol b/script/pol/upgrade/UpgradeRewardVaultHelper.s.sol new file mode 100644 index 0000000..c378f1b --- /dev/null +++ b/script/pol/upgrade/UpgradeRewardVaultHelper.s.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { RewardVaultHelper } from "src/pol/rewards/RewardVaultHelper.sol"; +import { BaseERC1967UpgradeScript } from "../../base/BaseUpgrade.s.sol"; + +contract UpgradeRewardVaultHelperScript is BaseERC1967UpgradeScript { + function _proxyAddress() internal view override returns (address) { + return _polAddresses.rewardVaultHelper; + } + + function _deployNewImplementation() internal override returns (address) { + return _deploy("RewardVaultHelper", type(RewardVaultHelper).creationCode, _polAddresses.rewardVaultHelperImpl); + } +} diff --git a/script/pol/upgrade/UpgradeWBERAStakerVault.s.sol b/script/pol/upgrade/UpgradeWBERAStakerVault.s.sol index 10ce1e4..5c0a72e 100644 --- a/script/pol/upgrade/UpgradeWBERAStakerVault.s.sol +++ b/script/pol/upgrade/UpgradeWBERAStakerVault.s.sol @@ -2,28 +2,14 @@ pragma solidity 0.8.26; import { console2 } from "forge-std/Script.sol"; -import { BaseDeployScript } from "../../base/BaseDeploy.s.sol"; import { WBERAStakerVault } from "src/pol/WBERAStakerVault.sol"; -import { ChainHelper } from "script/base/Chain.sol"; import { WBERAStakerVaultWithdrawalRequest } from "src/pol/WBERAStakerVaultWithdrawalRequest.sol"; +import { BaseERC1967UpgradeScript } from "../../base/BaseUpgrade.s.sol"; -import { AddressBook } from "script/base/AddressBook.sol"; - -contract UpgradeWBERAStakerVaultScript is BaseDeployScript, AddressBook { - function run() public pure { - console2.log("Please run specific function."); - } - - function deployNewVaultImplementation() public broadcast { - address newWBERAStakerVaultImpl = _deployVaultNewImpl(); - console2.log("New WBERAStakerVault implementation address:", newWBERAStakerVaultImpl); - } - +contract UpgradeWBERAStakerVaultScript is BaseERC1967UpgradeScript { function deployNewWithdrawal721Implementation() public broadcast { - address newWBERAStakerVaultWithdrawalRequestImpl = _deployWithdrawal721NewImpl(); - console2.log( - "New WBERAStakerVaultWithdrawalRequest implementation address:", newWBERAStakerVaultWithdrawalRequestImpl - ); + address impl = _deployWithdrawal721NewImpl(); + console2.log("New WBERAStakerVaultWithdrawalRequest implementation address:", impl); } function printSetWithdrawalRequests721CallSignature() public view { @@ -32,15 +18,11 @@ contract UpgradeWBERAStakerVaultScript is BaseDeployScript, AddressBook { ); } - /// @dev This function is only for testnet or test purposes. - function upgradeToAndCallTestnet(bytes memory callSignature) public broadcast { - address newImpl = _deployVaultNewImpl(); - console2.log("New WBERAStakerVault implementation address:", newImpl); - WBERAStakerVault(payable(_polAddresses.wberaStakerVault)).upgradeToAndCall(newImpl, callSignature); - console2.log("WBERAStakerVault upgraded successfully"); + function _proxyAddress() internal view override returns (address) { + return _polAddresses.wberaStakerVault; } - function _deployVaultNewImpl() internal returns (address) { + function _deployNewImplementation() internal override returns (address) { return _deploy("WBERAStakerVault", type(WBERAStakerVault).creationCode, _polAddresses.wberaStakerVaultImpl); } diff --git a/src/base/FactoryOwnable.sol b/src/base/FactoryOwnable.sol index ab66320..9846988 100644 --- a/src/base/FactoryOwnable.sol +++ b/src/base/FactoryOwnable.sol @@ -115,10 +115,10 @@ abstract contract FactoryOwnable is Initializable { } } - /// @dev Returns the address of the BGTIncentiveDistributor contract. - function getBGTIncentiveDistributor() internal view returns (address) { + /// @dev Returns the address of the incentive tokens collector contract. + function getIncentiveTokensCollector() internal view returns (address) { FactoryOwnableStorage storage $ = _getFactoryOwnableStorage(); - return IRewardVaultFactory($._factory).bgtIncentiveDistributor(); + return IRewardVaultFactory($._factory).incentiveTokensCollector(); } /// @dev Sets the address of the factory contract. diff --git a/src/base/IStakingRewards.sol b/src/base/IStakingRewards.sol index addcbb4..8068ea0 100644 --- a/src/base/IStakingRewards.sol +++ b/src/base/IStakingRewards.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.26; import { IStakingRewardsErrors } from "./IStakingRewardsErrors.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @notice Interface of staking rewards interface IStakingRewards is IStakingRewardsErrors { @@ -37,6 +38,9 @@ interface IStakingRewards is IStakingRewardsErrors { /* GETTERS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + /// @notice The ERC20 token which users stake to earn rewards. + function stakeToken() external view returns (IERC20); + /// @notice Get the balance of the staked tokens for an account. /// @param account The account to get the balance for. /// @return The balance of the staked tokens. diff --git a/src/base/StakingRewards.sol b/src/base/StakingRewards.sol index 782f59d..bb80b4d 100644 --- a/src/base/StakingRewards.sol +++ b/src/base/StakingRewards.sol @@ -38,7 +38,7 @@ abstract contract StakingRewards is Initializable, IStakingRewards { IERC20 public stakeToken; /// @notice ERC20 token in which rewards are denominated and distributed. - IERC20 public rewardToken; + IERC20 internal _rewardToken; /// @notice The reward rate for the current reward period scaled by PRECISION. uint256 public rewardRate; @@ -73,18 +73,18 @@ abstract contract StakingRewards is Initializable, IStakingRewards { /// @dev Must be called by the initializer of the inheriting contract. /// @param _stakingToken The address of the token that users will stake. - /// @param _rewardToken The address of the token that will be distributed as rewards. + /// @param rewardToken_ The address of the token that will be distributed as rewards. /// @param _rewardsDuration The duration of the rewards cycle. function __StakingRewards_init( address _stakingToken, - address _rewardToken, + address rewardToken_, uint256 _rewardsDuration ) internal onlyInitializing { stakeToken = IERC20(_stakingToken); - rewardToken = IERC20(_rewardToken); + _rewardToken = IERC20(rewardToken_); rewardsDuration = _rewardsDuration; } @@ -125,7 +125,7 @@ abstract contract StakingRewards is Initializable, IStakingRewards { /// @notice Check if the rewards are solvent. /// @dev Inherited contracts may override this function to implement custom solvency checks. function _checkRewardSolvency() internal view virtual { - if (undistributedRewards / PRECISION > rewardToken.balanceOf(address(this))) { + if (undistributedRewards / PRECISION > _rewardToken.balanceOf(address(this))) { InsolventReward.selector.revertWith(); } } @@ -150,7 +150,7 @@ abstract contract StakingRewards is Initializable, IStakingRewards { /// @param to The recipient address. /// @param amount The amount of reward tokens to transfer. function _safeTransferRewardToken(address to, uint256 amount) internal virtual { - rewardToken.safeTransfer(to, amount); + _rewardToken.safeTransfer(to, amount); } /// @notice Stakes tokens in the vault for a specified account. @@ -259,6 +259,11 @@ abstract contract StakingRewards is Initializable, IStakingRewards { /* GETTERS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + /// @notice Returns the reward token. + function rewardToken() public view virtual returns (IERC20) { + return _rewardToken; + } + function balanceOf(address account) public view virtual returns (uint256) { return _accountInfo[account].balance; } diff --git a/src/old_versions/V0_Contracts/BlockRewardController_V0.sol b/src/old_versions/V0_Contracts/BlockRewardController_V0.sol new file mode 100644 index 0000000..0eeb7ae --- /dev/null +++ b/src/old_versions/V0_Contracts/BlockRewardController_V0.sol @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; + +import { Utils } from "src/libraries/Utils.sol"; +import { IBlockRewardController_V0 as IBlockRewardController } from "./interfaces/IBlockRewardController_V0.sol"; +import { IBeaconDeposit } from "src/pol/interfaces/IBeaconDeposit.sol"; +import { BGT } from "src/pol/BGT.sol"; + +/// @title BlockRewardController +/// @author Berachain Team +/// @notice The BlockRewardController contract is responsible for managing the reward rate of BGT. +/// @dev It should be owned by the governance module. +/// @dev It should also be the only contract that can mint the BGT token. +/// @dev The invariant(s) that should hold true are: +/// - processRewards() is only called at most once per block timestamp. +contract BlockRewardController_V0 is IBlockRewardController, OwnableUpgradeable, UUPSUpgradeable { + using Utils for bytes4; + + /// @notice The maximum value for base rate. + uint256 public constant MAX_BASE_RATE = 5 * FixedPointMathLib.WAD; + + /// @notice The maximum value for reward rate. + uint256 public constant MAX_REWARD_RATE = 5 * FixedPointMathLib.WAD; + + /// @notice The maximum value for the minimum reward rate after boosts accounting. + uint256 public constant MAX_MIN_BOOSTED_REWARD_RATE = 10 * FixedPointMathLib.WAD; + + /// @notice The maximum value for boost multiplier. + uint256 public constant MAX_BOOST_MULTIPLIER = 5 * FixedPointMathLib.WAD; + + /// @notice The maximum value for reward convexity parameter. + uint256 public constant MAX_REWARD_CONVEXITY = FixedPointMathLib.WAD; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STORAGE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice The BGT token contract that we are minting to the distributor. + BGT public bgt; + + /// @notice The Beacon deposit contract to check the pubkey -> operator relationship. + IBeaconDeposit public beaconDepositContract; + + /// @notice The distributor contract that receives the minted BGT. + address public distributor; + + /// @notice The constant base rate for BGT. + uint256 public baseRate; + + /// @notice The reward rate for BGT. + uint256 public rewardRate; + + /// @notice The minimum reward rate for BGT after accounting for validator boosts. + uint256 public minBoostedRewardRate; + + /// @notice The boost multiplier param in the function, determines the inflation cap, 18 dec. + uint256 public boostMultiplier; + + /// @notice The reward convexity param in the function, determines how fast it converges to its max, 18 dec. + int256 public rewardConvexity; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + address _bgt, + address _distributor, + address _beaconDepositContract, + address _governance + ) + external + initializer + { + __Ownable_init(_governance); + __UUPSUpgradeable_init(); + bgt = BGT(_bgt); + emit SetDistributor(_distributor); + // slither-disable-next-line missing-zero-check + distributor = _distributor; + // slither-disable-next-line missing-zero-check + beaconDepositContract = IBeaconDeposit(_beaconDepositContract); + } + + function _authorizeUpgrade(address newImplementation) internal override onlyOwner { } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* MODIFIER */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + modifier onlyDistributor() { + if (msg.sender != distributor) { + NotDistributor.selector.revertWith(); + } + _; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ADMIN FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IBlockRewardController + function setBaseRate(uint256 _baseRate) external onlyOwner { + if (_baseRate > MAX_BASE_RATE) { + InvalidBaseRate.selector.revertWith(); + } + emit BaseRateChanged(baseRate, _baseRate); + baseRate = _baseRate; + } + + /// @inheritdoc IBlockRewardController + function setRewardRate(uint256 _rewardRate) external onlyOwner { + if (_rewardRate > MAX_REWARD_RATE) { + InvalidRewardRate.selector.revertWith(); + } + emit RewardRateChanged(rewardRate, _rewardRate); + rewardRate = _rewardRate; + } + + /// @inheritdoc IBlockRewardController + function setMinBoostedRewardRate(uint256 _minBoostedRewardRate) external onlyOwner { + if (_minBoostedRewardRate > MAX_MIN_BOOSTED_REWARD_RATE) { + InvalidMinBoostedRewardRate.selector.revertWith(); + } + emit MinBoostedRewardRateChanged(minBoostedRewardRate, _minBoostedRewardRate); + minBoostedRewardRate = _minBoostedRewardRate; + } + + /// @inheritdoc IBlockRewardController + function setBoostMultiplier(uint256 _boostMultiplier) external onlyOwner { + if (_boostMultiplier > MAX_BOOST_MULTIPLIER) { + InvalidBoostMultiplier.selector.revertWith(); + } + emit BoostMultiplierChanged(boostMultiplier, _boostMultiplier); + boostMultiplier = _boostMultiplier; + } + + /// @inheritdoc IBlockRewardController + function setRewardConvexity(uint256 _rewardConvexity) external onlyOwner { + if (_rewardConvexity == 0 || _rewardConvexity > MAX_REWARD_CONVEXITY) { + InvalidRewardConvexity.selector.revertWith(); + } + emit RewardConvexityChanged(uint256(rewardConvexity), _rewardConvexity); + // store as int256 to avoid casting during computation + rewardConvexity = int256(_rewardConvexity); + } + + /// @inheritdoc IBlockRewardController + function setDistributor(address _distributor) external onlyOwner { + if (_distributor == address(0)) { + ZeroAddress.selector.revertWith(); + } + emit SetDistributor(_distributor); + distributor = _distributor; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* DISTRIBUTOR FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IBlockRewardController + function computeReward( + uint256 boostPower, + uint256 _rewardRate, + uint256 _boostMultiplier, + int256 _rewardConvexity + ) + public + pure + returns (uint256 reward) + { + // On conv == 0, mathematical result should be max reward even for boost == 0 (0^0 = 1) + // but since BlockRewardController enforces conv > 0, we're not adding code for conv == 0 case + if (boostPower > 0) { + // Compute intermediate parameters for the reward formula + uint256 one = FixedPointMathLib.WAD; + + if (boostPower == one) { + // avoid approx errors in the following code + reward = FixedPointMathLib.mulWad(_rewardRate, _boostMultiplier); + } else { + // boost^conv ∈ (0, 1] + uint256 tmp_0 = uint256(FixedPointMathLib.powWad(int256(boostPower), _rewardConvexity)); + // 1 + mul * boost^conv ∈ [1, 1 + mul] + uint256 tmp_1 = one + FixedPointMathLib.mulWad(_boostMultiplier, tmp_0); + // 1 - 1 / (1 + mul * boost^conv) ∈ [0, mul / (1 + mul)] + uint256 tmp_2 = one - FixedPointMathLib.divWad(one, tmp_1); + + // @dev Due to splitting fixed point ops, [mul / (1 + mul)] * (1 + mul) may be slightly > mul + uint256 coeff = FixedPointMathLib.mulWad(tmp_2, one + _boostMultiplier); + if (coeff > _boostMultiplier) coeff = _boostMultiplier; + + reward = FixedPointMathLib.mulWad(_rewardRate, coeff); + } + } + } + + /// @inheritdoc IBlockRewardController + function getMaxBGTPerBlock() public view returns (uint256 amount) { + amount = computeReward(FixedPointMathLib.WAD, rewardRate, boostMultiplier, rewardConvexity); + if (amount < minBoostedRewardRate) { + amount = minBoostedRewardRate; + } + amount += baseRate; + } + + /// @inheritdoc IBlockRewardController + function processRewards( + bytes calldata pubkey, + uint64 nextTimestamp, + bool isReady + ) + external + onlyDistributor + returns (uint256) + { + uint256 base = baseRate; + uint256 reward = 0; + + // Only compute vaults reward if berachef is ready + if (isReady) { + // Calculate the boost power for the validator + uint256 boostPower = bgt.normalizedBoost(pubkey); + reward = computeReward(boostPower, rewardRate, boostMultiplier, rewardConvexity); + if (reward < minBoostedRewardRate) reward = minBoostedRewardRate; + } + + emit BlockRewardProcessed(pubkey, nextTimestamp, base, reward); + + // Use the beaconDepositContract to fetch the operator, Its gauranteed to return a valid address. + // Beacon Deposit contract will enforce validators to set an operator. + address operator = beaconDepositContract.getOperator(pubkey); + if (base > 0) bgt.mint(operator, base); + + // Mint the scaled rewards BGT for validator reward allocation to the distributor. + if (reward > 0) bgt.mint(distributor, reward); + + return reward; + } +} diff --git a/src/old_versions/V0_Contracts/RewardVault_V0.sol b/src/old_versions/V0_Contracts/RewardVault_V0.sol index ed76af2..2b304e9 100644 --- a/src/old_versions/V0_Contracts/RewardVault_V0.sol +++ b/src/old_versions/V0_Contracts/RewardVault_V0.sol @@ -11,7 +11,7 @@ import { Utils } from "src/libraries/Utils.sol"; import { IBeaconDeposit } from "src/pol/interfaces/IBeaconDeposit.sol"; import { IRewardVault_V0 } from "./interfaces/IRewardVault_V0.sol"; import { FactoryOwnable } from "src/base/FactoryOwnable.sol"; -import { StakingRewards } from "src/base/StakingRewards.sol"; +import { StakingRewards_V0 } from "src/old_versions/V0_Contracts/base/StakingRewards_V0.sol"; /// @title Rewards Vault_V0 /// @author Berachain Team @@ -24,7 +24,7 @@ contract RewardVault_V0 is PausableUpgradeable, ReentrancyGuardUpgradeable, FactoryOwnable, - StakingRewards, + StakingRewards_V0, IRewardVault_V0 { using Utils for bytes4; diff --git a/src/old_versions/V0_Contracts/base/FactoryOwnable_V0.sol b/src/old_versions/V0_Contracts/base/FactoryOwnable_V0.sol new file mode 100644 index 0000000..b2071c2 --- /dev/null +++ b/src/old_versions/V0_Contracts/base/FactoryOwnable_V0.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { + IRewardVaultFactory_V3 as IRewardVaultFactory +} from "../../V3_Contracts/interfaces/IRewardVaultFactory_V3.sol"; + +import { Utils } from "src/libraries/Utils.sol"; + +/// @title FactoryOwnable +/// @author Berachain Team +/// @notice Contract module which provides a modifier for restricting access to the factory owner. +abstract contract FactoryOwnable_V0 is Initializable { + using Utils for bytes4; + + /// @notice Throws if sender is not the owner of factory contract. + /// @param account The address of the sender. + error OwnableUnauthorizedAccount(address account); + + /// @custom:storage-location erc7201:berachain.storage.factoryOwnable + struct FactoryOwnableStorage { + address _factory; + } + + // keccak256(abi.encode(uint256(keccak256("berachain.storage.factoryOwnable")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant FactoryOwnableStorageLocation = + 0x4e32a932fdd4658a66f9586d8955a0d0a795a01bd8251335b4fae29d972acc00; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INITIALIZER */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Must be called by the initializer of the inheriting contract. + /// @param factoryAddr The address of the ownable factory contract. + function __FactoryOwnable_init(address factoryAddr) internal onlyInitializing { + _setFactory(factoryAddr); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* MODIFIERS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Throws if called by any account other than the factory owner. + modifier onlyFactoryOwner() { + _checkFactoryOwner(); + _; + } + + /// @dev Throws if called by any account other than the factory vault manager. + modifier onlyFactoryVaultManager() { + _checkFactoryVaultManager(); + _; + } + + /// @dev Throws if called by any account other than the factory vault pauser. + modifier onlyFactoryVaultPauser() { + _checkFactoryVaultPauser(); + _; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* GETTERS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Returns the address of the factory contract. + function factory() public view virtual returns (address) { + FactoryOwnableStorage storage $ = _getFactoryOwnableStorage(); + return $._factory; + } + + /// @notice Returns if the user is a owner of the factory contract. + function isFactoryOwner(address user) public view virtual returns (bool) { + FactoryOwnableStorage storage $ = _getFactoryOwnableStorage(); + return AccessControlUpgradeable($._factory).hasRole(_getAdminRole(), user); + } + + /// @notice Returns if the account is a vault manager of the factory contract. + function isFactoryVaultManager(address user) public view virtual returns (bool) { + FactoryOwnableStorage storage $ = _getFactoryOwnableStorage(); + return AccessControlUpgradeable($._factory).hasRole(_getVaultManagerRole(), user); + } + + /// @notice Returns if the account is a vault pauser of the factory contract. + function isFactoryVaultPauser(address user) public view virtual returns (bool) { + FactoryOwnableStorage storage $ = _getFactoryOwnableStorage(); + return AccessControlUpgradeable($._factory).hasRole(_getVaultPauserRole(), user); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNALS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns the DEFAULT_ADMIN_ROLE of the factory contract. + function _getAdminRole() internal view returns (bytes32) { + FactoryOwnableStorage storage $ = _getFactoryOwnableStorage(); + return AccessControlUpgradeable($._factory).DEFAULT_ADMIN_ROLE(); + } + + /// @dev Returns the VAULT_MANAGER_ROLE of the factory contract. + function _getVaultManagerRole() internal view returns (bytes32) { + FactoryOwnableStorage storage $ = _getFactoryOwnableStorage(); + return IRewardVaultFactory($._factory).VAULT_MANAGER_ROLE(); + } + + /// @dev Returns the VAULT_PAUSER_ROLE of the factory contract. + function _getVaultPauserRole() internal view returns (bytes32) { + FactoryOwnableStorage storage $ = _getFactoryOwnableStorage(); + return IRewardVaultFactory($._factory).VAULT_PAUSER_ROLE(); + } + + /// @dev Returns the storage struct of the factory ownable contract. + function _getFactoryOwnableStorage() internal pure returns (FactoryOwnableStorage storage $) { + assembly { + $.slot := FactoryOwnableStorageLocation + } + } + + /// @dev Returns the address of the BGTIncentiveDistributor contract. + function getBGTIncentiveDistributor() internal view returns (address) { + FactoryOwnableStorage storage $ = _getFactoryOwnableStorage(); + return IRewardVaultFactory($._factory).bgtIncentiveDistributor(); + } + + /// @dev Sets the address of the factory contract. + function _setFactory(address factoryAddr) internal { + FactoryOwnableStorage storage $ = _getFactoryOwnableStorage(); + $._factory = factoryAddr; + } + + /// @dev Checks if the sender is a admin of the factory contract. + function _checkFactoryOwner() internal view { + if (!isFactoryOwner(msg.sender)) OwnableUnauthorizedAccount.selector.revertWith(msg.sender); + } + + /// @dev Check if the sender is a vault manager of the factory contract. + function _checkFactoryVaultManager() internal view { + if (!isFactoryVaultManager(msg.sender)) OwnableUnauthorizedAccount.selector.revertWith(msg.sender); + } + + /// @dev Check if the sender is a vault pauser of the factory contract. + function _checkFactoryVaultPauser() internal view { + if (!isFactoryVaultPauser(msg.sender)) OwnableUnauthorizedAccount.selector.revertWith(msg.sender); + } +} diff --git a/src/old_versions/V0_Contracts/base/StakingRewards_V0.sol b/src/old_versions/V0_Contracts/base/StakingRewards_V0.sol new file mode 100644 index 0000000..e7a33c9 --- /dev/null +++ b/src/old_versions/V0_Contracts/base/StakingRewards_V0.sol @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; + +import { Utils } from "src/libraries/Utils.sol"; +import { IStakingRewards } from "src/base/IStakingRewards.sol"; + +/// @title StakingRewards_V0 +/// @author Berachain Team +/// @notice Frozen base class used exclusively by old_versions contracts (V0–V9). +/// @dev Preserves the original `public rewardToken` state variable layout so that +/// old implementation snapshots compile unchanged. The current StakingRewards base +/// promotes `rewardToken` to an internal variable with a virtual getter to enable +/// per-vault overrides; old_versions do not need that capability. +abstract contract StakingRewards_V0 is Initializable, IStakingRewards { + using Utils for bytes4; + using SafeERC20 for IERC20; + + /// @notice Struct to hold account data. + /// @param balance The balance of the staked tokens. + /// @param unclaimedReward The amount of unclaimed rewards. + /// @param rewardsPerTokenPaid The amount of rewards per token paid, scaled by PRECISION. + struct Info { + uint256 balance; + uint256 unclaimedReward; + uint256 rewardsPerTokenPaid; + } + + uint256 internal constant PRECISION = 1e18; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STORAGE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice ERC20 token which users stake to earn rewards. + IERC20 public stakeToken; + + /// @notice ERC20 token in which rewards are denominated and distributed. + IERC20 public rewardToken; + + /// @notice The reward rate for the current reward period scaled by PRECISION. + uint256 public rewardRate; + + /// @notice The amount of undistributed rewards scaled by PRECISION. + uint256 public undistributedRewards; + + /// @notice The last updated reward per token scaled by PRECISION. + uint256 public rewardPerTokenStored; + + /// @notice The total supply of the staked tokens. + uint256 public totalSupply; + + /// @notice The end of the current reward period, where we need to start a new one. + uint256 public periodFinish; + + /// @notice The time over which the rewards will be distributed. Current default is 7 days. + uint256 public rewardsDuration; + + /// @notice The last time the rewards were updated. + uint256 public lastUpdateTime; + + /// @notice The mapping of accounts to their data. + mapping(address account => Info) internal _accountInfo; + + /// @dev This gap is used to prevent storage collisions. + uint256[50] private __gap; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INITIALIZER */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Must be called by the initializer of the inheriting contract. + /// @param _stakingToken The address of the token that users will stake. + /// @param _rewardToken The address of the token that will be distributed as rewards. + /// @param _rewardsDuration The duration of the rewards cycle. + function __StakingRewards_init( + address _stakingToken, + address _rewardToken, + uint256 _rewardsDuration + ) + internal + onlyInitializing + { + stakeToken = IERC20(_stakingToken); + rewardToken = IERC20(_rewardToken); + rewardsDuration = _rewardsDuration; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* MODIFIERS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + modifier updateReward(address account) { + _updateReward(account); + _; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STATE MUTATING FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Notifies the staking contract of a new reward transfer. + /// @param reward The quantity of reward tokens being notified. + /// @dev Only authorized notifiers should call this method to avoid griefing or false notifications. + function _notifyRewardAmount(uint256 reward) internal virtual updateReward(address(0)) { + reward = reward * PRECISION; + + if (totalSupply != 0 && block.timestamp < periodFinish) { + reward += _computeLeftOverReward(); + } + + undistributedRewards += reward; + _checkRewardSolvency(); + + if (totalSupply != 0) { + _setRewardRate(); + lastUpdateTime = block.timestamp; + } + + emit RewardAdded(reward); + } + + /// @notice Check if the rewards are solvent. + /// @dev Inherited contracts may override this function to implement custom solvency checks. + function _checkRewardSolvency() internal view virtual { + if (undistributedRewards / PRECISION > rewardToken.balanceOf(address(this))) { + InsolventReward.selector.revertWith(); + } + } + + /// @notice Claims the reward for a specified account and transfers it to the specified recipient. + /// @param account The account to claim the reward for. + /// @param recipient The account to receive the reward. + /// @return The amount of the reward claimed. + function _getReward(address account, address recipient) internal virtual updateReward(account) returns (uint256) { + Info storage info = _accountInfo[account]; + uint256 reward = info.unclaimedReward; // get the rewards owed to the account + if (reward != 0) { + info.unclaimedReward = 0; + _safeTransferRewardToken(recipient, reward); + emit RewardPaid(account, recipient, reward); + } + return reward; + } + + /// @notice Safely transfers the reward tokens to the specified recipient. + /// @dev Inherited contracts may override this function to implement custom transfer logic. + /// @param to The recipient address. + /// @param amount The amount of reward tokens to transfer. + function _safeTransferRewardToken(address to, uint256 amount) internal virtual { + rewardToken.safeTransfer(to, amount); + } + + /// @notice Stakes tokens in the vault for a specified account. + /// @param account The account to stake the tokens for. + /// @param amount The amount of tokens to stake. + function _stake(address account, uint256 amount) internal virtual { + if (amount == 0) StakeAmountIsZero.selector.revertWith(); + + // set the reward rate after the first stake if there are undistributed rewards + if (totalSupply == 0 && undistributedRewards > 0) { + _setRewardRate(); + } + + // update the rewards for the account after `rewardRate` is updated + _updateReward(account); + + unchecked { + uint256 totalSupplyBefore = totalSupply; // cache storage read + uint256 totalSupplyAfter = totalSupplyBefore + amount; + // `<=` and `<` are equivalent here but the former is cheaper + if (totalSupplyAfter <= totalSupplyBefore) TotalSupplyOverflow.selector.revertWith(); + totalSupply = totalSupplyAfter; + // `totalSupply` would have overflowed first because `totalSupplyBefore` >= `_accountInfo[account].balance` + _accountInfo[account].balance += amount; + } + _safeTransferFromStakeToken(msg.sender, amount); + emit Staked(account, amount); + } + + /// @notice Safely transfers staking tokens from the sender to the contract. + /// @dev Inherited contracts may override this function to implement custom transfer logic. + /// @param from The address to transfer the tokens from. + /// @param amount The amount of tokens to transfer. + function _safeTransferFromStakeToken(address from, uint256 amount) internal virtual { + stakeToken.safeTransferFrom(from, address(this), amount); + } + + /// @notice Withdraws staked tokens from the vault for a specified account. + /// @param account The account to withdraw the tokens for. + /// @param amount The amount of tokens to withdraw. + function _withdraw(address account, uint256 amount) internal virtual { + if (amount == 0) WithdrawAmountIsZero.selector.revertWith(); + + // update the rewards for the account before the balance is updated + _updateReward(account); + + unchecked { + Info storage info = _accountInfo[account]; + uint256 balanceBefore = info.balance; // cache storage read + if (balanceBefore < amount) InsufficientStake.selector.revertWith(); + info.balance = balanceBefore - amount; + // underflow not possible because `totalSupply` >= `balanceBefore` >= `amount` + totalSupply -= amount; + } + if (totalSupply == 0 && block.timestamp < periodFinish) { + undistributedRewards += _computeLeftOverReward(); + } + _safeTransferStakeToken(msg.sender, amount); + emit Withdrawn(account, amount); + } + + /// @notice Safely transfers staking tokens to the specified recipient. + /// @param to The recipient address. + /// @param amount The amount of tokens to transfer. + function _safeTransferStakeToken(address to, uint256 amount) internal virtual { + stakeToken.safeTransfer(to, amount); + } + + function _setRewardRate() internal virtual { + uint256 _rewardsDuration = rewardsDuration; // cache storage read + uint256 _rewardRate = undistributedRewards / _rewardsDuration; + rewardRate = _rewardRate; + periodFinish = block.timestamp + _rewardsDuration; + undistributedRewards -= _rewardRate * _rewardsDuration; + } + + function _updateReward(address account) internal virtual { + uint256 _rewardPerToken = rewardPerToken(); // cache result + rewardPerTokenStored = _rewardPerToken; + // record the last time the rewards were updated + lastUpdateTime = lastTimeRewardApplicable(); + if (account != address(0)) { + Info storage info = _accountInfo[account]; + (info.unclaimedReward, info.rewardsPerTokenPaid) = (earned(account), _rewardPerToken); + } + } + + /// @dev Allows to change `rewardsDuration` during the reward cycle, not affecting `rewardRate` which + /// will be updated upon next `_notifyRewardAmount()` call. + function _setRewardsDuration(uint256 _rewardsDuration) internal virtual { + if (_rewardsDuration == 0) RewardsDurationIsZero.selector.revertWith(); + rewardsDuration = _rewardsDuration; + emit RewardsDurationUpdated(_rewardsDuration); + } + + /// @notice Returns the left over amount scaled by PRECISION. + function _computeLeftOverReward() internal view returns (uint256 leftOver) { + uint256 remainingTime; + unchecked { + remainingTime = periodFinish - block.timestamp; + } + leftOver = rewardRate * remainingTime; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* GETTERS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function balanceOf(address account) public view virtual returns (uint256) { + return _accountInfo[account].balance; + } + + function rewards(address account) public view virtual returns (uint256) { + return _accountInfo[account].unclaimedReward; + } + + function userRewardPerTokenPaid(address account) public view virtual returns (uint256) { + return _accountInfo[account].rewardsPerTokenPaid; + } + + function lastTimeRewardApplicable() public view virtual returns (uint256) { + return FixedPointMathLib.min(block.timestamp, periodFinish); + } + + /// @dev Gives current reward per token, result is scaled by PRECISION. + function rewardPerToken() public view virtual returns (uint256) { + uint256 _totalSupply = totalSupply; // cache storage read + if (_totalSupply == 0) return rewardPerTokenStored; + uint256 timeDelta; + unchecked { + timeDelta = lastTimeRewardApplicable() - lastUpdateTime; + } + // computes reward per token by rounding it down to avoid reverting '_getReward' with insufficient rewards + uint256 _newRewardPerToken = FixedPointMathLib.fullMulDiv(rewardRate, timeDelta, _totalSupply); + return rewardPerTokenStored + _newRewardPerToken; + } + + function earned(address account) public view virtual returns (uint256) { + Info storage info = _accountInfo[account]; + (uint256 balance, uint256 unclaimedReward, uint256 rewardsPerTokenPaid) = + (info.balance, info.unclaimedReward, info.rewardsPerTokenPaid); + uint256 rewardPerTokenDelta; + unchecked { + rewardPerTokenDelta = rewardPerToken() - rewardsPerTokenPaid; + } + return unclaimedReward + FixedPointMathLib.fullMulDiv(balance, rewardPerTokenDelta, PRECISION); + } + + function getRewardForDuration() public view virtual returns (uint256) { + return FixedPointMathLib.fullMulDiv(rewardRate, rewardsDuration, PRECISION); + } +} diff --git a/src/old_versions/V0_Contracts/interfaces/IBeraChef_V0.sol b/src/old_versions/V0_Contracts/interfaces/IBeraChef_V0.sol index 8e9f936..2fc3531 100644 --- a/src/old_versions/V0_Contracts/interfaces/IBeraChef_V0.sol +++ b/src/old_versions/V0_Contracts/interfaces/IBeraChef_V0.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.26; -import { IPOLErrors } from "src/pol/interfaces/IPOLErrors.sol"; +import { IPOLErrors_V0 as IPOLErrors } from "./IPOLErrors_V0.sol"; /// @notice Interface of the BeraChef module interface IBeraChef_V0 is IPOLErrors { diff --git a/src/old_versions/V0_Contracts/interfaces/IBlockRewardController_V0.sol b/src/old_versions/V0_Contracts/interfaces/IBlockRewardController_V0.sol new file mode 100644 index 0000000..074aa09 --- /dev/null +++ b/src/old_versions/V0_Contracts/interfaces/IBlockRewardController_V0.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import { IPOLErrors_V0 as IPOLErrors } from "./IPOLErrors_V0.sol"; + +interface IBlockRewardController_V0 is IPOLErrors { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* EVENTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /** + * @notice Emitted when the constant base rate has changed. + * @param oldBaseRate The old base rate. + * @param newBaseRate The new base rate. + */ + event BaseRateChanged(uint256 oldBaseRate, uint256 newBaseRate); + + /** + * @notice Emitted when the reward rate has changed. + * @param oldRewardRate The old reward rate. + * @param newRewardRate The new reward rate. + */ + event RewardRateChanged(uint256 oldRewardRate, uint256 newRewardRate); + + /** + * @notice Emitted when the min boosted reward rate has changed. + * @param oldMinBoostedRewardRate The old min boosted reward rate. + * @param newMinBoostedRewardRate The new min boosted reward rate. + */ + event MinBoostedRewardRateChanged(uint256 oldMinBoostedRewardRate, uint256 newMinBoostedRewardRate); + + /** + * @notice Emitted when the boostMultiplier parameter has changed. + * @param oldBoostMultiplier The old boost multiplier parameter. + * @param newBoostMultiplier The new boost multiplier parameter. + */ + event BoostMultiplierChanged(uint256 oldBoostMultiplier, uint256 newBoostMultiplier); + + /** + * @notice Emitted when the reward formula convexity parameter has changed. + * @param oldRewardConvexity The old reward formula convexity parameter. + * @param newRewardConvexity The new reward formula convexity parameter. + */ + event RewardConvexityChanged(uint256 oldRewardConvexity, uint256 newRewardConvexity); + + /// @notice Emitted when the distributor is set. + event SetDistributor(address indexed rewardDistribution); + + /// @notice Emitted when the rewards for the specified block have been processed. + /// @param pubkey The validator's pubkey. + /// @param nextTimestamp The timestamp of the next beacon block that was processed. + /// @param baseRate The base amount of BGT minted to the validator's operator. + /// @param rewardRate The amount of BGT minted to the distributor. + event BlockRewardProcessed(bytes indexed pubkey, uint64 nextTimestamp, uint256 baseRate, uint256 rewardRate); + + /// @notice Returns the constant base rate for BGT. + /// @return The constant base amount of BGT to be minted in the current block. + function baseRate() external view returns (uint256); + + /// @notice Returns the reward rate for BGT. + /// @return The unscaled amount of BGT to be minted in the current block. + function rewardRate() external view returns (uint256); + + /// @notice Returns the minimum boosted reward rate for BGT. + /// @return The minimum amount of BGT to be minted in the current block. + function minBoostedRewardRate() external view returns (uint256); + + /// @notice Returns the boost mutliplier param in the reward function. + /// @return The parameter that determines the inflation cap. + function boostMultiplier() external view returns (uint256); + + /// @notice Returns the reward convexity param in the reward function. + /// @return The parameter that determines how fast the function converges to its max. + function rewardConvexity() external view returns (int256); + + /** + * @notice Computes the reward given specified parameters, according to the formula. + * r := (1 + mul) * (1 - 1 / (1 + mul * boost^conv)) * rewardRate ∈ [0, mul * rewardRate] + * @dev Returns 0 for boost == 0 even if conv == 0, since contract enforces conv > 0. + * @param boostPower the normalized boost. + * @param _rewardRate the reward rate parameter. + * @param _boostMultiplier the boost multiplier parameter. + * @param _rewardConvexity the reward convexity parameter. + * @return the reward amount. + */ + function computeReward( + uint256 boostPower, + uint256 _rewardRate, + uint256 _boostMultiplier, + int256 _rewardConvexity + ) + external + pure + returns (uint256); + + /** + * @notice Returns the current max BGT production per block. + * @dev Exposed for BGT contract to calculate the max burnable native token amount. + * @return amount The maximum amount of BGT that can be minted in one block. + */ + function getMaxBGTPerBlock() external view returns (uint256 amount); + + /** + * @notice Processes the rewards for the specified block and mints BGT to validator's operator and distributor. + * @dev This function can only be called by the distributor. + * @dev If in genesis only base rate for validators is minted. + * @param pubkey The validator's pubkey. + * @param nextTimestamp The timestamp of the next beacon block that was processed. + * @param isReady The flag to enable reward minting to distributor (true when BeraChef is ready). + * @return the amount of BGT minted to distributor. + */ + function processRewards(bytes calldata pubkey, uint64 nextTimestamp, bool isReady) external returns (uint256); + + /** + * @notice Sets the constant base reward rate for BGT. + * @dev This function can only be called by the owner, which is the governance address. + * @param _baseRate The new base rate. + */ + function setBaseRate(uint256 _baseRate) external; + + /** + * @notice Sets the reward rate for BGT. + * @dev This function can only be called by the owner, which is the governance address. + * @param _rewardRate The new reward rate. + */ + function setRewardRate(uint256 _rewardRate) external; + + /** + * @notice Sets the min boosted reward rate for BGT. + * @dev This function can only be called by the owner, which is the governance address. + * @param _minBoostedRewardRate The new min boosted reward rate. + */ + function setMinBoostedRewardRate(uint256 _minBoostedRewardRate) external; + + /** + * @notice Sets the boost multiplier parameter for the reward formula. + * @dev This function can only be called by the owner, which is the governance address. + * @param _boostMultiplier The new boost multiplier. + */ + function setBoostMultiplier(uint256 _boostMultiplier) external; + + /** + * @notice Sets the reward convexity parameter for the reward formula. + * @dev This function can only be called by the owner, which is the governance address. + * @param _rewardConvexity The new reward convexity. + */ + function setRewardConvexity(uint256 _rewardConvexity) external; + + /** + * @notice Sets the distributor contract that receives the minted BGT. + * @dev This function can only be called by the owner, which is the governance address. + * @param _distributor The new distributor contract. + */ + function setDistributor(address _distributor) external; +} diff --git a/src/old_versions/V0_Contracts/interfaces/IPOLErrors_V0.sol b/src/old_versions/V0_Contracts/interfaces/IPOLErrors_V0.sol new file mode 100644 index 0000000..d95cc1e --- /dev/null +++ b/src/old_versions/V0_Contracts/interfaces/IPOLErrors_V0.sol @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import { IStakingRewardsErrors } from "src/base/IStakingRewardsErrors.sol"; + +/// @notice Interface of POL errors +interface IPOLErrors_V0 is IStakingRewardsErrors { + // Signature: 0xf2d81d95 + error NotApprovedSender(); + // Signature: 0x1db3b859 + error NotDelegate(); + // Signature: 0x53f0a596 + error NotBGT(); + // Signature: 0x1b0eb4ec + error NotBlockRewardController(); + // Signature: 0x385296d5 + error NotDistributor(); + // Signature: 0x73fcd3fe + error NotFeeCollector(); + // Signature: 0x36407850 + error NotWhitelistedVault(); + // Signature:0x7c214f04 + error NotOperator(); + // Signature: 0xad3a8b9e + error NotEnoughBalance(); + // Signature: 0xadd377f6 + error InvalidActivateBoostDelay(); + // Signature: 0x2f14f4f9 + error InvalidDropBoostDelay(); + // Signature: 0x14969061 + error NotEnoughBoostedBalance(); + // Signature: 0xe8966d7a + error NotEnoughTime(); + // Signature: 0xec2caa0d + error InvalidStartBlock(); + // Signature: 0x3be31f8c + error RewardAllocationAlreadyQueued(); + // Signature: 0x13134d24 + error InvalidRewardAllocationWeights(); + // Signature: 0xf6fae721 + error TooManyWeights(); + // Signature: 0x3e38573f + error InvalidateDefaultRewardAllocation(); + // Signature: 0xd92e233d + error ZeroAddress(); + // Signature: 0x462a2bb2 + error RewardAllocationBlockDelayTooLarge(); + // Signature: 0x08519afa + error NotFactoryVault(); + // Signature: 0x978dc040 + error ZeroPercentageWeight(); + // Signature: 0x585b9263 + error InvalidWeight(); + // Signature: 0xcb1ee123 + error InvalidMaxWeightPerVault(); + // Signature: 0xab396d11 + error InvalidCommissionValue(); + // Signature: 0x0c32c4fa + error CommissionChangeAlreadyQueued(); + // Signature: 0xe9269446 + error CommissionNotQueuedOrDelayNotPassed(); + // Signature: 0xc1abde53 + error InvalidCommissionChangeDelay(); + // Signature: 0x716e870e + error InvalidIncentiveFeeRate(); + // Signature: 0x6a1aee5f + error NotRewardAllocator(); + // Signature: 0xeb732c63 + error InvalidRewardAllocationInactivityBlockSpan(); + + /* BLOCK REWARD CONTROLLLER */ + + // Signature: 0x2e2dab43 + error InvalidBaseRate(); + // Signature: 0x22be2284 + error InvalidRewardRate(); + // Signature: 0x15482337 + error InvalidMinBoostedRewardRate(); + // Signature: 0xb7b2319a + error InvalidBoostMultiplier(); + // Signature: 0x347f95b2 + error InvalidRewardConvexity(); + + /* STAKING */ + + // Signature: 0x09ee12d5 + error NotAContract(); + // Signature: 0xe4ea100b + error CannotRecoverRewardToken(); + // Signature: 0x1b813803 + error CannotRecoverStakingToken(); + // Signature: 0x2899103f + error CannotRecoverIncentiveToken(); + // Signature: 0x38432c89 + error IncentiveRateTooHigh(); + // Signature: 0x5ee4de0e + error NotIncentiveManager(); + // Signature: 0x92949442 + error AmountGreaterThanReward(); + + // Signature: 0xf84835a0 + error TokenNotWhitelisted(); + // Signature: 0x8d1473a6 + error InsufficientDelegateStake(); + // Signature: 0x08e88f46 + error InsufficientSelfStake(); + // Signature: 0xfbf97e07 + error TokenAlreadyWhitelistedOrLimitReached(); + // Signature: 0xad57d95d + error AmountLessThanMinIncentiveRate(); + // Signature: 0xfbf1123c + error InvalidMaxIncentiveTokensCount(); + + // Signature: 0x546c7600 + error PayoutAmountIsZero(); + // Signature: 0x89c622a2 + error DonateAmountLessThanPayoutAmount(); + // Signature: 0xa4cc22ed + error MaxNumWeightsPerRewardAllocationIsZero(); + // Signature: 0x0b5c3aff + error MinIncentiveRateIsZero(); + // Signature: 0x8e7572da + error InvariantCheckFailed(); + // Signature: 0x451fa036 + error InvalidIncentiveRate(); + // Signature: 0xdd9df759 + error DuplicateReceiver(address receiver); + // Signature: 0xe7726b79 + error InvalidRewardDuration(); + // Signature: 0x91373fcd + error NotRewardDurationManager(); + // Signature: 0xfb89696d + error NotRewardVaultManager(); + // Signature: 0x4c168419 + error DurationChangeNotAllowed(); + // Signature: 0x3a70eb50 + error RewardDurationCoolDownPeriodNotPassed(); + + /* BEACON ROOTS */ + + // Signature: 0x1390f2a1 + error IndexOutOfRange(); + // Signature: 0x09bde339 + error InvalidProof(); + // Signature: 0x5e742c5a + error NotSystemAddress(); + // Signature: 0xa55ba280 + error OnlySystemCallAllowed(); + // Signature: 0x0a431b2a + error TimestampAlreadyProcessed(); + + /* BEACON DEPOSIT */ + + /// @dev Error thrown when the deposit amount is too small, to prevent dust deposits. + // Signature: 0x0e1eddda + error InsufficientDeposit(); + + /// @dev Error thrown when the deposit amount is not a multiple of Gwei. + // Signature: 0x40567b38 + error DepositNotMultipleOfGwei(); + + /// @dev Error thrown when the deposit amount is too high, since it is a uint64. + // Signature: 0x2aa66734 + error DepositValueTooHigh(); + + /// @dev Error thrown when the public key length is not 48 bytes. + // Signature: 0x9f106472 + error InvalidPubKeyLength(); + + /// @dev Error thrown when the withdrawal credentials length is not 32 bytes. + // Signature: 0xb39bca16 + error InvalidCredentialsLength(); + + /// @dev Error thrown when the signature length is not 96 bytes. + // Signature: 0x4be6321b + error InvalidSignatureLength(); + + /// @dev Error thrown when the input operator is zero address on the first deposit. + // Signature: 0x51969a7a + error ZeroOperatorOnFirstDeposit(); + + /// @dev Error thrown when the operator is already set and caller passed non-zero operator. + // Signature: 0xc4142b41 + error OperatorAlreadySet(); + + /// @dev Error thrown when the caller is not the current operator. + // Signature: 0x819a0d0b + error NotNewOperator(); + + /* BGT INCENTIVE DISTRIBUTOR */ + // Signature: 0x1ec5aa51 + error InvalidArray(); + // Signature: 0x68b5c198 + error InvalidDistribution(); + // Signature: 0x9dd854d3 + error InvalidMerkleRoot(); + // Signature: 0x0995309b + error RewardInactive(); + // Signature: 0x5958c647 + error InvalidRewardClaimDelay(); + // Signature: 0x75f9d00c + error InsufficientIncentiveTokens(); + // Signature: 0xc1ab6dc1 + error InvalidToken(); + + /* WBERA STAKER VAULT */ + + // Signature: 0x35898e6e + error InsufficientNativeValue(); + // Signature: 0x0f2ca6e7 + error WithdrawalNotReady(); + // Signature: 0x30466bc7 + error UnauthorizedETHTransfer(); + // Signature: 0xc1a2e9a3 + error WithdrawalNotRequested(); + // Signature: 0xfb52063b + error WithdrawalAlreadyRequested(); + // Signature: 0xeceb35ec + error NotWBERAStakerVault(); + // Signature: 0x9cbe2357 + error NonTransferable(); + // Signature: 0x83f171d6 + error MethodNotAllowed(); + // Signature: 0x891ec151 + error OnlyNFTOwnerAllowed(); + + /* LST STAKER VAULT */ + + // Signature: 0xad74abc1 + error LSTStakerVaultAlreadyAdded(); + // Signature: 0xd53e3116 + error LSTStakerVaultNotFound(); + // Signature: 0x40a3833d + error NotStakerVault(); + + /* EMISSION MANAGER */ + + // Signature: 0xbad4ab07 + error InvalidEmissionPerc(); + // Signature: 0x6635c275 + error InvalidTargetEmission(); +} diff --git a/src/pol/BGTIncentiveFeeCollector.sol b/src/old_versions/V1_Contracts/BGTIncentiveFeeCollector_V1.sol similarity index 93% rename from src/pol/BGTIncentiveFeeCollector.sol rename to src/old_versions/V1_Contracts/BGTIncentiveFeeCollector_V1.sol index 372268f..fe03a6e 100644 --- a/src/pol/BGTIncentiveFeeCollector.sol +++ b/src/old_versions/V1_Contracts/BGTIncentiveFeeCollector_V1.sol @@ -9,18 +9,20 @@ import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/P import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { Utils } from "../libraries/Utils.sol"; -import { IWBERAStakerVault } from "./interfaces/IWBERAStakerVault.sol"; -import { IBGTIncentiveFeeCollector } from "./interfaces/IBGTIncentiveFeeCollector.sol"; -import { IStakerVault } from "./interfaces/lst/IStakerVault.sol"; -import { ILSTAdapter } from "./interfaces/lst/ILSTAdapter.sol"; +import { Utils } from "src/libraries/Utils.sol"; +import { IWBERAStakerVault } from "src/pol/interfaces/IWBERAStakerVault.sol"; +import { + IBGTIncentiveFeeCollector_V1 as IBGTIncentiveFeeCollector +} from "./interfaces/IBGTIncentiveFeeCollector_V1.sol"; +import { IStakerVault } from "src/pol/interfaces/lst/IStakerVault.sol"; +import { ILSTAdapter } from "src/pol/interfaces/lst/ILSTAdapter.sol"; /// @title BGTIncentiveFeeCollector /// @author Berachain Team /// @notice Collects the fees on the incentives posted on reward vaults and auction them for WBERA. /// Accrued WBERA serves as a payout for the stakers of `WBERAStakerVault.sol` and added `LSTStakerVault.sol`s. /// @dev This contract is inspired by the `FeeCollector.sol` for auctioning collected tokens. -contract BGTIncentiveFeeCollector is +contract BGTIncentiveFeeCollector_V1 is IBGTIncentiveFeeCollector, PausableUpgradeable, AccessControlUpgradeable, @@ -244,6 +246,9 @@ contract BGTIncentiveFeeCollector is } // 0 edge case: no stakes or no amount to split + // In this case, claimer will be able to claim all the fee tokens without paying the payout amount of WBERA. + // We are aware of this issue and does not consider it a problem given totalStake being 0 is not practically, + // possible situation as all the LST vaults are deployed with initial supply. if (totalStake == 0 || amount == 0) { return amounts; } diff --git a/src/old_versions/V1_Contracts/BeraChef_V1.sol b/src/old_versions/V1_Contracts/BeraChef_V1.sol index bd7bece..d9c9b8f 100644 --- a/src/old_versions/V1_Contracts/BeraChef_V1.sol +++ b/src/old_versions/V1_Contracts/BeraChef_V1.sol @@ -7,7 +7,7 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { Utils } from "src/libraries/Utils.sol"; import { IBeaconDeposit } from "src/pol/interfaces/IBeaconDeposit.sol"; import { IBeraChef_V1 } from "./interfaces/IBeraChef_V1.sol"; -import { RewardVault } from "src/pol/rewards/RewardVault.sol"; +import { RewardVault_V1 as RewardVault } from "./RewardVault_V1.sol"; import { IRewardVaultFactory } from "src/pol/interfaces/IRewardVaultFactory.sol"; /// @title BeraChef diff --git a/src/old_versions/V1_Contracts/Distributor_V1.sol b/src/old_versions/V1_Contracts/Distributor_V1.sol new file mode 100644 index 0000000..c7d6a49 --- /dev/null +++ b/src/old_versions/V1_Contracts/Distributor_V1.sol @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; +import { Multicallable } from "solady/src/utils/Multicallable.sol"; + +import { Utils } from "src/libraries/Utils.sol"; +import { IBeraChef } from "src/pol/interfaces/IBeraChef.sol"; +import { IBlockRewardController } from "src/pol/interfaces/IBlockRewardController.sol"; +import { IDistributor_V1 as IDistributor } from "./interfaces/IDistributor_V1.sol"; +import { IRewardVault } from "src/pol/interfaces/IRewardVault.sol"; +import { BeaconRootsHelper } from "src/pol/BeaconRootsHelper.sol"; +import { IDedicatedEmissionStreamManager } from "src/pol/interfaces/IDedicatedEmissionStreamManager.sol"; +import { IRewardAllocation } from "src/pol/interfaces/IRewardAllocation.sol"; + +/// @title Distributor +/// @author Berachain Team +/// @notice The Distributor contract is responsible for distributing the block rewards from the reward controller +/// and the reward allocation weights, to the reward allocation receivers. +/// @dev Each validator has its own reward allocation, if it does not exist, a default reward allocation is used. +/// And if governance has not set the default reward allocation, the rewards are not minted and distributed. +contract Distributor_V1 is + IDistributor, + BeaconRootsHelper, + ReentrancyGuardUpgradeable, + AccessControlUpgradeable, + UUPSUpgradeable, + Multicallable +{ + using Utils for bytes4; + using Utils for address; + + /// @notice The MANAGER role. + bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); + + /// @dev Represents 100%. Chosen to be less granular. + uint96 internal constant ONE_HUNDRED_PERCENT = 1e4; + + /// @dev Address controlled by the execution layer client and used to call `distributeFor` function. + address private constant SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE; + + /// @dev Pectra11 hard fork timestamp. + /// @dev Bepolia: 1_754_496_000, 2025-08-06T16:00:00.000Z + /// @dev Mainnet: 1_756_915_200, 2025-09-03T16:00:00.000Z + uint64 private constant PECTRA11_HARD_FORK_TIMESTAMP = 1_756_915_200; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STORAGE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice The BeraChef contract that we are getting the reward allocation from. + IBeraChef public beraChef; + + /// @notice The rewards controller contract that we are getting the rewards rate from. + /// @dev And is responsible for minting the BGT token. + IBlockRewardController public blockRewardController; + + /// @notice The BGT token contract that we are distributing to the reward allocation receivers. + address public bgt; + + /// @notice The dedicated emission stream manager contract. + IDedicatedEmissionStreamManager public dedicatedEmissionStreamManager; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + address _berachef, + address _bgt, + address _blockRewardController, + address _governance, + uint64 _zeroValidatorPubkeyGIndex, + uint64 _proposerIndexGIndex + ) + external + initializer + { + __AccessControl_init(); + __ReentrancyGuard_init(); + __UUPSUpgradeable_init(); + _grantRole(DEFAULT_ADMIN_ROLE, _governance); + beraChef = IBeraChef(_berachef); + bgt = _bgt; + blockRewardController = IBlockRewardController(_blockRewardController); + super.setZeroValidatorPubkeyGIndex(_zeroValidatorPubkeyGIndex); + super.setProposerIndexGIndex(_proposerIndexGIndex); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ADMIN FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) { } + + /// @dev This is necessary to call when the beacon chain hard forks (and specifically the underlying structure of + /// beacon block header is modified). + function setZeroValidatorPubkeyGIndex(uint64 _zeroValidatorPubkeyGIndex) public override onlyRole(MANAGER_ROLE) { + super.setZeroValidatorPubkeyGIndex(_zeroValidatorPubkeyGIndex); + } + + /// @dev This is necessary to call when the beacon chain hard forks (and specifically the underlying structure of + /// beacon block header is modified). + function setProposerIndexGIndex(uint64 _proposerIndexGIndex) public override onlyRole(MANAGER_ROLE) { + super.setProposerIndexGIndex(_proposerIndexGIndex); + } + + /// @inheritdoc IDistributor + function setDedicatedEmissionStreamManager(address _dedicatedEmissionStreamManager) + external + onlyRole(DEFAULT_ADMIN_ROLE) + { + if (_dedicatedEmissionStreamManager == address(0)) { + ZeroAddress.selector.revertWith(); + } + emit DedicatedEmissionStreamManagerSet( + address(dedicatedEmissionStreamManager), _dedicatedEmissionStreamManager + ); + dedicatedEmissionStreamManager = IDedicatedEmissionStreamManager(_dedicatedEmissionStreamManager); + } + + /// @inheritdoc IDistributor + function distributeFor( + uint64 nextTimestamp, + uint64 proposerIndex, + bytes calldata pubkey, + bytes32[] calldata proposerIndexProof, + bytes32[] calldata pubkeyProof + ) + external + nonReentrant + { + // only allow permissionless distribution using proofs till hard fork timestamp + if (nextTimestamp >= PECTRA11_HARD_FORK_TIMESTAMP) { + OnlySystemCallAllowed.selector.revertWith(); + } + // Process the timestamp in the history buffer, reverting if already processed. + bytes32 beaconBlockRoot = _processTimestampInBuffer(nextTimestamp); + + // Verify the given proposer index is the true proposer index of the beacon block. + _verifyProposerIndexInBeaconBlock(beaconBlockRoot, proposerIndexProof, proposerIndex); + + // Verify the given pubkey is of a validator in the beacon block, at the given validator index. + _verifyValidatorPubkeyInBeaconBlock(beaconBlockRoot, pubkeyProof, pubkey, proposerIndex); + + // Distribute the rewards to the proposer validator. + _distributeFor(pubkey, nextTimestamp); + } + + /// @inheritdoc IDistributor + function distributeFor(bytes calldata pubkey) external onlySystemCall { + _distributeFor(pubkey, uint64(block.timestamp)); + } + + /// @dev Distributes the rewards for the given validator for the given timestamp's parent block. + function _distributeFor(bytes calldata pubkey, uint64 nextTimestamp) internal { + // Process the rewards with the block rewards controller for the specified block number. + // Its dependent on the beraChef being ready, if not it will return zero rewards for the current block. + uint256 rewardRate = blockRewardController.processRewards(pubkey, nextTimestamp, beraChef.isReady()); + if (rewardRate == 0) { + // If berachef is not ready (genesis) or there aren't rewards to distribute, skip. This will skip since + // there is no default reward allocation. + return; + } + + if (address(dedicatedEmissionStreamManager) != address(0)) { + uint256 emissionPerc = dedicatedEmissionStreamManager.emissionPerc(); + IRewardAllocation.Weight[] memory rewardAllocation = dedicatedEmissionStreamManager.getRewardAllocation(); + + if (emissionPerc > 0 && rewardAllocation.length > 0) { + uint256 graAmount = FixedPointMathLib.fullMulDiv(rewardRate, emissionPerc, ONE_HUNDRED_PERCENT); + uint256 excessEmission = _distributeRewards(graAmount, rewardAllocation, pubkey, nextTimestamp, true); + + // Decrease the reward rate by the reward allocation amount. + rewardRate -= graAmount - excessEmission; + } + } + + // Activate the queued reward allocation if it is ready. + beraChef.activateReadyQueuedRewardAllocation(pubkey); + + // Get the active reward allocation for the validator. + // This will return the default reward allocation if the validator does not have an active reward allocation. + IRewardAllocation.RewardAllocation memory ra = beraChef.getActiveRewardAllocation(pubkey); + _distributeRewards(rewardRate, ra.weights, pubkey, nextTimestamp, false); + } + + /// @dev Accumulates any excess emission that cannot be distributed to a vault in the global reward allocation, + /// due to the requested emission amount exceeding the vault's target emission (as governed by the dedicated + /// emission stream manager logic). This excess is considered "distributed" for the purpose of accounting within + /// the reward allocation, but is actually reallocated to the validator's (default) reward allocation. + function _distributeRewards( + uint256 amount, + IRewardAllocation.Weight[] memory weights, + bytes calldata pubkey, + uint64 nextTimestamp, + bool isDedicatedEmission + ) + internal + returns (uint256 excessEmission) + { + if (amount == 0) { + return 0; + } + + uint256 totalRewardDistributed; + uint256 length = weights.length; + + for (uint256 i; i < length;) { + IRewardAllocation.Weight memory weight = weights[i]; + address receiver = weight.receiver; + + uint256 rewardAmount; + // Compute base reward amount for this vault. + if (i == length - 1) { + // For the last vault, distribute the remaining rewards, excluding any excess emission. + rewardAmount = amount - totalRewardDistributed - excessEmission; + } else { + rewardAmount = FixedPointMathLib.fullMulDiv(amount, weight.percentageNumerator, ONE_HUNDRED_PERCENT); + } + + if (isDedicatedEmission) { + uint256 maxEmission = dedicatedEmissionStreamManager.getMaxEmission(receiver, rewardAmount); + excessEmission += rewardAmount - maxEmission; + // Only distribute the maximum allowable emission to the vault. + rewardAmount = maxEmission; + } + + if (i != length - 1) { + totalRewardDistributed += rewardAmount; + } + + if (rewardAmount > 0) { + // The reward vault will pull the rewards from this contract so we can keep the approvals for the + // soul bound token BGT clean. + bgt.safeIncreaseAllowance(receiver, rewardAmount); + + // Notify the receiver of the reward. + IRewardVault(receiver).notifyRewardAmount(pubkey, rewardAmount); + + if (isDedicatedEmission) { + dedicatedEmissionStreamManager.notifyEmission(receiver, rewardAmount); + } + + emit Distributed(pubkey, nextTimestamp, receiver, rewardAmount); + } + + unchecked { + ++i; + } + } + + return excessEmission; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* MODIFIERS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Modifier to restrict function access to system address. + /// @dev This ensures only the execution layer client can call `distributeFor` function. + modifier onlySystemCall() { + if (msg.sender != SYSTEM_ADDRESS) { + NotSystemAddress.selector.revertWith(); + } + _; + } +} diff --git a/src/old_versions/V1_Contracts/RewardVault_V1.sol b/src/old_versions/V1_Contracts/RewardVault_V1.sol index 74cc96d..21a3dca 100644 --- a/src/old_versions/V1_Contracts/RewardVault_V1.sol +++ b/src/old_versions/V1_Contracts/RewardVault_V1.sol @@ -10,8 +10,8 @@ import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; import { Utils } from "src/libraries/Utils.sol"; import { IBeaconDeposit } from "src/pol/interfaces/IBeaconDeposit.sol"; import { IRewardVault_V1 } from "./interfaces/IRewardVault_V1.sol"; -import { FactoryOwnable } from "src/base/FactoryOwnable.sol"; -import { StakingRewards } from "src/base/StakingRewards.sol"; +import { FactoryOwnable_V0 as FactoryOwnable } from "../V0_Contracts/base/FactoryOwnable_V0.sol"; +import { StakingRewards_V0 as StakingRewards } from "../V0_Contracts/base/StakingRewards_V0.sol"; import { IBeraChef } from "src/pol/interfaces/IBeraChef.sol"; import { IDistributor } from "src/pol/interfaces/IDistributor.sol"; import { IBGTIncentiveDistributor } from "src/pol/interfaces/IBGTIncentiveDistributor.sol"; diff --git a/src/pol/interfaces/IBGTIncentiveFeeCollector.sol b/src/old_versions/V1_Contracts/interfaces/IBGTIncentiveFeeCollector_V1.sol similarity index 98% rename from src/pol/interfaces/IBGTIncentiveFeeCollector.sol rename to src/old_versions/V1_Contracts/interfaces/IBGTIncentiveFeeCollector_V1.sol index 78061e4..69c0d56 100644 --- a/src/pol/interfaces/IBGTIncentiveFeeCollector.sol +++ b/src/old_versions/V1_Contracts/interfaces/IBGTIncentiveFeeCollector_V1.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; -import { IPOLErrors } from "./IPOLErrors.sol"; +import { IPOLErrors } from "src/pol/interfaces/IPOLErrors.sol"; -interface IBGTIncentiveFeeCollector is IPOLErrors { +interface IBGTIncentiveFeeCollector_V1 is IPOLErrors { /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* EVENTS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/src/old_versions/V1_Contracts/interfaces/IBeraChef_V1.sol b/src/old_versions/V1_Contracts/interfaces/IBeraChef_V1.sol index 6921018..bc7650b 100644 --- a/src/old_versions/V1_Contracts/interfaces/IBeraChef_V1.sol +++ b/src/old_versions/V1_Contracts/interfaces/IBeraChef_V1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.26; -import { IPOLErrors } from "src/pol/interfaces/IPOLErrors.sol"; +import { IPOLErrors_V0 as IPOLErrors } from "../../V0_Contracts/interfaces/IPOLErrors_V0.sol"; /// @notice Interface of the BeraChef module interface IBeraChef_V1 is IPOLErrors { diff --git a/src/old_versions/V1_Contracts/interfaces/IDistributor_V1.sol b/src/old_versions/V1_Contracts/interfaces/IDistributor_V1.sol new file mode 100644 index 0000000..fb90be9 --- /dev/null +++ b/src/old_versions/V1_Contracts/interfaces/IDistributor_V1.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import { IPOLErrors } from "src/pol/interfaces/IPOLErrors.sol"; +import { IBeraChef } from "src/pol/interfaces/IBeraChef.sol"; +import { IDedicatedEmissionStreamManager } from "src/pol/interfaces/IDedicatedEmissionStreamManager.sol"; + +/// @notice Interface of the Distributor contract. +interface IDistributor_V1 is IPOLErrors { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* EVENTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + event Distributed(bytes indexed valPubkey, uint64 indexed nextTimestamp, address indexed receiver, uint256 amount); + event DedicatedEmissionStreamManagerSet( + address indexed oldDedicatedEmissionStreamManager, address indexed newDedicatedEmissionStreamManager + ); + + /** + * @notice Distribute the rewards to the reward allocation receivers. + * @dev Permissionless function to distribute rewards by providing the necessary Merkle proofs. Reverts if the + * proofs are invalid. + * @param nextTimestamp The timestamp of the next beacon block to distribute for. The EIP-4788 Beacon Roots + * contract is queried by this key, returning the parent beacon block root from the next timestamp. + * @param proposerIndex The proposer index of the beacon block. This should be the validator index corresponding + * to the pubkey in the validator registry in the beacon state. + * @param pubkey The validator pubkey of the proposer. + * @param proposerIndexProof The Merkle proof of the proposer index in the beacon block. + * @param pubkeyProof The Merkle proof of the validator pubkey of the proposer in the beacon block. + */ + function distributeFor( + uint64 nextTimestamp, + uint64 proposerIndex, + bytes calldata pubkey, + bytes32[] calldata proposerIndexProof, + bytes32[] calldata pubkeyProof + ) + external; + + /// @notice Distribute the rewards to the reward allocation receivers according to BRIP-0004. + /// @dev This will be called for block N at the top of block N+1. + /// @dev Only system calls allowed i.e only the execution layer client can call this function. + /// @param pubkey The validator pubkey of the proposer. + function distributeFor(bytes calldata pubkey) external; + + /// @notice Returns the address of the BeraChef contract. + function beraChef() external view returns (IBeraChef); + + /// @notice Returns the address of the dedicated emission stream manager contract. + function dedicatedEmissionStreamManager() external view returns (IDedicatedEmissionStreamManager); + + /// @notice Sets the address of the dedicated emission stream manager contract. + function setDedicatedEmissionStreamManager(address _dedicatedEmissionStreamManager) external; +} diff --git a/src/old_versions/V2_Contracts/BeraChef_V2.sol b/src/old_versions/V2_Contracts/BeraChef_V2.sol index 50f1f02..c38c96f 100644 --- a/src/old_versions/V2_Contracts/BeraChef_V2.sol +++ b/src/old_versions/V2_Contracts/BeraChef_V2.sol @@ -7,7 +7,7 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { Utils } from "src/libraries/Utils.sol"; import { IBeaconDeposit } from "src/pol/interfaces/IBeaconDeposit.sol"; import { IBeraChef_V2 } from "./interfaces/IBeraChef_V2.sol"; -import { RewardVault } from "src/pol/rewards/RewardVault.sol"; +import { RewardVault_V2 as RewardVault } from "./RewardVault_V2.sol"; import { IRewardVaultFactory } from "src/pol/interfaces/IRewardVaultFactory.sol"; /// @title BeraChef diff --git a/src/old_versions/V2_Contracts/RewardVaultFactory_V2.sol b/src/old_versions/V2_Contracts/RewardVaultFactory_V2.sol index 1d51c35..058f6e0 100644 --- a/src/old_versions/V2_Contracts/RewardVaultFactory_V2.sol +++ b/src/old_versions/V2_Contracts/RewardVaultFactory_V2.sol @@ -7,7 +7,7 @@ import { LibClone } from "solady/src/utils/LibClone.sol"; import { UpgradeableBeacon } from "solady/src/utils/UpgradeableBeacon.sol"; import { Utils } from "../../libraries/Utils.sol"; import { IRewardVaultFactory } from "./interfaces/IRewardVaultFactory_V2.sol"; -import { RewardVault } from "src/pol/rewards/RewardVault.sol"; +import { RewardVault_V2 as RewardVault } from "./RewardVault_V2.sol"; /// @title RewardVaultFactory /// @author Berachain Team diff --git a/src/old_versions/V2_Contracts/RewardVault_V2.sol b/src/old_versions/V2_Contracts/RewardVault_V2.sol index d611f56..ffccc1a 100644 --- a/src/old_versions/V2_Contracts/RewardVault_V2.sol +++ b/src/old_versions/V2_Contracts/RewardVault_V2.sol @@ -10,8 +10,8 @@ import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; import { Utils } from "src/libraries/Utils.sol"; import { IBeaconDeposit } from "src/pol/interfaces/IBeaconDeposit.sol"; import { IRewardVault_V1 } from "../V1_Contracts/interfaces/IRewardVault_V1.sol"; -import { FactoryOwnable } from "src/base/FactoryOwnable.sol"; -import { StakingRewards } from "src/base/StakingRewards.sol"; +import { FactoryOwnable_V0 as FactoryOwnable } from "../V0_Contracts/base/FactoryOwnable_V0.sol"; +import { StakingRewards_V0 } from "src/old_versions/V0_Contracts/base/StakingRewards_V0.sol"; import { IBeraChef } from "src/pol/interfaces/IBeraChef.sol"; import { IDistributor } from "src/pol/interfaces/IDistributor.sol"; import { IBGTIncentiveDistributor } from "src/pol/interfaces/IBGTIncentiveDistributor.sol"; @@ -27,7 +27,7 @@ contract RewardVault_V2 is PausableUpgradeable, ReentrancyGuardUpgradeable, FactoryOwnable, - StakingRewards, + StakingRewards_V0, IRewardVault_V1 { using Utils for bytes4; diff --git a/src/old_versions/V2_Contracts/interfaces/IRewardVaultFactory_V2.sol b/src/old_versions/V2_Contracts/interfaces/IRewardVaultFactory_V2.sol index 098b772..6fb7416 100644 --- a/src/old_versions/V2_Contracts/interfaces/IRewardVaultFactory_V2.sol +++ b/src/old_versions/V2_Contracts/interfaces/IRewardVaultFactory_V2.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.26; -import { IPOLErrors } from "src/pol/interfaces/IPOLErrors.sol"; +import { IPOLErrors_V0 as IPOLErrors } from "../../V0_Contracts/interfaces/IPOLErrors_V0.sol"; interface IRewardVaultFactory is IPOLErrors { /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ diff --git a/src/old_versions/V3_Contracts/BeraChef_V3.sol b/src/old_versions/V3_Contracts/BeraChef_V3.sol index 7c6c86c..31fd514 100644 --- a/src/old_versions/V3_Contracts/BeraChef_V3.sol +++ b/src/old_versions/V3_Contracts/BeraChef_V3.sol @@ -7,7 +7,7 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { Utils } from "../../libraries/Utils.sol"; import { IBeaconDeposit } from "src/pol/interfaces/IBeaconDeposit.sol"; import { IBeraChef_V3 } from "./interfaces/IBeraChef_V3.sol"; -import { RewardVault } from "src/pol/rewards/RewardVault.sol"; +import { RewardVault_V9 as RewardVault } from "../V9_Contracts/RewardVault_V9.sol"; import { IRewardVaultFactory } from "src/pol/interfaces/IRewardVaultFactory.sol"; import { IRewardAllocatorFactory } from "src/pol/interfaces/IRewardAllocatorFactory.sol"; diff --git a/src/old_versions/V3_Contracts/RewardVaultFactory_V3.sol b/src/old_versions/V3_Contracts/RewardVaultFactory_V3.sol new file mode 100644 index 0000000..429167d --- /dev/null +++ b/src/old_versions/V3_Contracts/RewardVaultFactory_V3.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { LibClone } from "solady/src/utils/LibClone.sol"; +import { UpgradeableBeacon } from "solady/src/utils/UpgradeableBeacon.sol"; +import { Utils } from "../../libraries/Utils.sol"; +import { IRewardVaultFactory_V3 as IRewardVaultFactory } from "./interfaces/IRewardVaultFactory_V3.sol"; +import { RewardVault_V9 as RewardVault } from "../V9_Contracts/RewardVault_V9.sol"; + +/// @title RewardVaultFactory +/// @author Berachain Team +/// @notice Factory contract for creating RewardVaults and keeping track of them. +contract RewardVaultFactory is IRewardVaultFactory, AccessControlUpgradeable, UUPSUpgradeable { + using Utils for bytes4; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STORAGE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice The VAULT MANAGER role. + bytes32 public constant VAULT_MANAGER_ROLE = keccak256("VAULT_MANAGER_ROLE"); + + /// @notice The VAULT PAUSER role. + bytes32 public constant VAULT_PAUSER_ROLE = keccak256("VAULT_PAUSER_ROLE"); + + /// @notice Maximum fee rate in basis points (100%). + uint256 private constant MAX_INC_FEE_RATE = 10_000; + + /// @notice 100% constant in basis points. + uint256 private constant ONE_HUNDRED_PERCENT = 10_000; + + /// @notice The beacon address. + address public beacon; + + /// @notice The BGT token address. + address public bgt; + + /// @notice The distributor address. + address public distributor; + + /// @notice The BeaconDeposit contract address. + address public beaconDepositContract; + + /// @notice Mapping of staking token to vault address. + mapping(address stakingToken => address vault) public getVault; + + /// @notice Array of all vaults that have been created. + address[] public allVaults; + + /// @notice The address of the BGTIncentiveDistributor contract to receive + /// the BGT booster share of the incentive tokens. + address public bgtIncentiveDistributor; + + /// @notice Fee rate on incentives in basis points (e.g., 100 = 1%). + uint256 public bgtIncentiveFeeRate; + + /// @notice The address of the BGTIncentiveFeeCollector contract to receive fees. + address public bgtIncentiveFeeCollector; + + /// @notice The address of the RewardVaultHelper contract to help claim rewards. + address public rewardVaultHelper; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + address _bgt, + address _distributor, + address _beaconDepositContract, + address _governance, + address _vaultImpl + ) + external + initializer + { + __AccessControl_init(); + __UUPSUpgradeable_init(); + _grantRole(DEFAULT_ADMIN_ROLE, _governance); + // Allow the vault manager to manage the vault pauser role. + // vault manager can grant and revoke the access for the vault pauser role. + _setRoleAdmin(VAULT_PAUSER_ROLE, VAULT_MANAGER_ROLE); + // slither-disable-next-line missing-zero-check + bgt = _bgt; + // slither-disable-next-line missing-zero-check + distributor = _distributor; + // slither-disable-next-line missing-zero-check + beaconDepositContract = _beaconDepositContract; + + beacon = address(new UpgradeableBeacon(_governance, _vaultImpl)); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ADMIN */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) { } + + /// @inheritdoc IRewardVaultFactory + function setBGTIncentiveDistributor(address _bgtIncentiveDistributor) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_bgtIncentiveDistributor == address(0)) ZeroAddress.selector.revertWith(); + emit BGTIncentiveDistributorSet(_bgtIncentiveDistributor, bgtIncentiveDistributor); + bgtIncentiveDistributor = _bgtIncentiveDistributor; + } + + /// @inheritdoc IRewardVaultFactory + function setBGTIncentiveFeeRate(uint256 _bgtIncentiveFeeRate) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_bgtIncentiveFeeRate > MAX_INC_FEE_RATE) InvalidIncentiveFeeRate.selector.revertWith(); + emit IncentiveFeeRateUpdated(_bgtIncentiveFeeRate, bgtIncentiveFeeRate); + bgtIncentiveFeeRate = _bgtIncentiveFeeRate; + } + + /// @inheritdoc IRewardVaultFactory + function setBGTIncentiveFeeCollector(address _bgtIncentiveFeeCollector) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_bgtIncentiveFeeCollector == address(0)) ZeroAddress.selector.revertWith(); + emit IncentiveFeeCollectorUpdated(_bgtIncentiveFeeCollector, bgtIncentiveFeeCollector); + bgtIncentiveFeeCollector = _bgtIncentiveFeeCollector; + } + + /// @inheritdoc IRewardVaultFactory + function setRewardVaultHelper(address _rewardVaultHelper) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_rewardVaultHelper == address(0)) ZeroAddress.selector.revertWith(); + emit RewardVaultHelperUpdated(_rewardVaultHelper, rewardVaultHelper); + rewardVaultHelper = _rewardVaultHelper; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* VAULT CREATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IRewardVaultFactory + function createRewardVault(address stakingToken) external returns (address) { + address cachedAddress = getVault[stakingToken]; + if (cachedAddress != address(0)) return cachedAddress; + + // Check the code size of the staking token. + if (stakingToken.code.length == 0) NotAContract.selector.revertWith(); + + // Use solady library to deploy deterministic clone of vaultImpl. + bytes32 salt; + assembly ("memory-safe") { + mstore(0, stakingToken) + salt := keccak256(0, 0x20) + } + address vault = LibClone.deployDeterministicERC1967BeaconProxy(beacon, salt); + + // Store the vault in the mapping and array. + getVault[stakingToken] = vault; + allVaults.push(vault); + emit VaultCreated(stakingToken, vault); + + // Initialize the vault. + RewardVault(vault).initialize(beaconDepositContract, bgt, distributor, stakingToken); + + return vault; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* READS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IRewardVaultFactory + function predictRewardVaultAddress(address stakingToken) external view returns (address) { + bytes32 salt; + assembly ("memory-safe") { + mstore(0, stakingToken) + salt := keccak256(0, 0x20) + } + return LibClone.predictDeterministicAddressERC1967BeaconProxy(beacon, salt, address(this)); + } + + /// @inheritdoc IRewardVaultFactory + function allVaultsLength() external view returns (uint256) { + return allVaults.length; + } + + /// @inheritdoc IRewardVaultFactory + function getIncentiveFeeAmount(uint256 incentiveAmount) external view returns (uint256) { + return (incentiveAmount * bgtIncentiveFeeRate) / ONE_HUNDRED_PERCENT; + } +} diff --git a/src/old_versions/V3_Contracts/RewardVault_V3.sol b/src/old_versions/V3_Contracts/RewardVault_V3.sol index 3caeb33..124de6c 100644 --- a/src/old_versions/V3_Contracts/RewardVault_V3.sol +++ b/src/old_versions/V3_Contracts/RewardVault_V3.sol @@ -10,8 +10,8 @@ import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; import { Utils } from "src/libraries/Utils.sol"; import { IBeaconDeposit } from "src/pol/interfaces/IBeaconDeposit.sol"; import { IRewardVault_V3 } from "./interfaces/IRewardVault_V3.sol"; -import { FactoryOwnable } from "src/base/FactoryOwnable.sol"; -import { StakingRewards } from "src/base/StakingRewards.sol"; +import { FactoryOwnable_V0 as FactoryOwnable } from "../V0_Contracts/base/FactoryOwnable_V0.sol"; +import { StakingRewards_V0 } from "src/old_versions/V0_Contracts/base/StakingRewards_V0.sol"; import { IBeraChef } from "src/pol/interfaces/IBeraChef.sol"; import { IDistributor } from "src/pol/interfaces/IDistributor.sol"; import { IBGTIncentiveDistributor } from "src/pol/interfaces/IBGTIncentiveDistributor.sol"; @@ -27,7 +27,7 @@ contract RewardVault_V3 is PausableUpgradeable, ReentrancyGuardUpgradeable, FactoryOwnable, - StakingRewards, + StakingRewards_V0, IRewardVault_V3 { using Utils for bytes4; diff --git a/src/old_versions/V3_Contracts/interfaces/IRewardVaultFactory_V3.sol b/src/old_versions/V3_Contracts/interfaces/IRewardVaultFactory_V3.sol new file mode 100644 index 0000000..f36f125 --- /dev/null +++ b/src/old_versions/V3_Contracts/interfaces/IRewardVaultFactory_V3.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import { IPOLErrors_V0 as IPOLErrors } from "../../V0_Contracts/interfaces/IPOLErrors_V0.sol"; + +interface IRewardVaultFactory_V3 is IPOLErrors { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* EVENTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /** + * @notice Emitted when a new vault is created. + * @param stakingToken The address of the staking token. + * @param vault The address of the vault. + */ + event VaultCreated(address indexed stakingToken, address indexed vault); + + /** + * @notice Emitted when the BGTIncentiveDistributor contract is set. + * @param newBGTIncentiveDistributor The address of the new BGTIncentiveDistributor contract. + * @param oldBGTIncentiveDistributor The address of the old BGTIncentiveDistributor contract. + */ + event BGTIncentiveDistributorSet( + address indexed newBGTIncentiveDistributor, address indexed oldBGTIncentiveDistributor + ); + + /** + * @notice Emitted when the incentive fee percentage is updated. + * @param newValue The new rate (in basis points). + * @param oldValue The old rate (in basis points). + */ + event IncentiveFeeRateUpdated(uint256 newValue, uint256 oldValue); + + /** + * @notice Emitted when the incentive fee collector address is updated. + * @param newAddress The new address for incentive fees. + * @param oldAddress The old address for incentive fees. + */ + event IncentiveFeeCollectorUpdated(address newAddress, address oldAddress); + + /** + * @notice Emitted when the reward vault helper address is updated. + * @param newAddress The new address for reward vault helper. + * @param oldAddress The old address for reward vault helper. + */ + event RewardVaultHelperUpdated(address newAddress, address oldAddress); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ADMIN */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /** + * @notice Sets the BGTIncentiveDistributor contract. + * @dev Only callable by the admin. + * @param _bgtIncentiveDistributor The address of the new BGTIncentiveDistributor contract. + */ + function setBGTIncentiveDistributor(address _bgtIncentiveDistributor) external; + + /** + * @notice Sets the incentives fee rate. + * @dev Only callable by the admin. + * @param _bgtIncentiveFeeRate The new value for the rate (in basis points). + */ + function setBGTIncentiveFeeRate(uint256 _bgtIncentiveFeeRate) external; + + /** + * @notice Sets the BGTIncentiveDistributor contract. + * @dev Only callable by the admin. + * @param _bgtIncentiveFeeCollector The address of the new BGTIncentiveFeeCollector contract. + */ + function setBGTIncentiveFeeCollector(address _bgtIncentiveFeeCollector) external; + + /** + * @notice Sets the reward vault helper address. + * @dev Only callable by the admin. + * @param _rewardVaultHelper The address of the new RewardVaultHelper contract. + */ + function setRewardVaultHelper(address _rewardVaultHelper) external; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* VAULT CREATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /** + * @notice Creates a new reward vault vault for the given staking token. + * @dev Reverts if the staking token is not a contract. + * @param stakingToken The address of the staking token. + * @return The address of the new vault. + */ + function createRewardVault(address stakingToken) external returns (address); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* READS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /** + * @notice Gets the VAULT_MANAGER_ROLE. + * @return The VAULT_MANAGER_ROLE. + */ + function VAULT_MANAGER_ROLE() external view returns (bytes32); + + /** + * @notice Gets the VAULT_PAUSER_ROLE. + * @return The VAULT_PAUSER_ROLE. + */ + function VAULT_PAUSER_ROLE() external view returns (bytes32); + + /** + * @notice Gets the vault for the given staking token. + * @param stakingToken The address of the staking token. + * @return The address of the vault. + */ + function getVault(address stakingToken) external view returns (address); + + /** + * @notice Gets the number of vaults that have been created. + * @return The number of vaults. + */ + function allVaultsLength() external view returns (uint256); + + /** + * @notice Gets the address of the BGTIncentiveDistributor contract. + * @return The address of the BGTIncentiveDistributor contract. + */ + function bgtIncentiveDistributor() external view returns (address); + + /** + * @notice Gets the address of the RewardVaultHelper contract. + * @return The address of the RewardVaultHelper contract. + */ + function rewardVaultHelper() external view returns (address); + + /** + * @notice Predicts the address of the reward vault for the given staking token. + * @param stakingToken The address of the staking token. + * @return The address of the reward vault. + */ + function predictRewardVaultAddress(address stakingToken) external view returns (address); + + /** + * @notice Gets the value of the incentive fee rate. + * @return The rate (in basis points). + */ + function bgtIncentiveFeeRate() external view returns (uint256); + + /** + * @notice Gets the address of the incentive fee collector. + * @return The address of the BGTIncentiveFeeCollector contract. + */ + function bgtIncentiveFeeCollector() external view returns (address); + + /** + * @notice Applies the fee percentage on the incentive amount. + * @param incentiveAmount The amount of incentive tokens. + * @return The fee amount. + */ + function getIncentiveFeeAmount(uint256 incentiveAmount) external view returns (uint256); +} diff --git a/src/old_versions/V4_Contracts/RewardVault_V4.sol b/src/old_versions/V4_Contracts/RewardVault_V4.sol index a665105..a85a4c8 100644 --- a/src/old_versions/V4_Contracts/RewardVault_V4.sol +++ b/src/old_versions/V4_Contracts/RewardVault_V4.sol @@ -10,8 +10,8 @@ import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; import { Utils } from "src/libraries/Utils.sol"; import { IBeaconDeposit } from "src/pol/interfaces/IBeaconDeposit.sol"; import { IRewardVault_V3 } from "../V3_Contracts/interfaces/IRewardVault_V3.sol"; -import { FactoryOwnable } from "src/base/FactoryOwnable.sol"; -import { StakingRewards } from "src/base/StakingRewards.sol"; +import { FactoryOwnable_V0 as FactoryOwnable } from "../V0_Contracts/base/FactoryOwnable_V0.sol"; +import { StakingRewards_V0 } from "src/old_versions/V0_Contracts/base/StakingRewards_V0.sol"; import { IBeraChef } from "src/pol/interfaces/IBeraChef.sol"; import { IDistributor } from "src/pol/interfaces/IDistributor.sol"; import { IBGTIncentiveDistributor } from "src/pol/interfaces/IBGTIncentiveDistributor.sol"; @@ -27,7 +27,7 @@ contract RewardVault_V4 is PausableUpgradeable, ReentrancyGuardUpgradeable, FactoryOwnable, - StakingRewards, + StakingRewards_V0, IRewardVault_V3 { using Utils for bytes4; diff --git a/src/old_versions/V5_Contracts/RewardVault_V5.sol b/src/old_versions/V5_Contracts/RewardVault_V5.sol index 2b60ce3..06e3763 100644 --- a/src/old_versions/V5_Contracts/RewardVault_V5.sol +++ b/src/old_versions/V5_Contracts/RewardVault_V5.sol @@ -10,8 +10,8 @@ import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; import { Utils } from "src/libraries/Utils.sol"; import { IBeaconDeposit } from "src/pol/interfaces/IBeaconDeposit.sol"; import { IRewardVault_V3 } from "../V3_Contracts/interfaces/IRewardVault_V3.sol"; -import { FactoryOwnable } from "src/base/FactoryOwnable.sol"; -import { StakingRewards } from "src/base/StakingRewards.sol"; +import { FactoryOwnable_V0 as FactoryOwnable } from "../V0_Contracts/base/FactoryOwnable_V0.sol"; +import { StakingRewards_V0 } from "src/old_versions/V0_Contracts/base/StakingRewards_V0.sol"; import { IBeraChef } from "src/pol/interfaces/IBeraChef.sol"; import { IDistributor } from "src/pol/interfaces/IDistributor.sol"; import { IBGTIncentiveDistributor } from "src/pol/interfaces/IBGTIncentiveDistributor.sol"; @@ -27,7 +27,7 @@ contract RewardVault_V5 is PausableUpgradeable, ReentrancyGuardUpgradeable, FactoryOwnable, - StakingRewards, + StakingRewards_V0, IRewardVault_V3 { using Utils for bytes4; diff --git a/src/old_versions/V6_Contracts/IRewardVault_V6.sol b/src/old_versions/V6_Contracts/IRewardVault_V6.sol index e3e0a3c..df84833 100644 --- a/src/old_versions/V6_Contracts/IRewardVault_V6.sol +++ b/src/old_versions/V6_Contracts/IRewardVault_V6.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.26; -import { IPOLErrors } from "src/pol/interfaces/IPOLErrors.sol"; +import { IPOLErrors_V0 as IPOLErrors } from "../V0_Contracts/interfaces/IPOLErrors_V0.sol"; import { IStakingRewards } from "src/base/IStakingRewards.sol"; interface IRewardVault_V6 is IPOLErrors, IStakingRewards { diff --git a/src/old_versions/V6_Contracts/RewardVault_V6.sol b/src/old_versions/V6_Contracts/RewardVault_V6.sol index 1223242..0f6f851 100644 --- a/src/old_versions/V6_Contracts/RewardVault_V6.sol +++ b/src/old_versions/V6_Contracts/RewardVault_V6.sol @@ -10,8 +10,8 @@ import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; import { Utils } from "src/libraries/Utils.sol"; import { IBeaconDeposit } from "src/pol/interfaces/IBeaconDeposit.sol"; import { IRewardVault_V6 } from "./IRewardVault_V6.sol"; -import { FactoryOwnable } from "src/base/FactoryOwnable.sol"; -import { StakingRewards } from "src/base/StakingRewards.sol"; +import { FactoryOwnable_V0 as FactoryOwnable } from "../V0_Contracts/base/FactoryOwnable_V0.sol"; +import { StakingRewards_V0 } from "src/old_versions/V0_Contracts/base/StakingRewards_V0.sol"; import { IBeraChef } from "src/pol/interfaces/IBeraChef.sol"; import { IDistributor } from "src/pol/interfaces/IDistributor.sol"; import { IBGTIncentiveDistributor } from "src/pol/interfaces/IBGTIncentiveDistributor.sol"; @@ -27,7 +27,7 @@ contract RewardVault_V6 is PausableUpgradeable, ReentrancyGuardUpgradeable, FactoryOwnable, - StakingRewards, + StakingRewards_V0, IRewardVault_V6 { using Utils for bytes4; diff --git a/src/old_versions/V7_Contracts/RewardVault_V7.sol b/src/old_versions/V7_Contracts/RewardVault_V7.sol index 75f329d..3ad3c6e 100644 --- a/src/old_versions/V7_Contracts/RewardVault_V7.sol +++ b/src/old_versions/V7_Contracts/RewardVault_V7.sol @@ -10,12 +10,12 @@ import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; import { Utils } from "src/libraries/Utils.sol"; import { IBeaconDeposit } from "src/pol/interfaces/IBeaconDeposit.sol"; import { IRewardVault_V7 } from "./IRewardVault_V7.sol"; -import { FactoryOwnable } from "src/base/FactoryOwnable.sol"; -import { StakingRewards } from "src/base/StakingRewards.sol"; +import { FactoryOwnable_V0 as FactoryOwnable } from "../V0_Contracts/base/FactoryOwnable_V0.sol"; +import { StakingRewards_V0 } from "src/old_versions/V0_Contracts/base/StakingRewards_V0.sol"; import { IBeraChef } from "src/pol/interfaces/IBeraChef.sol"; import { IDistributor } from "src/pol/interfaces/IDistributor.sol"; import { IBGTIncentiveDistributor } from "src/pol/interfaces/IBGTIncentiveDistributor.sol"; -import { IRewardVaultFactory } from "src/pol/interfaces/IRewardVaultFactory.sol"; +import { IRewardVaultFactory_V3 as IRewardVaultFactory } from "../V3_Contracts/interfaces/IRewardVaultFactory_V3.sol"; /// @title Rewards Vault /// @author Berachain Team @@ -28,7 +28,7 @@ contract RewardVault_V7 is PausableUpgradeable, ReentrancyGuardUpgradeable, FactoryOwnable, - StakingRewards, + StakingRewards_V0, IRewardVault_V7 { using Utils for bytes4; diff --git a/src/old_versions/V8_Contracts/RewardVault_V8.sol b/src/old_versions/V8_Contracts/RewardVault_V8.sol index 0b50513..4ae220f 100644 --- a/src/old_versions/V8_Contracts/RewardVault_V8.sol +++ b/src/old_versions/V8_Contracts/RewardVault_V8.sol @@ -10,12 +10,12 @@ import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; import { Utils } from "../../libraries/Utils.sol"; import { IBeaconDeposit } from "src/pol/interfaces/IBeaconDeposit.sol"; import { IRewardVault } from "./interfaces/IRewardVault_V8.sol"; -import { FactoryOwnable } from "src/base/FactoryOwnable.sol"; -import { StakingRewards } from "src/base/StakingRewards.sol"; +import { FactoryOwnable_V0 as FactoryOwnable } from "../V0_Contracts/base/FactoryOwnable_V0.sol"; +import { StakingRewards_V0 } from "src/old_versions/V0_Contracts/base/StakingRewards_V0.sol"; import { IBeraChef } from "src/pol/interfaces/IBeraChef.sol"; import { IDistributor } from "src/pol/interfaces/IDistributor.sol"; import { IBGTIncentiveDistributor } from "src/pol/interfaces/IBGTIncentiveDistributor.sol"; -import { IRewardVaultFactory } from "src/pol/interfaces/IRewardVaultFactory.sol"; +import { IRewardVaultFactory_V3 as IRewardVaultFactory } from "../V3_Contracts/interfaces/IRewardVaultFactory_V3.sol"; /// @title Rewards Vault /// @author Berachain Team @@ -24,7 +24,13 @@ import { IRewardVaultFactory } from "src/pol/interfaces/IRewardVaultFactory.sol" /// https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol /// We are using this model instead of 4626 because we want to incentivize staying in the vault for x period of time /// to be considered a 'miner' and not a 'trader'. -contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, FactoryOwnable, StakingRewards, IRewardVault { +contract RewardVault is + PausableUpgradeable, + ReentrancyGuardUpgradeable, + FactoryOwnable, + StakingRewards_V0, + IRewardVault +{ using Utils for bytes4; using SafeERC20 for IERC20; using Utils for address; diff --git a/src/old_versions/V9_Contracts/RewardVault_V9.sol b/src/old_versions/V9_Contracts/RewardVault_V9.sol new file mode 100644 index 0000000..8fa4329 --- /dev/null +++ b/src/old_versions/V9_Contracts/RewardVault_V9.sol @@ -0,0 +1,721 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; + +import { Utils } from "../../libraries/Utils.sol"; +import { IBeaconDeposit } from "src/pol/interfaces/IBeaconDeposit.sol"; +import { IRewardVault_V9 as IRewardVault } from "./interfaces/IRewardVault_V9.sol"; +import { FactoryOwnable_V0 as FactoryOwnable } from "../V0_Contracts/base/FactoryOwnable_V0.sol"; +import { StakingRewards_V0 } from "src/old_versions/V0_Contracts/base/StakingRewards_V0.sol"; +import { IBeraChef } from "src/pol/interfaces/IBeraChef.sol"; +import { IDistributor } from "src/pol/interfaces/IDistributor.sol"; +import { IBGTIncentiveDistributor } from "src/pol/interfaces/IBGTIncentiveDistributor.sol"; +import { IRewardVaultFactory_V3 as IRewardVaultFactory } from "../V3_Contracts/interfaces/IRewardVaultFactory_V3.sol"; + +/// @title Rewards Vault +/// @author Berachain Team +/// @notice This contract is the vault for the Berachain rewards, it handles the staking and rewards accounting of BGT. +/// @dev This contract is taken from the stable and tested: +/// https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol +/// We are using this model instead of 4626 because we want to incentivize staying in the vault for x period of time +/// to be considered a 'miner' and not a 'trader'. +contract RewardVault_V9 is + PausableUpgradeable, + ReentrancyGuardUpgradeable, + FactoryOwnable, + StakingRewards_V0, + IRewardVault +{ + using Utils for bytes4; + using SafeERC20 for IERC20; + using Utils for address; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STRUCTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Struct to hold delegate stake data. + /// @param delegateTotalStaked The total amount staked by delegates. + /// @param stakedByDelegate The mapping of the amount staked by each delegate. + struct DelegateStake { + uint256 delegateTotalStaked; + mapping(address delegate => uint256 amount) stakedByDelegate; + } + + /// @notice Struct to hold an incentive data. + /// @param minIncentiveRate The minimum amount of the token to incentivize per BGT emission. + /// @param incentiveRate The amount of the token to incentivize per BGT emission. + /// @param amountRemaining The amount of the token remaining to incentivize. + /// @param manager The address of the manager that can addIncentive for this incentive token. + struct Incentive { + uint256 minIncentiveRate; + uint256 incentiveRate; + uint256 amountRemaining; + address manager; + } + + uint256 private constant MAX_INCENTIVE_RATE = 1e36; // for 18 decimal token, this will mean 1e18 incentiveTokens + // per BGT emission. + + // Safe gas limit for low level call operations to avoid griefing. + // This is mostly for low level call like approve, receiveIncentive (IBGTIncentiveDistributor which uses + // transferFrom). + uint256 private constant SAFE_GAS_LIMIT = 500_000; + + /// @notice The minimum reward duration. + uint256 public constant MIN_REWARD_DURATION = 3 days; + /// @notice The maximum reward duration. + uint256 public constant MAX_REWARD_DURATION = 7 days; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STORAGE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice The maximum count of incentive tokens that can be stored. + uint8 public maxIncentiveTokensCount; + + /// @notice The address of the distributor contract. + address public distributor; + + /// @notice The BeaconDeposit contract. + IBeaconDeposit public beaconDepositContract; + + mapping(address account => DelegateStake) internal _delegateStake; + + /// @notice The mapping of accounts to their operators. + mapping(address account => address operator) internal _operators; + + /// @notice The mapping of incentive token to its incentive data. + mapping(address token => Incentive) public incentives; + + /// @notice The list of whitelisted tokens. + address[] public whitelistedTokens; + + /// @notice The address authorized to manage reward vault operations and configurations. + /// @dev This role is typically assigned to dApp teams to enable them to configure reward distribution parameters. + address public rewardVaultManager; + + // deprecated + uint256 private _lastRewardDurationChangeTimestamp; + + /// @notice The target rewards per second, scaled by PRECISION. + /// @dev This acts as both a maximum and a target rate. When the calculated reward rate exceeds this value, + /// the duration is dynamically adjusted to achieve this target rate, but never goes below MIN_REWARD_DURATION. + /// This prevents the issue where a spike in rewards would permanently expand the duration, causing subsequent + /// smaller rewards to be spread over longer periods with very low rates. + uint256 public targetRewardsPerSecond; + + /// @notice The pending rewards duration. + /// @dev Comes into effect during the next `notifyRewardAmount` call. + uint256 public pendingRewardsDuration; + + /// @notice The reward duration in case targetRewardsPerSecond is not met. + /// @dev must be between MIN_REWARD_DURATION and MAX_REWARD_DURATION and can be set only by reward vault manager. + uint256 public minRewardDurationForTargetRate; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @inheritdoc IRewardVault + function initialize( + address _beaconDepositContract, + address _bgt, + address _distributor, + address _stakingToken + ) + external + initializer + { + __FactoryOwnable_init(msg.sender); + __Pausable_init(); + __ReentrancyGuard_init(); + __StakingRewards_init(_stakingToken, _bgt, 7 days); + maxIncentiveTokensCount = 3; + // slither-disable-next-line missing-zero-check + distributor = _distributor; + beaconDepositContract = IBeaconDeposit(_beaconDepositContract); + emit DistributorSet(_distributor); + emit MaxIncentiveTokensCountUpdated(maxIncentiveTokensCount); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* MODIFIERS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + modifier onlyDistributor() { + if (msg.sender != distributor) NotDistributor.selector.revertWith(); + _; + } + + modifier onlyUserOrOperator(address account) { + IRewardVaultFactory factory = IRewardVaultFactory(factory()); + address rewardVaultHelper = factory.rewardVaultHelper(); + + if (msg.sender != account && msg.sender != rewardVaultHelper) { + if (msg.sender != _operators[account]) { + NotOperator.selector.revertWith(); + } + } + _; + } + + modifier checkSelfStakedBalance(address account, uint256 amount) { + _checkSelfStakedBalance(account, amount); + _; + } + + modifier onlyWhitelistedToken(address token) { + if (incentives[token].minIncentiveRate == 0) TokenNotWhitelisted.selector.revertWith(); + _; + } + + modifier onlyRewardVaultManager() { + if (msg.sender != rewardVaultManager) NotRewardVaultManager.selector.revertWith(); + _; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ADMIN FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IRewardVault + function setDistributor(address _rewardDistribution) external onlyFactoryOwner { + if (_rewardDistribution == address(0)) ZeroAddress.selector.revertWith(); + distributor = _rewardDistribution; + emit DistributorSet(_rewardDistribution); + } + + /// @inheritdoc IRewardVault + function notifyRewardAmount(bytes calldata pubkey, uint256 reward) external onlyDistributor { + _notifyRewardAmount(reward); + _processIncentives(pubkey, reward); + } + + /// @inheritdoc IRewardVault + function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyFactoryOwner { + if (incentives[tokenAddress].minIncentiveRate != 0) CannotRecoverIncentiveToken.selector.revertWith(); + if (tokenAddress == address(stakeToken)) { + uint256 maxRecoveryAmount = IERC20(stakeToken).balanceOf(address(this)) - totalSupply; + if (tokenAmount > maxRecoveryAmount) { + NotEnoughBalance.selector.revertWith(); + } + } + IERC20(tokenAddress).safeTransfer(msg.sender, tokenAmount); + emit Recovered(tokenAddress, tokenAmount); + } + + /// @inheritdoc IRewardVault + function setRewardsDuration(uint256 _rewardsDuration) external onlyRewardVaultManager { + // protocol must not have switched to targetRewardsPerSecond logic. + if (targetRewardsPerSecond != 0) DurationChangeNotAllowed.selector.revertWith(); + // check if the reward duration is within the allowed range. + if (_rewardsDuration < MIN_REWARD_DURATION || _rewardsDuration > MAX_REWARD_DURATION) { + InvalidRewardDuration.selector.revertWith(); + } + // store the pending rewards duration. + pendingRewardsDuration = _rewardsDuration; + } + + /// @inheritdoc IRewardVault + function setTargetRewardsPerSecond(uint256 _targetRewardsPerSecond) external onlyRewardVaultManager { + // set the `minRewardDurationForTargetRate` to `MIN_REWARD_DURATION` if it is not set. + if (minRewardDurationForTargetRate == 0) { + minRewardDurationForTargetRate = MIN_REWARD_DURATION; + emit MinRewardDurationForTargetRateUpdated(MIN_REWARD_DURATION, 0); + } + // if we are setting target rate to 0, this means we are switching back to duration based distribution + // in duration based distribution, duration must be within the allowed range. + // duration will never go below `MIN_REWARD_DURATION` as `minRewardDurationForTargetRate` can never be less + // than `MIN_REWARD_DURATION`. + // so we need to check if current duration is higher than `MAX_REWARD_DURATION`, if yes, set it to + // `MAX_REWARD_DURATION`. + if (_targetRewardsPerSecond == 0 && rewardsDuration > MAX_REWARD_DURATION) { + pendingRewardsDuration = MAX_REWARD_DURATION; + } + + emit TargetRewardsPerSecondUpdated(_targetRewardsPerSecond, targetRewardsPerSecond); + targetRewardsPerSecond = _targetRewardsPerSecond; + } + + /// @inheritdoc IRewardVault + function setMinRewardDurationForTargetRate(uint256 _minRewardDurationForTargetRate) + external + onlyRewardVaultManager + { + // check if the reward duration is within the allowed range. + if ( + _minRewardDurationForTargetRate < MIN_REWARD_DURATION + || _minRewardDurationForTargetRate > MAX_REWARD_DURATION + ) { + InvalidRewardDuration.selector.revertWith(); + } + emit MinRewardDurationForTargetRateUpdated(_minRewardDurationForTargetRate, minRewardDurationForTargetRate); + minRewardDurationForTargetRate = _minRewardDurationForTargetRate; + } + + /// @inheritdoc IRewardVault + function whitelistIncentiveToken( + address token, + uint256 minIncentiveRate, + address manager + ) + external + onlyFactoryOwner + { + // validate `minIncentiveRate` value + if (minIncentiveRate == 0) MinIncentiveRateIsZero.selector.revertWith(); + if (minIncentiveRate > MAX_INCENTIVE_RATE) IncentiveRateTooHigh.selector.revertWith(); + + // validate token and manager address + if (token == address(0) || manager == address(0)) ZeroAddress.selector.revertWith(); + + Incentive storage incentive = incentives[token]; + if (whitelistedTokens.length == maxIncentiveTokensCount || incentive.minIncentiveRate != 0) { + TokenAlreadyWhitelistedOrLimitReached.selector.revertWith(); + } + whitelistedTokens.push(token); + //set the incentive rate to the minIncentiveRate. + incentive.incentiveRate = minIncentiveRate; + incentive.minIncentiveRate = minIncentiveRate; + // set the manager + incentive.manager = manager; + emit IncentiveTokenWhitelisted(token, minIncentiveRate, manager); + } + + /// @inheritdoc IRewardVault + function removeIncentiveToken(address token) external onlyFactoryVaultManager onlyWhitelistedToken(token) { + delete incentives[token]; + // delete the token from the list. + _deleteWhitelistedTokenFromList(token); + emit IncentiveTokenRemoved(token); + } + + /// @inheritdoc IRewardVault + function updateIncentiveManager( + address token, + address newManager + ) + external + onlyFactoryOwner + onlyWhitelistedToken(token) + { + if (newManager == address(0)) ZeroAddress.selector.revertWith(); + Incentive storage incentive = incentives[token]; + // cache the current manager + address currentManager = incentive.manager; + incentive.manager = newManager; + emit IncentiveManagerChanged(token, newManager, currentManager); + } + + /// @inheritdoc IRewardVault + function setMaxIncentiveTokensCount(uint8 _maxIncentiveTokensCount) external onlyFactoryOwner { + if (_maxIncentiveTokensCount < whitelistedTokens.length) { + InvalidMaxIncentiveTokensCount.selector.revertWith(); + } + maxIncentiveTokensCount = _maxIncentiveTokensCount; + emit MaxIncentiveTokensCountUpdated(_maxIncentiveTokensCount); + } + + /// @inheritdoc IRewardVault + function pause() external onlyFactoryVaultPauser { + _pause(); + } + + /// @inheritdoc IRewardVault + function unpause() external onlyFactoryVaultManager { + _unpause(); + } + + /// @inheritdoc IRewardVault + function setRewardVaultManager(address _rewardVaultManager) external onlyFactoryVaultManager { + if (_rewardVaultManager == address(0)) ZeroAddress.selector.revertWith(); + emit RewardVaultManagerSet(_rewardVaultManager, rewardVaultManager); + rewardVaultManager = _rewardVaultManager; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* GETTERS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IRewardVault + function operator(address account) external view returns (address) { + return _operators[account]; + } + + /// @inheritdoc IRewardVault + function getWhitelistedTokensCount() external view returns (uint256) { + return whitelistedTokens.length; + } + + /// @inheritdoc IRewardVault + function getWhitelistedTokens() public view returns (address[] memory) { + return whitelistedTokens; + } + + /// @inheritdoc IRewardVault + function getTotalDelegateStaked(address account) external view returns (uint256) { + return _delegateStake[account].delegateTotalStaked; + } + + /// @inheritdoc IRewardVault + function getDelegateStake(address account, address delegate) external view returns (uint256) { + return _delegateStake[account].stakedByDelegate[delegate]; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* WRITES */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IRewardVault + function stake(uint256 amount) external nonReentrant whenNotPaused { + _stake(msg.sender, amount); + } + + /// @inheritdoc IRewardVault + function delegateStake(address account, uint256 amount) external nonReentrant whenNotPaused { + if (msg.sender == account) NotDelegate.selector.revertWith(); + + _stake(account, amount); + unchecked { + DelegateStake storage info = _delegateStake[account]; + // Overflow is not possible for `delegateTotalStaked` as it is bounded by the `totalSupply` which has + // been incremented in `_stake`. + info.delegateTotalStaked += amount; + + // If the total staked by all delegates does not overflow, this increment is safe. + info.stakedByDelegate[msg.sender] += amount; + } + emit DelegateStaked(account, msg.sender, amount); + } + + /// @inheritdoc IRewardVault + function stakeOnBehalf(address account, uint256 amount) external nonReentrant whenNotPaused { + _stake(account, amount); + } + + /// @inheritdoc IRewardVault + function withdraw(uint256 amount) external nonReentrant checkSelfStakedBalance(msg.sender, amount) whenNotPaused { + _withdraw(msg.sender, amount); + } + + /// @inheritdoc IRewardVault + function delegateWithdraw(address account, uint256 amount) external nonReentrant whenNotPaused { + if (msg.sender == account) NotDelegate.selector.revertWith(); + + unchecked { + DelegateStake storage info = _delegateStake[account]; + uint256 stakedByDelegate = info.stakedByDelegate[msg.sender]; + if (stakedByDelegate < amount) InsufficientDelegateStake.selector.revertWith(); + info.stakedByDelegate[msg.sender] = stakedByDelegate - amount; + // underflow is impossible because `info.delegateTotalStaked` >= `stakedByDelegate` >= `amount` + info.delegateTotalStaked -= amount; + } + _withdraw(account, amount); + emit DelegateWithdrawn(account, msg.sender, amount); + } + + /// @inheritdoc IRewardVault + function getReward( + address account, + address recipient + ) + external + nonReentrant + whenNotPaused + onlyUserOrOperator(account) + returns (uint256) + { + return _getReward(account, recipient); + } + + /// @inheritdoc IRewardVault + function getPartialReward( + address account, + address recipient, + uint256 amount + ) + external + nonReentrant + whenNotPaused + onlyUserOrOperator(account) + { + _getPartialReward(account, recipient, amount); + } + + /// @inheritdoc IRewardVault + function exit(address recipient) external nonReentrant whenNotPaused { + // self-staked amount + uint256 amount = _accountInfo[msg.sender].balance - _delegateStake[msg.sender].delegateTotalStaked; + _withdraw(msg.sender, amount); + _getReward(msg.sender, recipient); + } + + /// @inheritdoc IRewardVault + function setOperator(address _operator) external { + _operators[msg.sender] = _operator; + emit OperatorSet(msg.sender, _operator); + } + + /// @inheritdoc IRewardVault + function addIncentive( + address token, + uint256 amount, + uint256 incentiveRate + ) + external + nonReentrant + onlyWhitelistedToken(token) + { + if (incentiveRate > MAX_INCENTIVE_RATE) IncentiveRateTooHigh.selector.revertWith(); + Incentive storage incentive = incentives[token]; + (uint256 minIncentiveRate, uint256 incentiveRateStored, uint256 amountRemainingBefore, address manager) = + (incentive.minIncentiveRate, incentive.incentiveRate, incentive.amountRemaining, incentive.manager); + + // Only allow the incentive token manager to add incentive. + if (msg.sender != manager) NotIncentiveManager.selector.revertWith(); + + // The incentive amount should be equal to or greater than the `minIncentiveRate` to avoid spamming. + // If the `minIncentiveRate` is 100 USDC/BGT, the amount should be at least 100 USDC. + if (amount < minIncentiveRate) AmountLessThanMinIncentiveRate.selector.revertWith(); + + // The incentive rate should be greater than or equal to the `minIncentiveRate`. + if (incentiveRate < minIncentiveRate) InvalidIncentiveRate.selector.revertWith(); + + // Transfer the full amount to the contract. + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + + incentive.amountRemaining = amountRemainingBefore + amount; + + // Allows updating the incentive rate if the remaining incentive amount is 0. + // Allow to decrease the incentive rate when accounted incentives are finished. + if (amountRemainingBefore == 0) { + incentive.incentiveRate = incentiveRate; + } + // Always allow to increase the incentive rate. + else if (incentiveRate >= incentiveRateStored) { + incentive.incentiveRate = incentiveRate; + } + // If the remaining incentive amount is not 0 and the new rate is less than the current rate, revert. + else { + InvalidIncentiveRate.selector.revertWith(); + } + + emit IncentiveAdded(token, msg.sender, amount, incentive.incentiveRate); + } + + /// @inheritdoc IRewardVault + function accountIncentives(address token, uint256 amount) external nonReentrant onlyWhitelistedToken(token) { + Incentive storage incentive = incentives[token]; + (uint256 minIncentiveRate, uint256 incentiveRateStored, uint256 amountRemainingBefore, address manager) = + (incentive.minIncentiveRate, incentive.incentiveRate, incentive.amountRemaining, incentive.manager); + + // Only allow the incentive token manager to account for cumulated incentives. + if (msg.sender != manager) NotIncentiveManager.selector.revertWith(); + + if (amount < minIncentiveRate) AmountLessThanMinIncentiveRate.selector.revertWith(); + + uint256 incentiveBalance = IERC20(token).balanceOf(address(this)); + if (token == address(stakeToken)) { + incentiveBalance -= totalSupply; + } + + if (amount > incentiveBalance - amountRemainingBefore) NotEnoughBalance.selector.revertWith(); + + incentive.amountRemaining += amount; + + emit IncentiveAdded(token, msg.sender, amount, incentiveRateStored); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Check if the account has enough self-staked balance. + /// @param account The account to check the self-staked balance for. + /// @param amount The amount being withdrawn. + function _checkSelfStakedBalance(address account, uint256 amount) internal view { + unchecked { + uint256 selfStaked = _accountInfo[account].balance - _delegateStake[account].delegateTotalStaked; + if (selfStaked < amount) InsufficientSelfStake.selector.revertWith(); + } + } + + /// @dev The Distributor grants this contract the allowance to transfer the BGT in its balance. + function _safeTransferRewardToken(address to, uint256 amount) internal override { + rewardToken.safeTransferFrom(distributor, to, amount); + } + + // Ensure the provided reward amount is not more than the balance in the contract. + // This keeps the reward rate in the right range, preventing overflows due to + // very high values of rewardRate in the earned and rewardsPerToken functions; + // Reward + leftover must be less than 2^256 / 10^18 to avoid overflow. + function _checkRewardSolvency() internal view override { + uint256 allowance = rewardToken.allowance(distributor, address(this)); + if (undistributedRewards / PRECISION > allowance) InsolventReward.selector.revertWith(); + } + + /// @notice process the incentives for a validator. + /// @notice If a token transfer consumes more than 500k gas units, the transfer alone will fail. + /// @param pubkey The pubkey of the validator to process the incentives for. + /// @param bgtEmitted The amount of BGT emitted by the validator. + function _processIncentives(bytes calldata pubkey, uint256 bgtEmitted) internal { + // Validator's operator corresponding to the pubkey receives the incentives. + // The pubkey -> operator relationship is maintained by the BeaconDeposit contract. + address _operator = beaconDepositContract.getOperator(pubkey); + IBeraChef beraChef = IDistributor(distributor).beraChef(); + address bgtIncentiveDistributor = getBGTIncentiveDistributor(); + + uint256 whitelistedTokensCount = whitelistedTokens.length; + unchecked { + for (uint256 i; i < whitelistedTokensCount; ++i) { + address token = whitelistedTokens[i]; + Incentive storage incentive = incentives[token]; + uint256 amount = FixedPointMathLib.mulDiv(bgtEmitted, incentive.incentiveRate, PRECISION); + uint256 amountRemaining = incentive.amountRemaining; + amount = FixedPointMathLib.min(amount, amountRemaining); + // collect the incentive fee. + (amount, amountRemaining) = _collectIncentiveFee(token, amount, amountRemaining); + + uint256 validatorShare; + if (amount > 0) { + validatorShare = beraChef.getValidatorIncentiveTokenShare(pubkey, amount); + amount -= validatorShare; + } + + if (validatorShare > 0) { + // Transfer the validator share of the incentive to its operator address. + // slither-disable-next-line arbitrary-send-erc20 + bool success = token.trySafeTransfer(_operator, validatorShare); + if (success) { + // Update the remaining amount only if tokens were transferred. + amountRemaining -= validatorShare; + emit IncentivesProcessed(pubkey, token, bgtEmitted, validatorShare); + } else { + emit IncentivesProcessFailed(pubkey, token, bgtEmitted, validatorShare); + } + } + + if (amount > 0) { + // Transfer the remaining amount of the incentive to the bgtIncentiveDistributor contract for + // distribution among BGT boosters. + // give the bgtIncentiveDistributor the allowance to transfer the incentive token. + bytes memory data = abi.encodeCall(IERC20.approve, (bgtIncentiveDistributor, amount)); + (bool success,) = token.call{ gas: SAFE_GAS_LIMIT }(data); + if (success) { + // reuse the already defined data variable to avoid stack too deep error. + data = abi.encodeCall(IBGTIncentiveDistributor.receiveIncentive, (pubkey, token, amount)); + (success,) = bgtIncentiveDistributor.call{ gas: SAFE_GAS_LIMIT }(data); + if (success) { + amountRemaining -= amount; + emit BGTBoosterIncentivesProcessed(pubkey, token, bgtEmitted, amount); + } else { + // If the transfer fails, set the allowance back to 0. + // If we don't reset the allowance, the approved tokens remain unused, and future calls to + // _processIncentives would revert for tokens like USDT that require allowance to be 0 + // before setting a new value, blocking the entire incentive distribution process. + data = abi.encodeCall(IERC20.approve, (bgtIncentiveDistributor, 0)); + (success,) = token.call{ gas: SAFE_GAS_LIMIT }(data); + emit BGTBoosterIncentivesProcessFailed(pubkey, token, bgtEmitted, amount); + } + } + // if the approve fails, log the failure in sending the incentive to the bgtIncentiveDistributor. + else { + emit BGTBoosterIncentivesProcessFailed(pubkey, token, bgtEmitted, amount); + } + } + incentive.amountRemaining = amountRemaining; + } + } + } + + function _deleteWhitelistedTokenFromList(address token) internal { + uint256 activeTokens = whitelistedTokens.length; + // The length of `whitelistedTokens` cannot be 0 because the `onlyWhitelistedToken` check has already been + // performed. + unchecked { + for (uint256 i; i < activeTokens; ++i) { + if (token == whitelistedTokens[i]) { + whitelistedTokens[i] = whitelistedTokens[activeTokens - 1]; + whitelistedTokens.pop(); + return; + } + } + } + } + + function _collectIncentiveFee( + address token, + uint256 amount, + uint256 amountRemaining + ) + internal + returns (uint256, uint256) + { + // Computes the fee amount based on the incentive fee rate, and transfers it to the collector. + IRewardVaultFactory factory = IRewardVaultFactory(factory()); + uint256 feeAmount = factory.getIncentiveFeeAmount(amount); + if (feeAmount > 0) { + amount -= feeAmount; + bool success = token.trySafeTransfer(factory.bgtIncentiveFeeCollector(), feeAmount); + if (success) { + amountRemaining -= feeAmount; + emit IncentiveFeeCollected(token, feeAmount); + } else { + emit IncentiveFeeCollectionFailed(token, feeAmount); + } + } + return (amount, amountRemaining); + } + + function _setRewardRate() internal override { + // if the pending rewards duration is 0, use the current rewards duration, + // otherwise use the pending rewards duration. + uint256 _rewardsDuration = pendingRewardsDuration == 0 ? rewardsDuration : pendingRewardsDuration; + // clear the pending rewards duration. + pendingRewardsDuration = 0; + uint256 _targetRewardsPerSecond = targetRewardsPerSecond; // cache storage read + uint256 _rewardRate = undistributedRewards / _rewardsDuration; + + if (_targetRewardsPerSecond > 0) { + // Always try to achieve the target rate by adjusting duration + uint256 targetDuration = undistributedRewards / _targetRewardsPerSecond; + + // Ensure the duration doesn't go below the minimum + if (targetDuration < minRewardDurationForTargetRate) { + // If we can't achieve the target rate within min duration, + // calculate the rate based on minimum duration + _rewardRate = undistributedRewards / minRewardDurationForTargetRate; + targetDuration = minRewardDurationForTargetRate; + } else { + // Use the target rate and update duration + _rewardRate = _targetRewardsPerSecond; + } + _rewardsDuration = targetDuration; + } + // update the rewards duration if it has changed + if (_rewardsDuration != rewardsDuration) { + _setRewardsDuration(_rewardsDuration); + } + rewardRate = _rewardRate; + periodFinish = block.timestamp + _rewardsDuration; + undistributedRewards -= _rewardRate * _rewardsDuration; + } + + function _getPartialReward(address account, address recipient, uint256 amount) internal updateReward(account) { + Info storage info = _accountInfo[account]; + uint256 reward = info.unclaimedReward; // get the rewards owed to the account + if (amount > reward) AmountGreaterThanReward.selector.revertWith(); + if (amount != 0) { + info.unclaimedReward = reward - amount; + _safeTransferRewardToken(recipient, amount); + emit RewardPaid(account, recipient, amount); + } + } +} diff --git a/src/old_versions/V9_Contracts/interfaces/IRewardVault_V9.sol b/src/old_versions/V9_Contracts/interfaces/IRewardVault_V9.sol new file mode 100644 index 0000000..c161ee0 --- /dev/null +++ b/src/old_versions/V9_Contracts/interfaces/IRewardVault_V9.sol @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import { IPOLErrors } from "src/pol/interfaces/IPOLErrors.sol"; +import { IStakingRewards } from "src/base/IStakingRewards.sol"; + +interface IRewardVault_V9 is IPOLErrors, IStakingRewards { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* EVENTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Emitted when a delegate has staked on behalf of an account. + /// @param account The account whose delegate has staked. + /// @param delegate The delegate that has staked. + /// @param amount The amount of staked tokens. + event DelegateStaked(address indexed account, address indexed delegate, uint256 amount); + + /// @notice Emitted when a delegate has withdrawn on behalf of an account. + /// @param account The account whose delegate has withdrawn. + /// @param delegate The delegate that has withdrawn. + /// @param amount The amount of withdrawn tokens. + event DelegateWithdrawn(address indexed account, address indexed delegate, uint256 amount); + + /// @notice Emitted when a token has been recovered. + /// @param token The token that has been recovered. + /// @param amount The amount of token recovered. + event Recovered(address token, uint256 amount); + + /// @notice Emitted when the msg.sender has set an operator to handle its rewards. + /// @param account The account that has set the operator. + /// @param operator The operator that has been set. + event OperatorSet(address account, address operator); + + /// @notice Emitted when the distributor is set. + /// @param distributor The address of the distributor. + event DistributorSet(address indexed distributor); + + /// @notice Emitted when the manager of an incentive token is changed. + /// @param token The address of the incentive token. + /// @param newManager The new manager of the incentive token. + /// @param oldManager The old manager of the incentive token. + event IncentiveManagerChanged(address indexed token, address newManager, address oldManager); + + /// @notice Emitted when an incentive token is whitelisted. + /// @param token The address of the token that has been whitelisted. + /// @param minIncentiveRate The minimum amount of the token to incentivize per BGT emission. + /// @param manager The address of the manager that can addIncentive for this incentive token. + event IncentiveTokenWhitelisted(address indexed token, uint256 minIncentiveRate, address manager); + + /// @notice Emitted when an incentive token is removed. + /// @param token The address of the token that has been removed. + event IncentiveTokenRemoved(address indexed token); + + /// @notice Emitted when maxIncentiveTokensCount is updated. + /// @param maxIncentiveTokensCount The max count of incentive tokens. + event MaxIncentiveTokensCountUpdated(uint8 maxIncentiveTokensCount); + + /// @notice Emitted when validator share of incentives are processed to the operator address of a validator. + event IncentivesProcessed(bytes indexed pubkey, address indexed token, uint256 bgtEmitted, uint256 amount); + + /// @notice Emitted when validator share of incentives fail to be processed to the operator address of a validator. + event IncentivesProcessFailed(bytes indexed pubkey, address indexed token, uint256 bgtEmitted, uint256 amount); + + /// @notice Emitted when incentives are added to the vault. + /// @param token The incentive token. + /// @param sender The address that added the incentive. + /// @param amount The amount of the incentive. + /// @param incentiveRate The amount of the token to incentivize per BGT emission. + event IncentiveAdded(address indexed token, address sender, uint256 amount, uint256 incentiveRate); + + /// @notice Emitted when the BGT booster share of the incentive is processed. + /// @param pubkey The pubkey of the validator. + /// @param token The address of the incentive token. + /// @param bgtEmitted The amount of BGT emitted by the validator. + /// @param amount The amount of the incentive. + event BGTBoosterIncentivesProcessed( + bytes indexed pubkey, address indexed token, uint256 bgtEmitted, uint256 amount + ); + + /// @notice Emitted when the BGT booster share of the incentive fails to be processed. + /// @param pubkey The pubkey of the validator. + /// @param token The address of the incentive token. + /// @param bgtEmitted The amount of BGT emitted by the validator. + /// @param amount The amount of the incentive. + event BGTBoosterIncentivesProcessFailed( + bytes indexed pubkey, address indexed token, uint256 bgtEmitted, uint256 amount + ); + + /// @notice Emitted when the incentive fee is sent to the collector. + /// @param token The address of the incentive token. + /// @param amount The amount of the incentive fee. + event IncentiveFeeCollected(address indexed token, uint256 amount); + + /// @notice Emitted when the incentive fee is failed to be sent to the collector. + /// @param token The address of the incentive token. + /// @param amount The amount of the incentive fee. + event IncentiveFeeCollectionFailed(address indexed token, uint256 amount); + + /// @notice Emitted when the target rewards per second is updated. + /// @param newTargetRewardsPerSecond The new target rewards per second. + /// @param oldTargetRewardsPerSecond The old target rewards per second. + event TargetRewardsPerSecondUpdated(uint256 newTargetRewardsPerSecond, uint256 oldTargetRewardsPerSecond); + + /// @notice Emitted when the reward vault manager is set. + /// @param newRewardVaultManager The address of the new reward vault manager. + /// @param oldRewardVaultManager The address of the old reward vault manager. + event RewardVaultManagerSet(address indexed newRewardVaultManager, address indexed oldRewardVaultManager); + + /// @notice Emitted when the min reward duration for target rate is updated. + /// @param newMinRewardDurationForTargetRate The new min reward duration for target rate. + /// @param oldMinRewardDurationForTargetRate The old min reward duration for target rate. + event MinRewardDurationForTargetRateUpdated( + uint256 newMinRewardDurationForTargetRate, uint256 oldMinRewardDurationForTargetRate + ); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* GETTERS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Get the address that is allowed to distribute rewards. + /// @return The address that is allowed to distribute rewards. + function distributor() external view returns (address); + + /// @notice Get the operator for an account. + /// @param account The account to get the operator for. + /// @return The operator for the account. + function operator(address account) external view returns (address); + + /// @notice Get the count of active incentive tokens. + /// @return The count of active incentive tokens. + function getWhitelistedTokensCount() external view returns (uint256); + + /// @notice Get the list of whitelisted tokens. + /// @return The list of whitelisted tokens. + function getWhitelistedTokens() external view returns (address[] memory); + + /// @notice Get the total amount staked by delegates. + /// @return The total amount staked by delegates. + function getTotalDelegateStaked(address account) external view returns (uint256); + + /// @notice Get the amount staked by a delegate on behalf of an account. + /// @return The amount staked by a delegate. + function getDelegateStake(address account, address delegate) external view returns (uint256); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ADMIN */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /** + * @notice Initialize the vault, this is only callable once and by the factory since its the deployer. + * @param _berachef The address of the berachef. + * @param _bgt The address of the BGT token. + * @param _distributor The address of the distributor. + * @param _stakingToken The address of the staking token. + */ + function initialize(address _berachef, address _bgt, address _distributor, address _stakingToken) external; + + /// @notice Allows the factory owner to set the contract that is allowed to distribute rewards. + /// @param _rewardDistribution The address that is allowed to distribute rewards. + function setDistributor(address _rewardDistribution) external; + + /// @notice Allows the distributor to notify the reward amount. + /// @param pubkey The pubkey of the validator. + /// @param reward The amount of reward to notify. + function notifyRewardAmount(bytes calldata pubkey, uint256 reward) external; + + /// @notice Allows the factory owner to recover any ERC20 token from the vault. + /// @param tokenAddress The address of the token to recover. + /// @param tokenAmount The amount of token to recover. + function recoverERC20(address tokenAddress, uint256 tokenAmount) external; + + /// @notice Allows the reward vault manager to update the duration of the rewards. + /// @dev Only allowed if targetRewardsPerSecond is not set. + /// @dev The duration must be between `MIN_REWARD_DURATION` and `MAX_REWARD_DURATION`. + /// @dev New duration is stored as pending rewards duration and is applied on the next `notifyRewardAmount` call. + /// @param _rewardsDuration The new duration of the rewards. + function setRewardsDuration(uint256 _rewardsDuration) external; + + /// @notice Sets the target rewards per second rate. + /// @dev This rate acts as both a maximum and a target. When rewards exceed this rate, the duration is + /// dynamically adjusted to achieve this target rate while respecting MIN_REWARD_DURATION constraints. + /// This prevents permanent duration expansion from reward spikes that would cause subsequent smaller + /// rewards to be distributed at very low rates. + /// @dev Min reward duration for target rate gets set to `MIN_REWARD_DURATION` if it is not set. + /// @dev Allows resetting the target rewards per second to 0 to enable duration based distribution. + /// @param _targetRewardsPerSecond The new target rewards per second, scaled by PRECISION. + function setTargetRewardsPerSecond(uint256 _targetRewardsPerSecond) external; + + /// @notice Allows the reward vault manager to set the min reward duration for target rate. + /// @dev This duration is used in case target rewards per second is not met. + /// @dev The duration must be between `MIN_REWARD_DURATION` and `MAX_REWARD_DURATION`. + /// @param _minRewardDurationForTargetRate The new min reward duration for target rate. + function setMinRewardDurationForTargetRate(uint256 _minRewardDurationForTargetRate) external; + + /// @notice Allows the factory owner to whitelist a token to incentivize with. + /// @param token The address of the token to whitelist. + /// @param minIncentiveRate The minimum amount of the token to incentivize per BGT emission. + /// @param manager The address of the manager that can addIncentive for this token. + function whitelistIncentiveToken(address token, uint256 minIncentiveRate, address manager) external; + + /// @notice Allows the factory vault manager to remove a whitelisted incentive token. + /// @param token The address of the token to remove. + function removeIncentiveToken(address token) external; + + /// @notice Allows the factory owner to update the maxIncentiveTokensCount. + /// @param _maxIncentiveTokensCount The new maxIncentiveTokens count. + function setMaxIncentiveTokensCount(uint8 _maxIncentiveTokensCount) external; + + /// @notice Allows the factory vault pauser to pause the vault. + function pause() external; + + /// @notice Allows the factory vault manager to unpause the vault. + function unpause() external; + + /// @notice Allows the factory vault manager to set the address responsible for managing the reward vault. + /// @param _rewardVaultManager The address of the reward vault manager. + function setRewardVaultManager(address _rewardVaultManager) external; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* MUTATIVE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Exit the vault with the staked tokens and claim the reward. + /// @dev Only the account holder can call this function, not the operator. + /// @dev Clears out the user self-staked balance and rewards. + /// @param recipient The address to send the 'BGT' reward to. + function exit(address recipient) external; + + /// @notice Claim the reward. + /// @dev The operator only handles BGT, not STAKING_TOKEN. + /// @dev Callable by the operator or the account holder. + /// @param account The account to get the reward for. + /// @param recipient The address to send the reward to. + /// @return The amount of the reward claimed. + function getReward(address account, address recipient) external returns (uint256); + + /// @notice Claim a partial reward. + /// @notice Use `getReward` if you want to claim the full reward. + /// @dev The operator only handles BGT, not STAKING_TOKEN. + /// @dev Callable by the operator or the account holder. + /// @param account The account to get the reward for. + /// @param recipient The address to send the reward to. + /// @param amount The amount of the reward to claim. + function getPartialReward(address account, address recipient, uint256 amount) external; + + /// @notice Stake tokens in the vault. + /// @param amount The amount of tokens to stake. + function stake(uint256 amount) external; + + /// @notice Stake tokens on behalf of another account. + /// @param account The account to stake for. + /// @param amount The amount of tokens to stake. + function delegateStake(address account, uint256 amount) external; + + /// @notice Stake tokens on behalf of another account. + /// @param account The account to stake for. + /// @param amount The amount of tokens to stake. + function stakeOnBehalf(address account, uint256 amount) external; + + /// @notice Withdraw the staked tokens from the vault. + /// @param amount The amount of tokens to withdraw. + function withdraw(uint256 amount) external; + + /// @notice Withdraw tokens staked on behalf of another account by the delegate (msg.sender). + /// @param account The account to withdraw for. + /// @param amount The amount of tokens to withdraw. + function delegateWithdraw(address account, uint256 amount) external; + + /// @notice Allows msg.sender to set another address to claim and manage their rewards. + /// @param _operator The address that will be allowed to claim and manage rewards. + function setOperator(address _operator) external; + + /// @notice Update the manager of an incentive token. + /// @dev Permissioned function, only allow factory owner to update the manager. + /// @param token The address of the incentive token. + /// @param newManager The new manager of the incentive token. + function updateIncentiveManager(address token, address newManager) external; + + /// @notice Add an incentive token to the vault. + /// @notice The incentive token's transfer should not exceed a gas usage of 500k units. + /// In case the transfer exceeds 500k gas units, your incentive will fail to be transferred to the validator and + /// its delegates. + /// @param token The address of the token to add as an incentive. + /// @param amount The amount of the token to add as an incentive. + /// @param incentiveRate The amount of the token to incentivize per BGT emission. + /// @dev Permissioned function, only callable by incentive token manager. + function addIncentive(address token, uint256 amount, uint256 incentiveRate) external; + + /// @notice Process incentives added via IERC20.transfer, adding them to the incentive accounting. + /// @notice Allows permissionless incentive addition, without dossing the manager possibility to + /// let the incentive accounting go to 0 in order to be able to decrease the incentive rate. + /// @param token The address of the token to process. + /// @param amount The amount of token to account as incentive. + /// @dev Permissioned function, only callable by incentive token manager. + function accountIncentives(address token, uint256 amount) external; +} diff --git a/src/pol/BGTStaker.sol b/src/pol/BGTStaker.sol index 84e0360..724ee4e 100644 --- a/src/pol/BGTStaker.sol +++ b/src/pol/BGTStaker.sol @@ -84,7 +84,7 @@ contract BGTStaker is IBGTStaker, OwnableUpgradeable, UUPSUpgradeable, StakingRe /// @inheritdoc IBGTStaker function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyOwner { - if (tokenAddress == address(rewardToken)) CannotRecoverRewardToken.selector.revertWith(); + if (tokenAddress == address(_rewardToken)) CannotRecoverRewardToken.selector.revertWith(); IERC20(tokenAddress).safeTransfer(owner(), tokenAmount); emit Recovered(tokenAddress, tokenAmount); } diff --git a/src/pol/IncentivesCollector.sol b/src/pol/IncentivesCollector.sol new file mode 100644 index 0000000..0775868 --- /dev/null +++ b/src/pol/IncentivesCollector.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +import { Utils } from "../libraries/Utils.sol"; +import { IWBERAStakerVault } from "./interfaces/IWBERAStakerVault.sol"; +import { IIncentivesCollector } from "./interfaces/IIncentivesCollector.sol"; +import { IStakerVault } from "./interfaces/lst/IStakerVault.sol"; +import { ILSTAdapter } from "./interfaces/lst/ILSTAdapter.sol"; + +/// @title IncentivesCollector +/// @author Berachain Team +/// @notice Collects the incentives posted on reward vaults and auction them for WBERA. +/// Accrued WBERA serves as a payout for the stakers of `WBERAStakerVault.sol` and added `LSTStakerVault.sol`s. +/// @dev This contract is inspired by the `FeeCollector.sol` for auctioning collected tokens. +contract IncentivesCollector is IIncentivesCollector, PausableUpgradeable, AccessControlUpgradeable, UUPSUpgradeable { + using SafeERC20 for IERC20; + using Utils for bytes4; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice The PAUSER role + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + + /// @notice The MANAGER role. + bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); + + /// @notice The WBERA token address, serves as payout token. + address public constant WBERA = 0x6969696969696969696969696969696969696969; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STORAGE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IIncentivesCollector + uint256 public queuedPayoutAmount; + + /// @inheritdoc IIncentivesCollector + uint256 public payoutAmount; + + /// @inheritdoc IIncentivesCollector + address public wberaStakerVault; + + /// @inheritdoc IIncentivesCollector + address[] public lstStakerVaults; + + /// @inheritdoc IIncentivesCollector + mapping(address lstVault => address lstAdapter) public lstAdapters; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Initialize the contract. + /// @param governance The address of the governance contract. + /// @param _payoutAmount The payout amount. + function initialize(address governance, uint256 _payoutAmount, address _wberaStakerVault) public initializer { + __Pausable_init(); + __AccessControl_init(); + __UUPSUpgradeable_init(); + if (governance == address(0)) ZeroAddress.selector.revertWith(); + if (_payoutAmount == 0) PayoutAmountIsZero.selector.revertWith(); + if (_wberaStakerVault == address(0)) ZeroAddress.selector.revertWith(); + + _grantRole(DEFAULT_ADMIN_ROLE, governance); + // Allow the MANAGER to control the PAUSER_ROLE + _setRoleAdmin(PAUSER_ROLE, MANAGER_ROLE); + + payoutAmount = _payoutAmount; + emit PayoutAmountSet(0, _payoutAmount); + + wberaStakerVault = _wberaStakerVault; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ADMIN FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) { } + + /// @inheritdoc IIncentivesCollector + function queuePayoutAmountChange(uint256 _newPayoutAmount) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_newPayoutAmount == 0) PayoutAmountIsZero.selector.revertWith(); + emit QueuedPayoutAmount(_newPayoutAmount, payoutAmount); + queuedPayoutAmount = _newPayoutAmount; + } + + /// @inheritdoc IIncentivesCollector + function addLstStakerVault(address lstStakerVault, address lstAdapter) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (lstStakerVault == address(0)) ZeroAddress.selector.revertWith(); + if (lstAdapter == address(0)) ZeroAddress.selector.revertWith(); + if (lstAdapters[lstStakerVault] != address(0)) LSTStakerVaultAlreadyAdded.selector.revertWith(); + + if (IERC20Metadata(IERC4626(lstStakerVault).asset()).decimals() != 18) { + InvalidToken.selector.revertWith(IERC4626(lstStakerVault).asset()); + } + + emit LstStakerVaultAdded(lstStakerVault, lstAdapter); + lstStakerVaults.push(lstStakerVault); + lstAdapters[lstStakerVault] = lstAdapter; + } + + /// @inheritdoc IIncentivesCollector + function removeLstStakerVault(address lstStakerVault) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (lstAdapters[lstStakerVault] == address(0)) LSTStakerVaultNotFound.selector.revertWith(); + emit LstStakerVaultRemoved(lstStakerVault); + + for (uint256 i = 0; i < lstStakerVaults.length; i++) { + if (lstStakerVaults[i] == lstStakerVault) { + uint256 lastIndex = lstStakerVaults.length - 1; + + // Swap and pop + if (i != lastIndex) { + lstStakerVaults[i] = lstStakerVaults[lastIndex]; + } + lstStakerVaults.pop(); + + delete lstAdapters[lstStakerVault]; + return; + } + } + } + + /// @inheritdoc IIncentivesCollector + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + /// @inheritdoc IIncentivesCollector + function unpause() external onlyRole(MANAGER_ROLE) { + _unpause(); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* TOKENS AUCTION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IIncentivesCollector + function claimFees(address _recipient, address[] calldata _feeTokens) external whenNotPaused { + _claim(_recipient, _feeTokens); + } + + /// @inheritdoc IIncentivesCollector + function claim(address _recipient, address[] calldata _incentiveTokens) external whenNotPaused { + _claim(_recipient, _incentiveTokens); + } + + /// @inheritdoc IIncentivesCollector + function lstStakerVaultsLength() external view returns (uint256) { + return lstStakerVaults.length; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function _claim(address _recipient, address[] calldata _incentiveTokens) internal { + // Transfer the payout amount of the payout token to this contract from msg.sender. + IERC20(WBERA).safeTransferFrom(msg.sender, address(this), payoutAmount); + uint256[] memory amounts = _splitAmount(payoutAmount); + + // Send rewards to vaults + + // WBERA + { + // approve the WBERAStakerVault contract to spend its share of the payout amount + IERC20(WBERA).forceApprove(wberaStakerVault, amounts[0]); + // send the payout amount to the WBERAStakerVault contract + IWBERAStakerVault(wberaStakerVault).receiveRewards(amounts[0]); + } + + // LSTs + for (uint256 i = 1; i < amounts.length;) { + IStakerVault vault = IStakerVault(lstStakerVaults[i - 1]); + if (amounts[i] > 0) { + ILSTAdapter lstAdapter = ILSTAdapter(lstAdapters[address(vault)]); + IERC20(WBERA).forceApprove(address(lstAdapter), amounts[i]); + uint256 lstAmount = lstAdapter.stake(amounts[i]); + emit RewardConverted(address(vault), amounts[i], lstAmount); + IERC20(IERC4626(address(vault)).asset()).forceApprove(address(vault), lstAmount); + vault.receiveRewards(lstAmount); + } + unchecked { + ++i; + } + } + + // From all the specified incentive tokens, transfer them to the recipient. + for (uint256 i; i < _incentiveTokens.length;) { + address incentiveToken = _incentiveTokens[i]; + uint256 incentiveTokenAmountToTransfer = IERC20(incentiveToken).balanceOf(address(this)); + IERC20(incentiveToken).safeTransfer(_recipient, incentiveTokenAmountToTransfer); + emit IncentiveTokenClaimed(msg.sender, _recipient, incentiveToken, incentiveTokenAmountToTransfer); + unchecked { + ++i; + } + } + + emit IncentivesClaimed(msg.sender, _recipient); + if (queuedPayoutAmount != 0) _setPayoutAmount(); + } + + /// @notice Set the payout amount to the queued payout amount + function _setPayoutAmount() internal { + emit PayoutAmountSet(payoutAmount, queuedPayoutAmount); + payoutAmount = queuedPayoutAmount; + queuedPayoutAmount = 0; + } + + /// @dev Split the given `amount` between the WBERA staker vault (index 0) and all + /// LST staker vaults (indices 1..len) proportionally to their stake in WBERA terms. + /// @return amounts Array of length (lstStakerVaults.length + 1) + function _splitAmount(uint256 amount) internal view returns (uint256[] memory amounts) { + uint256 len = lstStakerVaults.length; + + amounts = new uint256[](len + 1); + uint256[] memory stakes = new uint256[](len + 1); + + // WBERA net stake + stakes[0] = IERC4626(wberaStakerVault).totalAssets(); + uint256 totalStake = stakes[0]; + + // Iterate over vaults to fill stakes[1..len+1] + for (uint256 i = 1; i < len + 1;) { + IERC4626 vault = IERC4626(lstStakerVaults[i - 1]); + uint256 stake = vault.totalAssets(); + + if (stake > 0) { + uint256 rate = ILSTAdapter(lstAdapters[address(vault)]).getRate(); + uint256 value = (stake * rate) / 1e18; + stakes[i] = value; + totalStake += value; + } + + unchecked { + ++i; + } + } + + // 0 edge case: no stakes or no amount to split + // In this case, claimer will be able to claim all the tokens without paying the payout amount of WBERA. + // We are aware of this issue and does not consider it a problem given totalStake being 0 is not practically, + // possible situation as all the LST vaults are deployed with initial supply. + if (totalStake == 0 || amount == 0) { + return amounts; + } + + // Make the splits + uint256 distributed; + for (uint256 i; i < len + 1;) { + uint256 part = (amount * stakes[i]) / totalStake; + amounts[i] = part; + distributed += part; + unchecked { + ++i; + } + } + + // Assign dust to WBERA bucket + if (distributed < amount) { + unchecked { + amounts[0] += amount - distributed; + } + } + + return amounts; + } +} diff --git a/src/pol/BGTIncentiveFeeDeployer.sol b/src/pol/IncentivesCollectorDeployer.sol similarity index 65% rename from src/pol/BGTIncentiveFeeDeployer.sol rename to src/pol/IncentivesCollectorDeployer.sol index 2fd23b4..d8a1683 100644 --- a/src/pol/BGTIncentiveFeeDeployer.sol +++ b/src/pol/IncentivesCollectorDeployer.sol @@ -5,38 +5,38 @@ import { Create2Deployer } from "../base/Create2Deployer.sol"; import { Salt } from "../base/Salt.sol"; import { WBERAStakerVault } from "./WBERAStakerVault.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { BGTIncentiveFeeCollector } from "./BGTIncentiveFeeCollector.sol"; +import { IncentivesCollector } from "./IncentivesCollector.sol"; -/// @title BGTIncentiveFeeDeployer +/// @title IncentivesCollectorDeployer /// @author Berachain Team -/// @notice This contract is used to deploy the BGTIncentiveFeeCollector and WBERAStakerVault contracts. +/// @notice This contract is used to deploy the IncentivesCollector and WBERAStakerVault contracts. /// @dev Caller must have BERA balance of `INITIAL_DEPOSIT_AMOUNT` which is used to deposit in the vault to avoid /// inflation attack. -contract BGTIncentiveFeeDeployer is Create2Deployer { +contract IncentivesCollectorDeployer is Create2Deployer { /// @notice The initial deposit amount to the WBERAStakerVault to avoid inflation attack. uint256 public constant INITIAL_DEPOSIT_AMOUNT = 10e18; /// @notice The WBERAStakerVault contract. WBERAStakerVault public immutable wberaStakerVault; - /// @notice The BGTIncentiveFeeCollector contract. - BGTIncentiveFeeCollector public immutable bgtIncentiveFeeCollector; + /// @notice The IncentivesCollector contract. + IncentivesCollector public immutable incentivesCollector; /// @notice The WBERA token address, serves as underlying asset. IERC20 public constant WBERA = IERC20(0x6969696969696969696969696969696969696969); - /// @notice Constructor for the BGTIncentiveFeeDeployer. + /// @notice Constructor for the IncentivesCollectorDeployer. /// @param governance The address of the governance contract. /// @param tokenProvider The address of the token provider for initial deposit. - /// @param payoutAmount The amount of payout for the BGTIncentiveFeeCollector. + /// @param payoutAmount The amount of payout for the IncentivesCollector. /// @param wberaStakerVaultSalt The salt for the WBERAStakerVault. - /// @param bgtIncentiveFeeCollectorSalt The salt for the BGTIncentiveFeeCollector. + /// @param incentivesCollectorSalt The salt for the IncentivesCollector. constructor( address governance, address tokenProvider, uint256 payoutAmount, Salt memory wberaStakerVaultSalt, - Salt memory bgtIncentiveFeeCollectorSalt + Salt memory incentivesCollectorSalt ) { // deploy the WBERAStakerVault implementation address wberaStakerVaultImpl = @@ -45,18 +45,16 @@ contract BGTIncentiveFeeDeployer is Create2Deployer { wberaStakerVault = WBERAStakerVault(payable(deployProxyWithCreate2(wberaStakerVaultImpl, wberaStakerVaultSalt.proxy))); - // deploy the BGTIncentiveFeeCollector implementation - address bgtIncentiveFeeCollectorImpl = deployWithCreate2( - bgtIncentiveFeeCollectorSalt.implementation, type(BGTIncentiveFeeCollector).creationCode - ); - // deploy the BGTIncentiveFeeCollector proxy - bgtIncentiveFeeCollector = BGTIncentiveFeeCollector( - deployProxyWithCreate2(bgtIncentiveFeeCollectorImpl, bgtIncentiveFeeCollectorSalt.proxy) - ); + // deploy the IncentivesCollector implementation + address incentivesCollectorImpl = + deployWithCreate2(incentivesCollectorSalt.implementation, type(IncentivesCollector).creationCode); + // deploy the IncentivesCollector proxy + incentivesCollector = + IncentivesCollector(deployProxyWithCreate2(incentivesCollectorImpl, incentivesCollectorSalt.proxy)); // initialize the contracts wberaStakerVault.initialize(governance); - bgtIncentiveFeeCollector.initialize(governance, payoutAmount, address(wberaStakerVault)); + incentivesCollector.initialize(governance, payoutAmount, address(wberaStakerVault)); // deposit `INITIAL_DEPOSIT_AMOUNT` to the vault to avoid inflation attack // first get tokens from the token provider diff --git a/src/pol/POLDeployer.sol b/src/pol/POLDeployer.sol index 8d6b91f..67d4de0 100644 --- a/src/pol/POLDeployer.sol +++ b/src/pol/POLDeployer.sol @@ -61,8 +61,9 @@ contract POLDeployer is Create2Deployer { address blockRewardControllerImpl = deployWithCreate2(blockRewardControllerSalt.implementation, type(BlockRewardController).creationCode); // deploy the BlockRewardController proxy - blockRewardController = - BlockRewardController(deployProxyWithCreate2(blockRewardControllerImpl, blockRewardControllerSalt.proxy)); + blockRewardController = BlockRewardController( + payable(deployProxyWithCreate2(blockRewardControllerImpl, blockRewardControllerSalt.proxy)) + ); // deploy the Distributor implementation address distributorImpl = deployWithCreate2(distributorSalt.implementation, type(Distributor).creationCode); diff --git a/src/pol/RewardVaultHelperDeployer.sol b/src/pol/RewardVaultHelperDeployer.sol index 2e9338e..77e814c 100644 --- a/src/pol/RewardVaultHelperDeployer.sol +++ b/src/pol/RewardVaultHelperDeployer.sol @@ -21,7 +21,7 @@ contract RewardVaultHelperDeployer is Create2Deployer { deployWithCreate2(rewardVaultHelperSalt.implementation, type(RewardVaultHelper).creationCode); // deploy the RewardVaultHelper proxy rewardVaultHelper = - RewardVaultHelper(deployProxyWithCreate2(rewardVaultHelperImpl, rewardVaultHelperSalt.proxy)); + RewardVaultHelper(payable(deployProxyWithCreate2(rewardVaultHelperImpl, rewardVaultHelperSalt.proxy))); // initialize the contract rewardVaultHelper.initialize(owner); } diff --git a/src/pol/interfaces/IBGTIncentiveDistributor.sol b/src/pol/interfaces/IBGTIncentiveDistributor.sol index dfbc955..d894fd6 100644 --- a/src/pol/interfaces/IBGTIncentiveDistributor.sol +++ b/src/pol/interfaces/IBGTIncentiveDistributor.sol @@ -57,6 +57,12 @@ interface IBGTIncentiveDistributor is IPOLErrors { */ event RewardClaimDelaySet(uint64 delay); + /** + * @notice Event emitted when the last aggregation timestamp is set + * @param timestamp The timestamp of the last aggregation + */ + event LastAggregationTimestampSet(uint64 timestamp); + /** * @notice Event emitted when a reward is claimed * @param identifier The identifier of the reward @@ -94,6 +100,14 @@ interface IBGTIncentiveDistributor is IPOLErrors { */ event IncentiveReceived(bytes indexed pubkey, address indexed token, uint256 amount); + /** + * @notice Event emitted when incentive tokens are allocated to a validator + * @param pubkey The pubkey of the validator + * @param token The address of the incentive token + * @param amount The amount of tokens allocated + */ + event IncentiveAllocated(bytes indexed pubkey, address indexed token, uint256 amount); + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /* FUNCTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -113,6 +127,13 @@ interface IBGTIncentiveDistributor is IPOLErrors { */ function setRewardClaimDelay(uint64 _delay) external; + /** + * @notice Set the last aggregation timestamp + * @dev Only address with DEFAULT_ADMIN_ROLE can call this function + * @param timestamp_ The timestamp of the last aggregation + */ + function setLastAggregationTimestamp(uint64 timestamp_) external; + /** * @notice Receive incentive tokens from POL reward vaults * @dev Token approval must be given by the caller to this function before calling it. @@ -122,8 +143,19 @@ interface IBGTIncentiveDistributor is IPOLErrors { */ function receiveIncentive(bytes calldata pubkey, address token, uint256 _amount) external; + /** + * @notice Allocate incentive tokens to a validator + * @dev Only address with MANAGER_ROLE can call this function + * @dev This function is used during claim automation to allocate incentive tokens to a validator and collapsed + * claims for the same token. + * @param token The address of the incentive token + * @param amount The amount of tokens to allocate + */ + function allocateIncentiveToValidator(address token, uint256 amount) external; + /** * @notice Claim rewards based on the specified metadata + * @dev If the contract is paused, only address with MANAGER_ROLE can call this function * @param _claims Claim[] List of claim metadata */ function claim(Claim[] calldata _claims) external; diff --git a/src/pol/interfaces/IBlockRewardController.sol b/src/pol/interfaces/IBlockRewardController.sol index 38a03a2..7e70d4c 100644 --- a/src/pol/interfaces/IBlockRewardController.sol +++ b/src/pol/interfaces/IBlockRewardController.sol @@ -22,134 +22,63 @@ interface IBlockRewardController is IPOLErrors { */ event RewardRateChanged(uint256 oldRewardRate, uint256 newRewardRate); - /** - * @notice Emitted when the min boosted reward rate has changed. - * @param oldMinBoostedRewardRate The old min boosted reward rate. - * @param newMinBoostedRewardRate The new min boosted reward rate. - */ - event MinBoostedRewardRateChanged(uint256 oldMinBoostedRewardRate, uint256 newMinBoostedRewardRate); - - /** - * @notice Emitted when the boostMultiplier parameter has changed. - * @param oldBoostMultiplier The old boost multiplier parameter. - * @param newBoostMultiplier The new boost multiplier parameter. - */ - event BoostMultiplierChanged(uint256 oldBoostMultiplier, uint256 newBoostMultiplier); - - /** - * @notice Emitted when the reward formula convexity parameter has changed. - * @param oldRewardConvexity The old reward formula convexity parameter. - * @param newRewardConvexity The new reward formula convexity parameter. - */ - event RewardConvexityChanged(uint256 oldRewardConvexity, uint256 newRewardConvexity); - /// @notice Emitted when the distributor is set. event SetDistributor(address indexed rewardDistribution); /// @notice Emitted when the rewards for the specified block have been processed. /// @param pubkey The validator's pubkey. /// @param nextTimestamp The timestamp of the next beacon block that was processed. - /// @param baseRate The base amount of BGT minted to the validator's operator. - /// @param rewardRate The amount of BGT minted to the distributor. + /// @param baseRate The base amount of WBERA sent to the validator's operator. + /// @param rewardRate The amount of WBERA sent to the distributor. event BlockRewardProcessed(bytes indexed pubkey, uint64 nextTimestamp, uint256 baseRate, uint256 rewardRate); - /// @notice Returns the constant base rate for BGT. - /// @return The constant base amount of BGT to be minted in the current block. + /// @notice Emitted when the exceeding native balance is burnt. + /// @param amount The amount of native tokens sent to address(0). + event ExceedingBalanceBurnt(uint256 amount); + + /// @notice Returns the constant base rate for the emission token. + /// @return The constant base amount of emission token to be emitted in the current block. function baseRate() external view returns (uint256); - /// @notice Returns the reward rate for BGT. - /// @return The unscaled amount of BGT to be minted in the current block. + /// @notice Returns the reward rate for the emission token. + /// @return The unscaled amount of emission token to be emitted in the current block. function rewardRate() external view returns (uint256); - /// @notice Returns the minimum boosted reward rate for BGT. - /// @return The minimum amount of BGT to be minted in the current block. - function minBoostedRewardRate() external view returns (uint256); - - /// @notice Returns the boost mutliplier param in the reward function. - /// @return The parameter that determines the inflation cap. - function boostMultiplier() external view returns (uint256); - - /// @notice Returns the reward convexity param in the reward function. - /// @return The parameter that determines how fast the function converges to its max. - function rewardConvexity() external view returns (int256); - - /** - * @notice Computes the reward given specified parameters, according to the formula. - * r := (1 + mul) * (1 - 1 / (1 + mul * boost^conv)) * rewardRate ∈ [0, mul * rewardRate] - * @dev Returns 0 for boost == 0 even if conv == 0, since contract enforces conv > 0. - * @param boostPower the normalized boost. - * @param _rewardRate the reward rate parameter. - * @param _boostMultiplier the boost multiplier parameter. - * @param _rewardConvexity the reward convexity parameter. - * @return the reward amount. - */ - function computeReward( - uint256 boostPower, - uint256 _rewardRate, - uint256 _boostMultiplier, - int256 _rewardConvexity - ) - external - pure - returns (uint256); - /** * @notice Returns the current max BGT production per block. - * @dev Exposed for BGT contract to calculate the max burnable native token amount. + * @dev Deprecated. Exposed for BGT contract to calculate the max burnable native token amount. * @return amount The maximum amount of BGT that can be minted in one block. */ function getMaxBGTPerBlock() external view returns (uint256 amount); /** - * @notice Processes the rewards for the specified block and mints BGT to validator's operator and distributor. + * @notice Returns the current max emission per block. + * @return amount The maximum amount of rewards that can be emitted in one block. + */ + function getMaxEmissionPerBlock() external view returns (uint256 amount); + + /** + * @notice Processes the rewards for the specified block and distributes emission tokens to the validator's + * operator and distributor. * @dev This function can only be called by the distributor. - * @dev If in genesis only base rate for validators is minted. + * @dev If in genesis only base rate for validators is sent. * @param pubkey The validator's pubkey. * @param nextTimestamp The timestamp of the next beacon block that was processed. - * @param isReady The flag to enable reward minting to distributor (true when BeraChef is ready). - * @return the amount of BGT minted to distributor. + * @param isReady The flag to enable reward sending to distributor (true when BeraChef is ready). + * @return the amount of emission tokens sent to distributor. */ function processRewards(bytes calldata pubkey, uint64 nextTimestamp, bool isReady) external returns (uint256); /** - * @notice Sets the constant base reward rate for BGT. - * @dev This function can only be called by the owner, which is the governance address. - * @param _baseRate The new base rate. - */ - function setBaseRate(uint256 _baseRate) external; - - /** - * @notice Sets the reward rate for BGT. - * @dev This function can only be called by the owner, which is the governance address. - * @param _rewardRate The new reward rate. - */ - function setRewardRate(uint256 _rewardRate) external; - - /** - * @notice Sets the min boosted reward rate for BGT. - * @dev This function can only be called by the owner, which is the governance address. - * @param _minBoostedRewardRate The new min boosted reward rate. - */ - function setMinBoostedRewardRate(uint256 _minBoostedRewardRate) external; - - /** - * @notice Sets the boost multiplier parameter for the reward formula. - * @dev This function can only be called by the owner, which is the governance address. - * @param _boostMultiplier The new boost multiplier. - */ - function setBoostMultiplier(uint256 _boostMultiplier) external; - - /** - * @notice Sets the reward convexity parameter for the reward formula. + * @notice Sets the distributor contract that receives the minted WBERA. * @dev This function can only be called by the owner, which is the governance address. - * @param _rewardConvexity The new reward convexity. + * @param _distributor The new distributor contract. */ - function setRewardConvexity(uint256 _rewardConvexity) external; + function setDistributor(address _distributor) external; /** - * @notice Sets the distributor contract that receives the minted BGT. - * @dev This function can only be called by the owner, which is the governance address. - * @param _distributor The new distributor contract. + * @notice Burns all exceeding native balance by sending it to address(0). + * @dev Can only be called once per block by the distributor. */ - function setDistributor(address _distributor) external; + function burnExceedingBalance() external; } diff --git a/src/pol/interfaces/IDistributor.sol b/src/pol/interfaces/IDistributor.sol index de71a1e..b3d70bc 100644 --- a/src/pol/interfaces/IDistributor.sol +++ b/src/pol/interfaces/IDistributor.sol @@ -14,6 +14,7 @@ interface IDistributor is IPOLErrors { event DedicatedEmissionStreamManagerSet( address indexed oldDedicatedEmissionStreamManager, address indexed newDedicatedEmissionStreamManager ); + event EmissionTokenSet(address indexed emissionToken); /** * @notice Distribute the rewards to the reward allocation receivers. @@ -45,9 +46,15 @@ interface IDistributor is IPOLErrors { /// @notice Returns the address of the BeraChef contract. function beraChef() external view returns (IBeraChef); + /// @notice Returns the address of the emission token contract. + function emissionToken() external view returns (address); + /// @notice Returns the address of the dedicated emission stream manager contract. function dedicatedEmissionStreamManager() external view returns (IDedicatedEmissionStreamManager); /// @notice Sets the address of the dedicated emission stream manager contract. function setDedicatedEmissionStreamManager(address _dedicatedEmissionStreamManager) external; + + /// @notice Sets the address of the emission token contract. + function setEmissionToken() external; } diff --git a/src/pol/interfaces/IIncentivesCollector.sol b/src/pol/interfaces/IIncentivesCollector.sol new file mode 100644 index 0000000..301a9f8 --- /dev/null +++ b/src/pol/interfaces/IIncentivesCollector.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import { IPOLErrors } from "./IPOLErrors.sol"; + +interface IIncentivesCollector is IPOLErrors { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* EVENTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + /** + * @notice Emitted when the admin queues the payout amount. + * @param queuedPayoutAmount The queued payout amount. + * @param currentPayoutAmount The current payout amount. + */ + event QueuedPayoutAmount(uint256 queuedPayoutAmount, uint256 currentPayoutAmount); + + /** + * @notice Emitted when the payout amount is updated. + * @param oldPayoutAmount The old payout amount. + * @param newPayoutAmount The new payout amount. + */ + event PayoutAmountSet(uint256 indexed oldPayoutAmount, uint256 indexed newPayoutAmount); + + /** + * @notice Emitted when the incentives are claimed. + * @param caller Caller of the `claim` function. + * @param recipient The address to which collected incentive tokens will be transferred. + */ + event IncentivesClaimed(address indexed caller, address indexed recipient); + + /** + * @notice Emitted when a incentive token is claimed. + * @param caller Caller of the `claim` function. + * @param recipient The address to which collected incentive tokens will be transferred. + * @param incentiveToken The address of the incentive token to collect. + * @param amount The amount of incentive token transferred. + */ + event IncentiveTokenClaimed( + address indexed caller, address indexed recipient, address indexed incentiveToken, uint256 amount + ); + + /** + * @notice Emitted when a new LST staker vault is added. + * @param lstStakerVault The address of the LST staker vault. + * @param lstAdapter The address of the LST adapter. + */ + event LstStakerVaultAdded(address indexed lstStakerVault, address indexed lstAdapter); + + /** + * @notice Emitted when a LST staker vault is removed. + * @param lstStakerVault The address of the LST staker vault. + */ + event LstStakerVaultRemoved(address indexed lstStakerVault); + + /** + * @notice Emitted when a WBERA denominated reward is converted to LST. + * @param lstStakerVault The address of the LST staker vault. + * @param amountBera The amount of BERA being swapped. + * @param amountLST The amount of LST received. + */ + event RewardConverted(address indexed lstStakerVault, uint256 amountBera, uint256 amountLST); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* Getters */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /** + * @notice Get the queued payout amount. + * @return The queued payout amount. + */ + function queuedPayoutAmount() external view returns (uint256); + + /** + * @notice Get the current payout amount. + * @return The current payout amount. + */ + function payoutAmount() external view returns (uint256); + + /** + * @notice Get the WBERA staker vault address. + * @return The WBERA staker vault address. + */ + function wberaStakerVault() external view returns (address); + + /** + * @notice Get the size of the LST staker vault address array. + * @return The length of the LST staker vault address array. + */ + function lstStakerVaultsLength() external view returns (uint256); + + /** + * @notice Getter for the LST staker vault address array. + * @param index The index of the LST staker vault. + * @return The LST staker vault address at the given index. + */ + function lstStakerVaults(uint256 index) external view returns (address); + + /** + * @notice Getter for the LST adapter address for a given LST staker vault. + * @param lstStakerVault The address of the LST staker vault. + * @return The LST adapter address for the given LST staker vault. + */ + function lstAdapters(address lstStakerVault) external view returns (address); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* PERMISSIONED FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /** + * @notice Queue a payout amount change. + * @dev Can only be called by `DEFAULT_ADMIN_ROLE`. + * @param _newPayoutAmount The new payout amount. + */ + function queuePayoutAmountChange(uint256 _newPayoutAmount) external; + + /** + * @notice Add a new LST staker vault to be considered for incentive tokens distribution. + * @dev Can only be called by `DEFAULT_ADMIN_ROLE`. + * @dev Only supports 18 decimals tokens. + * @param lstStakerVault The address of the LST staker vault. + * @param lstAdapter The address of the LST adapter. + */ + function addLstStakerVault(address lstStakerVault, address lstAdapter) external; + + /** + * @notice Remove an existing LST staker vault from the incentive tokens distribution list. + * @dev Can only be called by `DEFAULT_ADMIN_ROLE`. + * @param lstStakerVault The address of the LST staker vault. + */ + function removeLstStakerVault(address lstStakerVault) external; + + /** + * @notice Pause the contract. + * @dev Can only be called by `PAUSER_ROLE`. + */ + function pause() external; + + /** + * @notice Unpause the contract. + * @dev Can only be called by `MANAGER_ROLE`. + */ + function unpause() external; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STATE MUTATING FUNCTION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /** + * @notice Deprecated. Use `claim` instead. + * @notice Claim collected incentive fees and transfer them to the recipient. + * @dev Caller needs to pay the PAYOUT_AMOUNT of WBERA tokens. + * @dev This function is NOT implementing slippage protection. Caller has to check that received amounts match the + * minimum expected. + * @param recipient The address to which collected incentive fees will be transferred. + * @param incentiveFeeTokens The addresses of the incentive fee token to collect to the recipient. + */ + function claimFees(address recipient, address[] calldata incentiveFeeTokens) external; + + /** + * @notice Claim collected incentive tokens and transfer them to the recipient. + * @dev Caller needs to pay the PAYOUT_AMOUNT of WBERA tokens. + * @dev This function is NOT implementing slippage protection. Caller has to check that received amounts match the + * minimum expected. + * @param recipient The address to which collected incentive tokens will be transferred. + * @param incentiveTokens The addresses of the incentive tokens to collect to the recipient. + */ + function claim(address recipient, address[] calldata incentiveTokens) external; +} diff --git a/src/pol/interfaces/IPOLErrors.sol b/src/pol/interfaces/IPOLErrors.sol index a828930..1c06948 100644 --- a/src/pol/interfaces/IPOLErrors.sol +++ b/src/pol/interfaces/IPOLErrors.sol @@ -47,8 +47,6 @@ interface IPOLErrors is IStakingRewardsErrors { error RewardAllocationBlockDelayTooLarge(); // Signature: 0x08519afa error NotFactoryVault(); - // Signature: 0x978dc040 - error ZeroPercentageWeight(); // Signature: 0x585b9263 error InvalidWeight(); // Signature: 0xcb1ee123 @@ -61,26 +59,11 @@ interface IPOLErrors is IStakingRewardsErrors { error CommissionNotQueuedOrDelayNotPassed(); // Signature: 0xc1abde53 error InvalidCommissionChangeDelay(); - // Signature: 0x716e870e - error InvalidIncentiveFeeRate(); // Signature: 0x6a1aee5f error NotRewardAllocator(); // Signature: 0xeb732c63 error InvalidRewardAllocationInactivityBlockSpan(); - /* BLOCK REWARD CONTROLLLER */ - - // Signature: 0x2e2dab43 - error InvalidBaseRate(); - // Signature: 0x22be2284 - error InvalidRewardRate(); - // Signature: 0x15482337 - error InvalidMinBoostedRewardRate(); - // Signature: 0xb7b2319a - error InvalidBoostMultiplier(); - // Signature: 0x347f95b2 - error InvalidRewardConvexity(); - /* STAKING */ // Signature: 0x09ee12d5 @@ -97,6 +80,8 @@ interface IPOLErrors is IStakingRewardsErrors { error NotIncentiveManager(); // Signature: 0x92949442 error AmountGreaterThanReward(); + // Signature: 0x493309ac + error NotRewardVaultHelper(); // Signature: 0xf84835a0 error TokenNotWhitelisted(); @@ -127,15 +112,10 @@ interface IPOLErrors is IStakingRewardsErrors { error DuplicateReceiver(address receiver); // Signature: 0xe7726b79 error InvalidRewardDuration(); - // Signature: 0x91373fcd - error NotRewardDurationManager(); // Signature: 0xfb89696d error NotRewardVaultManager(); // Signature: 0x4c168419 error DurationChangeNotAllowed(); - // Signature: 0x3a70eb50 - error RewardDurationCoolDownPeriodNotPassed(); - /* BEACON ROOTS */ // Signature: 0x1390f2a1 diff --git a/src/pol/interfaces/IRewardVault.sol b/src/pol/interfaces/IRewardVault.sol index 4668acf..1335ec2 100644 --- a/src/pol/interfaces/IRewardVault.sol +++ b/src/pol/interfaces/IRewardVault.sol @@ -43,7 +43,7 @@ interface IRewardVault is IPOLErrors, IStakingRewards { /// @notice Emitted when an incentive token is whitelisted. /// @param token The address of the token that has been whitelisted. - /// @param minIncentiveRate The minimum amount of the token to incentivize per BGT emission. + /// @param minIncentiveRate The minimum amount of the token to incentivize per emission token emitted. /// @param manager The address of the manager that can addIncentive for this incentive token. event IncentiveTokenWhitelisted(address indexed token, uint256 minIncentiveRate, address manager); @@ -56,46 +56,34 @@ interface IRewardVault is IPOLErrors, IStakingRewards { event MaxIncentiveTokensCountUpdated(uint8 maxIncentiveTokensCount); /// @notice Emitted when validator share of incentives are processed to the operator address of a validator. - event IncentivesProcessed(bytes indexed pubkey, address indexed token, uint256 bgtEmitted, uint256 amount); + event IncentivesProcessed(bytes indexed pubkey, address indexed token, uint256 rewardsEmitted, uint256 amount); /// @notice Emitted when validator share of incentives fail to be processed to the operator address of a validator. - event IncentivesProcessFailed(bytes indexed pubkey, address indexed token, uint256 bgtEmitted, uint256 amount); + event IncentivesProcessFailed(bytes indexed pubkey, address indexed token, uint256 rewardsEmitted, uint256 amount); /// @notice Emitted when incentives are added to the vault. /// @param token The incentive token. /// @param sender The address that added the incentive. /// @param amount The amount of the incentive. - /// @param incentiveRate The amount of the token to incentivize per BGT emission. + /// @param incentiveRate The amount of the token to incentivize per emission token emitted. event IncentiveAdded(address indexed token, address sender, uint256 amount, uint256 incentiveRate); - /// @notice Emitted when the BGT booster share of the incentive is processed. + /// @notice Emitted when the incentive tokens are collected. /// @param pubkey The pubkey of the validator. /// @param token The address of the incentive token. - /// @param bgtEmitted The amount of BGT emitted by the validator. - /// @param amount The amount of the incentive. - event BGTBoosterIncentivesProcessed( - bytes indexed pubkey, address indexed token, uint256 bgtEmitted, uint256 amount - ); + /// @param rewardsEmitted The amount of rewards emitted by the validator. + /// @param amount The amount of the incentives collected. + event IncentivesCollected(bytes indexed pubkey, address indexed token, uint256 rewardsEmitted, uint256 amount); - /// @notice Emitted when the BGT booster share of the incentive fails to be processed. + /// @notice Emitted when the incentive tokens failed to be sent to the collector. /// @param pubkey The pubkey of the validator. /// @param token The address of the incentive token. - /// @param bgtEmitted The amount of BGT emitted by the validator. - /// @param amount The amount of the incentive. - event BGTBoosterIncentivesProcessFailed( - bytes indexed pubkey, address indexed token, uint256 bgtEmitted, uint256 amount + /// @param rewardsEmitted The amount of rewards emitted by the validator. + /// @param amount The amount of the incentives not collected due to failure. + event IncentivesCollectionFailed( + bytes indexed pubkey, address indexed token, uint256 rewardsEmitted, uint256 amount ); - /// @notice Emitted when the incentive fee is sent to the collector. - /// @param token The address of the incentive token. - /// @param amount The amount of the incentive fee. - event IncentiveFeeCollected(address indexed token, uint256 amount); - - /// @notice Emitted when the incentive fee is failed to be sent to the collector. - /// @param token The address of the incentive token. - /// @param amount The amount of the incentive fee. - event IncentiveFeeCollectionFailed(address indexed token, uint256 amount); - /// @notice Emitted when the target rewards per second is updated. /// @param newTargetRewardsPerSecond The new target rewards per second. /// @param oldTargetRewardsPerSecond The old target rewards per second. @@ -113,6 +101,11 @@ interface IRewardVault is IPOLErrors, IStakingRewards { uint256 newMinRewardDurationForTargetRate, uint256 oldMinRewardDurationForTargetRate ); + /// @notice Emitted when the reward token is migrated from BGT to WBERA. + /// @param oldToken The previous reward token address (BGT). + /// @param newToken The new reward token address (WBERA). + event RewardTokenMigrated(address indexed oldToken, address indexed newToken); + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* GETTERS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -149,7 +142,7 @@ interface IRewardVault is IPOLErrors, IStakingRewards { /** * @notice Initialize the vault, this is only callable once and by the factory since its the deployer. * @param _berachef The address of the berachef. - * @param _bgt The address of the BGT token. + * @param _bgt The address of the BGT token, preserved for the lazy migration to WBERA as reward token. * @param _distributor The address of the distributor. * @param _stakingToken The address of the staking token. */ @@ -194,7 +187,7 @@ interface IRewardVault is IPOLErrors, IStakingRewards { /// @notice Allows the factory owner to whitelist a token to incentivize with. /// @param token The address of the token to whitelist. - /// @param minIncentiveRate The minimum amount of the token to incentivize per BGT emission. + /// @param minIncentiveRate The minimum amount of the token to incentivize per emission token emitted. /// @param manager The address of the manager that can addIncentive for this token. function whitelistIncentiveToken(address token, uint256 minIncentiveRate, address manager) external; @@ -223,11 +216,11 @@ interface IRewardVault is IPOLErrors, IStakingRewards { /// @notice Exit the vault with the staked tokens and claim the reward. /// @dev Only the account holder can call this function, not the operator. /// @dev Clears out the user self-staked balance and rewards. - /// @param recipient The address to send the 'BGT' reward to. + /// @param recipient The address to send the reward to. function exit(address recipient) external; /// @notice Claim the reward. - /// @dev The operator only handles BGT, not STAKING_TOKEN. + /// @dev The operator only handles the reward token, not STAKING_TOKEN. /// @dev Callable by the operator or the account holder. /// @param account The account to get the reward for. /// @param recipient The address to send the reward to. @@ -236,7 +229,7 @@ interface IRewardVault is IPOLErrors, IStakingRewards { /// @notice Claim a partial reward. /// @notice Use `getReward` if you want to claim the full reward. - /// @dev The operator only handles BGT, not STAKING_TOKEN. + /// @dev The operator only handles the reward token, not STAKING_TOKEN. /// @dev Callable by the operator or the account holder. /// @param account The account to get the reward for. /// @param recipient The address to send the reward to. @@ -261,6 +254,13 @@ interface IRewardVault is IPOLErrors, IStakingRewards { /// @param amount The amount of tokens to withdraw. function withdraw(uint256 amount) external; + /// @notice Withdraw the entire self-staked balance of an account. + /// @dev Only the reward vault helper contract can call this function. + /// Withdrawn tokens are sent to the caller which have to handle them appropriately. + /// @param account The account whose stake is withdrawn. + /// @return The amount of tokens withdrawn. + function withdrawAllFor(address account) external returns (uint256); + /// @notice Withdraw tokens staked on behalf of another account by the delegate (msg.sender). /// @param account The account to withdraw for. /// @param amount The amount of tokens to withdraw. @@ -282,7 +282,7 @@ interface IRewardVault is IPOLErrors, IStakingRewards { /// its delegates. /// @param token The address of the token to add as an incentive. /// @param amount The amount of the token to add as an incentive. - /// @param incentiveRate The amount of the token to incentivize per BGT emission. + /// @param incentiveRate The amount of the token to incentivize per emission token emitted. /// @dev Permissioned function, only callable by incentive token manager. function addIncentive(address token, uint256 amount, uint256 incentiveRate) external; diff --git a/src/pol/interfaces/IRewardVaultFactory.sol b/src/pol/interfaces/IRewardVaultFactory.sol index 29b282a..35ff0fa 100644 --- a/src/pol/interfaces/IRewardVaultFactory.sol +++ b/src/pol/interfaces/IRewardVaultFactory.sol @@ -16,27 +16,11 @@ interface IRewardVaultFactory is IPOLErrors { event VaultCreated(address indexed stakingToken, address indexed vault); /** - * @notice Emitted when the BGTIncentiveDistributor contract is set. - * @param newBGTIncentiveDistributor The address of the new BGTIncentiveDistributor contract. - * @param oldBGTIncentiveDistributor The address of the old BGTIncentiveDistributor contract. + * @notice Emitted when the incentive tokens collector address is updated. + * @param newAddress The new address for incentive tokens collector. + * @param oldAddress The old address for incentive tokens collector. */ - event BGTIncentiveDistributorSet( - address indexed newBGTIncentiveDistributor, address indexed oldBGTIncentiveDistributor - ); - - /** - * @notice Emitted when the incentive fee percentage is updated. - * @param newValue The new rate (in basis points). - * @param oldValue The old rate (in basis points). - */ - event IncentiveFeeRateUpdated(uint256 newValue, uint256 oldValue); - - /** - * @notice Emitted when the incentive fee collector address is updated. - * @param newAddress The new address for incentive fees. - * @param oldAddress The old address for incentive fees. - */ - event IncentiveFeeCollectorUpdated(address newAddress, address oldAddress); + event IncentiveTokensCollectorUpdated(address newAddress, address oldAddress); /** * @notice Emitted when the reward vault helper address is updated. @@ -50,25 +34,11 @@ interface IRewardVaultFactory is IPOLErrors { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /** - * @notice Sets the BGTIncentiveDistributor contract. + * @notice Sets the incentive tokens collector address. * @dev Only callable by the admin. - * @param _bgtIncentiveDistributor The address of the new BGTIncentiveDistributor contract. + * @param _incentiveTokensCollector The address of the new incentive tokens collector contract. */ - function setBGTIncentiveDistributor(address _bgtIncentiveDistributor) external; - - /** - * @notice Sets the incentives fee rate. - * @dev Only callable by the admin. - * @param _bgtIncentiveFeeRate The new value for the rate (in basis points). - */ - function setBGTIncentiveFeeRate(uint256 _bgtIncentiveFeeRate) external; - - /** - * @notice Sets the BGTIncentiveDistributor contract. - * @dev Only callable by the admin. - * @param _bgtIncentiveFeeCollector The address of the new BGTIncentiveFeeCollector contract. - */ - function setBGTIncentiveFeeCollector(address _bgtIncentiveFeeCollector) external; + function setIncentiveTokensCollector(address _incentiveTokensCollector) external; /** * @notice Sets the reward vault helper address. @@ -118,12 +88,6 @@ interface IRewardVaultFactory is IPOLErrors { */ function allVaultsLength() external view returns (uint256); - /** - * @notice Gets the address of the BGTIncentiveDistributor contract. - * @return The address of the BGTIncentiveDistributor contract. - */ - function bgtIncentiveDistributor() external view returns (address); - /** * @notice Gets the address of the RewardVaultHelper contract. * @return The address of the RewardVaultHelper contract. @@ -138,21 +102,8 @@ interface IRewardVaultFactory is IPOLErrors { function predictRewardVaultAddress(address stakingToken) external view returns (address); /** - * @notice Gets the value of the incentive fee rate. - * @return The rate (in basis points). - */ - function bgtIncentiveFeeRate() external view returns (uint256); - - /** - * @notice Gets the address of the incentive fee collector. - * @return The address of the BGTIncentiveFeeCollector contract. - */ - function bgtIncentiveFeeCollector() external view returns (address); - - /** - * @notice Applies the fee percentage on the incentive amount. - * @param incentiveAmount The amount of incentive tokens. - * @return The fee amount. + * @notice Gets the address of the incentive tokens collector. + * @return The address of the incentive tokens collector. */ - function getIncentiveFeeAmount(uint256 incentiveAmount) external view returns (uint256); + function incentiveTokensCollector() external view returns (address); } diff --git a/src/pol/interfaces/IRewardVaultHelper.sol b/src/pol/interfaces/IRewardVaultHelper.sol index 5977c3c..d426b54 100644 --- a/src/pol/interfaces/IRewardVaultHelper.sol +++ b/src/pol/interfaces/IRewardVaultHelper.sol @@ -4,9 +4,33 @@ pragma solidity ^0.8.26; import { IPOLErrors } from "./IPOLErrors.sol"; interface IRewardVaultHelper is IPOLErrors { + /// @notice Emitted when the sWBERA address is set. + /// @param sWBERA The address of the sWBERA token. + event SWBERASet(address sWBERA); + + /// @notice Emitted when rewards are claimed. + /// @param amount The amount of rewards claimed. + /// @param receiver The address to receive the rewards. + /// @param outputToken The token to convert the rewards to. + event RewardsClaimed(uint256 amount, address receiver, address outputToken); + /// @notice Claim all rewards from multiple vaults. /// @dev Reverts if any of the vaults do not implement the Berachain Reward Vault interface. /// @param vaults The array of vault addresses. /// @param receiver The address to receive the rewards. function claimAllRewards(address[] memory vaults, address receiver) external; + + /// @notice Claim all rewards from multiple vaults and convert them to a specific token. + /// @dev Reverts if any of the vaults do not implement the Berachain Reward Vault interface. + /// @dev Reverts if the output token is not a valid token (WBERA, sWBERA, or native BERA (address(0))). + /// @param vaults The array of vault addresses. + /// @param receiver The address to receive the rewards. + /// @param outputToken The token to convert the rewards to. + function claimAllRewards(address[] memory vaults, address receiver, address outputToken) external; + + /// @notice Withdraw the caller's self-staked balance from multiple vaults in a single transaction. + /// @dev Skips vaults where the caller has no self-staked balance. Delegate-staked amounts are not withdrawn. + /// @param vaults The array of vault addresses. + /// @param receiver The address to receive the withdrawn stake tokens. + function withdrawAllFromVaults(address[] memory vaults, address receiver) external; } diff --git a/src/pol/interfaces/IWBERA.sol b/src/pol/interfaces/IWBERA.sol new file mode 100644 index 0000000..87fbdcd --- /dev/null +++ b/src/pol/interfaces/IWBERA.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import { IERC20Metadata } from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; + +/// @title IWBERA +/// @notice Interface for the Wrapped BERA (WBERA) contract based on Solady's WETH implementation. +interface IWBERA is IERC20Metadata { + /// @notice Deposits BERA and mints WBERA to the caller. + function deposit() external payable; + + /// @notice Burns WBERA and withdraws BERA to the caller. + /// @param amount Amount of WBERA to burn/BERA to withdraw. + function withdraw(uint256 amount) external; +} diff --git a/src/pol/interfaces/IWBERAStakerVault.sol b/src/pol/interfaces/IWBERAStakerVault.sol index 980f9df..33e9f3e 100644 --- a/src/pol/interfaces/IWBERAStakerVault.sol +++ b/src/pol/interfaces/IWBERAStakerVault.sol @@ -105,7 +105,7 @@ interface IWBERAStakerVault is IPOLErrors { /** * @notice Receive rewards. - * @dev Rewards are received from `BGTIncentiveFeeCollector` contract and this function is being introduced + * @dev Rewards are received from `IncentivesCollector` contract and this function is being introduced * for better tracking of rewards. * @param amount The amount of rewards to receive. */ diff --git a/src/pol/interfaces/lst/ILSTStakerVaultFactory.sol b/src/pol/interfaces/lst/ILSTStakerVaultFactory.sol index 7fbbd27..1af0012 100644 --- a/src/pol/interfaces/lst/ILSTStakerVaultFactory.sol +++ b/src/pol/interfaces/lst/ILSTStakerVaultFactory.sol @@ -35,7 +35,7 @@ interface ILSTStakerVaultFactory is IPOLErrors { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @notice Creates a new pair LSTStakerVault (ERC4626) / LSTStakerVaultWithdrawalRequest (ERC721) - /// to participate to BGTIncentiveFeeCollector redistribution. + /// to participate to IncentivesCollector redistribution. /// @dev Can only be called by `DEFAULT_ADMIN_ROLE`. /// @dev Only supports 18 decimals tokens. /// @param stakingToken The address of the staking token. diff --git a/src/pol/interfaces/lst/IStakerVault.sol b/src/pol/interfaces/lst/IStakerVault.sol index 9acf3dc..a1506c8 100644 --- a/src/pol/interfaces/lst/IStakerVault.sol +++ b/src/pol/interfaces/lst/IStakerVault.sol @@ -74,7 +74,7 @@ interface IStakerVault is IPOLErrors { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @notice Receive rewards. - /// @dev Rewards are received from `BGTIncentiveFeeCollector` contract and this function is being introduced + /// @dev Rewards are received from `IncentivesCollector` contract and this function is being introduced /// for better tracking of rewards. /// @param amount The amount of rewards to receive. function receiveRewards(uint256 amount) external; diff --git a/src/pol/lst/LSTStakerVaultFactory.sol b/src/pol/lst/LSTStakerVaultFactory.sol index f52f3c6..1ea8553 100644 --- a/src/pol/lst/LSTStakerVaultFactory.sol +++ b/src/pol/lst/LSTStakerVaultFactory.sol @@ -111,6 +111,8 @@ contract LSTStakerVaultFactory is ILSTStakerVaultFactory, AccessControlUpgradeab IERC20(stakingToken).safeTransferFrom(msg.sender, address(this), INITIAL_DEPOSIT); IERC20(stakingToken).safeIncreaseAllowance(vault, INITIAL_DEPOSIT); LSTStakerVault(vault).deposit(INITIAL_DEPOSIT, msg.sender); + // make sure the inflation attack is avoided + require(LSTStakerVault(vault).totalSupply() == INITIAL_DEPOSIT, "Inflation attack detected"); return addresses; } diff --git a/src/pol/rewards/BGTIncentiveDistributor.sol b/src/pol/rewards/BGTIncentiveDistributor.sol index 759206a..0e2db00 100644 --- a/src/pol/rewards/BGTIncentiveDistributor.sol +++ b/src/pol/rewards/BGTIncentiveDistributor.sol @@ -49,6 +49,9 @@ contract BGTIncentiveDistributor is /// @notice Tracks the amount of incentive tokens currently held by the contract for each validator. mapping(bytes => mapping(address => uint256)) public incentiveTokensPerValidator; + /// @notice The timestamp of the last aggregation of the rewards. + uint64 public lastAggregationTimestamp; + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -79,6 +82,12 @@ contract BGTIncentiveDistributor is _setRewardClaimDelay(_delay); } + /// @inheritdoc IBGTIncentiveDistributor + function setLastAggregationTimestamp(uint64 timestamp_) external onlyRole(DEFAULT_ADMIN_ROLE) { + lastAggregationTimestamp = timestamp_; + emit LastAggregationTimestampSet(timestamp_); + } + /// @inheritdoc IBGTIncentiveDistributor function updateRewardsMetadata(Distribution[] calldata _distributions) external onlyRole(MANAGER_ROLE) { uint256 dLen = _distributions.length; @@ -134,12 +143,25 @@ contract BGTIncentiveDistributor is emit IncentiveReceived(pubkey, token, _amount); } + /// @inheritdoc IBGTIncentiveDistributor + function allocateIncentiveToValidator(address token, uint256 amount) external onlyRole(MANAGER_ROLE) { + bytes memory pubkey = bytes( + hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ); + if (amount > IERC20(token).balanceOf(address(this))) { + InsufficientIncentiveTokens.selector.revertWith(); + } + incentiveTokensPerValidator[pubkey][token] = amount; + emit IncentiveAllocated(pubkey, token, amount); + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* USER FUNCTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IBGTIncentiveDistributor - function claim(Claim[] calldata _claims) external nonReentrant whenNotPaused { + function claim(Claim[] calldata _claims) external nonReentrant { + if (paused() && !hasRole(MANAGER_ROLE, msg.sender)) EnforcedPause.selector.revertWith(); uint256 cLen = _claims.length; if (cLen == 0) InvalidArray.selector.revertWith(); @@ -165,6 +187,7 @@ contract BGTIncentiveDistributor is if (reward.merkleRoot == 0) InvalidMerkleRoot.selector.revertWith(); if (reward.activeAt > block.timestamp) RewardInactive.selector.revertWith(); + if (reward.activeAt < lastAggregationTimestamp) InvalidDistribution.selector.revertWith(); uint256 lifeTimeAmount = claimed[_identifier][_account] + _amount; diff --git a/src/pol/rewards/BeraChef.sol b/src/pol/rewards/BeraChef.sol index 4b91f53..54b8690 100644 --- a/src/pol/rewards/BeraChef.sol +++ b/src/pol/rewards/BeraChef.sol @@ -200,7 +200,7 @@ contract BeraChef is IBeraChef, OwnableUpgradeable, UUPSUpgradeable { onlyOwner { // Check if the proposed receiver (vault) is registered in the factory - address stakeToken = address(RewardVault(receiver).stakeToken()); + address stakeToken = address(RewardVault(payable(receiver)).stakeToken()); address factoryVault = IRewardVaultFactory(factory).getVault(stakeToken); if (receiver != factoryVault) { NotFactoryVault.selector.revertWith(); diff --git a/src/pol/rewards/BlockRewardController.sol b/src/pol/rewards/BlockRewardController.sol index 1995dc5..7d96a10 100644 --- a/src/pol/rewards/BlockRewardController.sol +++ b/src/pol/rewards/BlockRewardController.sol @@ -3,65 +3,71 @@ pragma solidity 0.8.26; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; +import { SafeTransferLib } from "solady/src/utils/SafeTransferLib.sol"; import { Utils } from "../../libraries/Utils.sol"; import { IBlockRewardController } from "../interfaces/IBlockRewardController.sol"; import { IBeaconDeposit } from "../interfaces/IBeaconDeposit.sol"; import { BGT } from "../BGT.sol"; +import { IWBERA } from "../interfaces/IWBERA.sol"; /// @title BlockRewardController /// @author Berachain Team -/// @notice The BlockRewardController contract is responsible for managing the reward rate of BGT. +/// @notice The BlockRewardController contract is responsible for managing the reward rate +/// and distributing WBERA (migrated from BGT). /// @dev It should be owned by the governance module. -/// @dev It should also be the only contract that can mint the BGT token. /// @dev The invariant(s) that should hold true are: /// - processRewards() is only called at most once per block timestamp. contract BlockRewardController is IBlockRewardController, OwnableUpgradeable, UUPSUpgradeable { using Utils for bytes4; - /// @notice The maximum value for base rate. - uint256 public constant MAX_BASE_RATE = 5 * FixedPointMathLib.WAD; + /// @notice The version of the contract. + uint64 public constant VERSION = 2; - /// @notice The maximum value for reward rate. - uint256 public constant MAX_REWARD_RATE = 5 * FixedPointMathLib.WAD; + /// @notice The constant base rate for the emission token sent to the validator's operator each block. + uint256 internal constant _BASE_RATE = 0.4e18; - /// @notice The maximum value for the minimum reward rate after boosts accounting. - uint256 public constant MAX_MIN_BOOSTED_REWARD_RATE = 10 * FixedPointMathLib.WAD; + /// @notice The constant reward rate for the emission token sent to the distributor each block. + uint256 internal constant _REWARD_RATE = 1.305e18; - /// @notice The maximum value for boost multiplier. - uint256 public constant MAX_BOOST_MULTIPLIER = 5 * FixedPointMathLib.WAD; - - /// @notice The maximum value for reward convexity parameter. - uint256 public constant MAX_REWARD_CONVEXITY = FixedPointMathLib.WAD; + /// @notice The WBERA token address. + address public constant WBERA_ADDRESS = 0x6969696969696969696969696969696969696969; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* STORAGE */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /// @notice The BGT token contract that we are minting to the distributor. + /// @notice The BGT token contract used internally for minting and redeeming to native tokens. BGT public bgt; /// @notice The Beacon deposit contract to check the pubkey -> operator relationship. IBeaconDeposit public beaconDepositContract; - /// @notice The distributor contract that receives the minted BGT. + /// @notice The distributor contract that receives the minted WBERA. address public distributor; - /// @notice The constant base rate for BGT. - uint256 public baseRate; + /// @dev Deprecated. Replaced by the `BASE_RATE` constant. Slot retained to preserve storage layout. + uint256 internal _baseRate; - /// @notice The reward rate for BGT. - uint256 public rewardRate; + /// @dev Deprecated. Replaced by the `REWARD_RATE` constant. Slot retained to preserve storage layout. + uint256 internal _rewardRate; + /// @dev Deprecated /// @notice The minimum reward rate for BGT after accounting for validator boosts. - uint256 public minBoostedRewardRate; + uint256 internal _minBoostedRewardRate; + /// @dev Deprecated /// @notice The boost multiplier param in the function, determines the inflation cap, 18 dec. - uint256 public boostMultiplier; + uint256 internal _boostMultiplier; + /// @dev Deprecated /// @notice The reward convexity param in the function, determines how fast it converges to its max, 18 dec. - int256 public rewardConvexity; + int256 internal _rewardConvexity; + + /// @notice The WBERA token contract used to wrap and distribute rewards. + IWBERA public wbera; + + receive() external payable { } /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -87,6 +93,22 @@ contract BlockRewardController is IBlockRewardController, OwnableUpgradeable, UU beaconDepositContract = IBeaconDeposit(_beaconDepositContract); } + /// @notice V2 initializer: migrates the emission token from BGT to WBERA. + /// @dev Sets the WBERA contract used for wrapping native tokens before distribution, + /// and clears deprecated rate and boost parameters from V1 (rates are now constants). + function initialize() external reinitializer(VERSION) onlyOwner { + wbera = IWBERA(WBERA_ADDRESS); + + emit BaseRateChanged(_baseRate, _BASE_RATE); + emit RewardRateChanged(_rewardRate, _REWARD_RATE); + + _baseRate = 0; + _rewardRate = 0; + _minBoostedRewardRate = 0; + _boostMultiplier = 0; + _rewardConvexity = 0; + } + function _authorizeUpgrade(address newImplementation) internal override onlyOwner { } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ @@ -104,52 +126,6 @@ contract BlockRewardController is IBlockRewardController, OwnableUpgradeable, UU /* ADMIN FUNCTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /// @inheritdoc IBlockRewardController - function setBaseRate(uint256 _baseRate) external onlyOwner { - if (_baseRate > MAX_BASE_RATE) { - InvalidBaseRate.selector.revertWith(); - } - emit BaseRateChanged(baseRate, _baseRate); - baseRate = _baseRate; - } - - /// @inheritdoc IBlockRewardController - function setRewardRate(uint256 _rewardRate) external onlyOwner { - if (_rewardRate > MAX_REWARD_RATE) { - InvalidRewardRate.selector.revertWith(); - } - emit RewardRateChanged(rewardRate, _rewardRate); - rewardRate = _rewardRate; - } - - /// @inheritdoc IBlockRewardController - function setMinBoostedRewardRate(uint256 _minBoostedRewardRate) external onlyOwner { - if (_minBoostedRewardRate > MAX_MIN_BOOSTED_REWARD_RATE) { - InvalidMinBoostedRewardRate.selector.revertWith(); - } - emit MinBoostedRewardRateChanged(minBoostedRewardRate, _minBoostedRewardRate); - minBoostedRewardRate = _minBoostedRewardRate; - } - - /// @inheritdoc IBlockRewardController - function setBoostMultiplier(uint256 _boostMultiplier) external onlyOwner { - if (_boostMultiplier > MAX_BOOST_MULTIPLIER) { - InvalidBoostMultiplier.selector.revertWith(); - } - emit BoostMultiplierChanged(boostMultiplier, _boostMultiplier); - boostMultiplier = _boostMultiplier; - } - - /// @inheritdoc IBlockRewardController - function setRewardConvexity(uint256 _rewardConvexity) external onlyOwner { - if (_rewardConvexity == 0 || _rewardConvexity > MAX_REWARD_CONVEXITY) { - InvalidRewardConvexity.selector.revertWith(); - } - emit RewardConvexityChanged(uint256(rewardConvexity), _rewardConvexity); - // store as int256 to avoid casting during computation - rewardConvexity = int256(_rewardConvexity); - } - /// @inheritdoc IBlockRewardController function setDistributor(address _distributor) external onlyOwner { if (_distributor == address(0)) { @@ -164,49 +140,23 @@ contract BlockRewardController is IBlockRewardController, OwnableUpgradeable, UU /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IBlockRewardController - function computeReward( - uint256 boostPower, - uint256 _rewardRate, - uint256 _boostMultiplier, - int256 _rewardConvexity - ) - public - pure - returns (uint256 reward) - { - // On conv == 0, mathematical result should be max reward even for boost == 0 (0^0 = 1) - // but since BlockRewardController enforces conv > 0, we're not adding code for conv == 0 case - if (boostPower > 0) { - // Compute intermediate parameters for the reward formula - uint256 one = FixedPointMathLib.WAD; - - if (boostPower == one) { - // avoid approx errors in the following code - reward = FixedPointMathLib.mulWad(_rewardRate, _boostMultiplier); - } else { - // boost^conv ∈ (0, 1] - uint256 tmp_0 = uint256(FixedPointMathLib.powWad(int256(boostPower), _rewardConvexity)); - // 1 + mul * boost^conv ∈ [1, 1 + mul] - uint256 tmp_1 = one + FixedPointMathLib.mulWad(_boostMultiplier, tmp_0); - // 1 - 1 / (1 + mul * boost^conv) ∈ [0, mul / (1 + mul)] - uint256 tmp_2 = one - FixedPointMathLib.divWad(one, tmp_1); - - // @dev Due to splitting fixed point ops, [mul / (1 + mul)] * (1 + mul) may be slightly > mul - uint256 coeff = FixedPointMathLib.mulWad(tmp_2, one + _boostMultiplier); - if (coeff > _boostMultiplier) coeff = _boostMultiplier; - - reward = FixedPointMathLib.mulWad(_rewardRate, coeff); - } - } + function baseRate() external pure returns (uint256) { + return _BASE_RATE; } /// @inheritdoc IBlockRewardController - function getMaxBGTPerBlock() public view returns (uint256 amount) { - amount = computeReward(FixedPointMathLib.WAD, rewardRate, boostMultiplier, rewardConvexity); - if (amount < minBoostedRewardRate) { - amount = minBoostedRewardRate; - } - amount += baseRate; + function rewardRate() external pure returns (uint256) { + return _REWARD_RATE; + } + + /// @inheritdoc IBlockRewardController + function getMaxBGTPerBlock() public pure returns (uint256 amount) { + return _BASE_RATE + _REWARD_RATE; + } + + /// @inheritdoc IBlockRewardController + function getMaxEmissionPerBlock() public pure returns (uint256 amount) { + return _BASE_RATE + _REWARD_RATE; } /// @inheritdoc IBlockRewardController @@ -219,27 +169,37 @@ contract BlockRewardController is IBlockRewardController, OwnableUpgradeable, UU onlyDistributor returns (uint256) { - uint256 base = baseRate; - uint256 reward = 0; - - // Only compute vaults reward if berachef is ready - if (isReady) { - // Calculate the boost power for the validator - uint256 boostPower = bgt.normalizedBoost(pubkey); - reward = computeReward(boostPower, rewardRate, boostMultiplier, rewardConvexity); - if (reward < minBoostedRewardRate) reward = minBoostedRewardRate; - } - - emit BlockRewardProcessed(pubkey, nextTimestamp, base, reward); + uint256 reward = isReady ? _REWARD_RATE : 0; + emit BlockRewardProcessed(pubkey, nextTimestamp, _BASE_RATE, reward); - // Use the beaconDepositContract to fetch the operator, Its gauranteed to return a valid address. - // Beacon Deposit contract will enforce validators to set an operator. address operator = beaconDepositContract.getOperator(pubkey); - if (base > 0) bgt.mint(operator, base); - - // Mint the scaled rewards BGT for validator reward allocation to the distributor. - if (reward > 0) bgt.mint(distributor, reward); + if (_BASE_RATE != 0) _handleMinting(operator, _BASE_RATE); + if (reward != 0) _handleMinting(distributor, reward); return reward; } + + /// @inheritdoc IBlockRewardController + function burnExceedingBalance() external onlyDistributor { + uint256 balance = address(this).balance; + if (balance > 0) { + SafeTransferLib.safeTransferETH(address(0), balance); + emit ExceedingBalanceBurnt(balance); + } + } + + /// @dev Handler to ensure distributeFor will not halt: as long as the consensus layer has not started minting + /// native tokens directly to this contract, this function guarantees by minting and redeeming BGT as needed that + /// there is always enough + /// native balance available to wrap and deliver emissions as WBERA. This wrapper remains necessary until native + /// token minting support arrives at the consensus layer. + function _handleMinting(address receiver, uint256 amount) internal { + if (address(this).balance < amount) { + bgt.mint(address(this), amount); + bgt.redeem(address(this), amount); + } + + wbera.deposit{ value: amount }(); + wbera.transfer(receiver, amount); + } } diff --git a/src/pol/rewards/Distributor.sol b/src/pol/rewards/Distributor.sol index df9c95b..7a32e4f 100644 --- a/src/pol/rewards/Distributor.sol +++ b/src/pol/rewards/Distributor.sol @@ -47,6 +47,9 @@ contract Distributor is /// @dev Mainnet: 1_756_915_200, 2025-09-03T16:00:00.000Z uint64 private constant PECTRA11_HARD_FORK_TIMESTAMP = 1_756_915_200; + /// @notice The WBERA token address. + address private constant WBERA_ADDRESS = 0x6969696969696969696969696969696969696969; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* STORAGE */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -55,11 +58,11 @@ contract Distributor is IBeraChef public beraChef; /// @notice The rewards controller contract that we are getting the rewards rate from. - /// @dev And is responsible for minting the BGT token. + /// @dev Contract is responsible for minting the emission token. IBlockRewardController public blockRewardController; - /// @notice The BGT token contract that we are distributing to the reward allocation receivers. - address public bgt; + /// @notice The emission token contract that we are distributing to the reward allocation receivers. + address public emissionToken; /// @notice The dedicated emission stream manager contract. IDedicatedEmissionStreamManager public dedicatedEmissionStreamManager; @@ -71,7 +74,7 @@ contract Distributor is function initialize( address _berachef, - address _bgt, + address _emissionToken, address _blockRewardController, address _governance, uint64 _zeroValidatorPubkeyGIndex, @@ -85,7 +88,7 @@ contract Distributor is __UUPSUpgradeable_init(); _grantRole(DEFAULT_ADMIN_ROLE, _governance); beraChef = IBeraChef(_berachef); - bgt = _bgt; + emissionToken = _emissionToken; blockRewardController = IBlockRewardController(_blockRewardController); super.setZeroValidatorPubkeyGIndex(_zeroValidatorPubkeyGIndex); super.setProposerIndexGIndex(_proposerIndexGIndex); @@ -123,6 +126,12 @@ contract Distributor is dedicatedEmissionStreamManager = IDedicatedEmissionStreamManager(_dedicatedEmissionStreamManager); } + /// @inheritdoc IDistributor + function setEmissionToken() external onlyRole(DEFAULT_ADMIN_ROLE) { + emissionToken = WBERA_ADDRESS; + emit EmissionTokenSet(emissionToken); + } + /// @inheritdoc IDistributor function distributeFor( uint64 nextTimestamp, @@ -161,32 +170,34 @@ contract Distributor is // Process the rewards with the block rewards controller for the specified block number. // Its dependent on the beraChef being ready, if not it will return zero rewards for the current block. uint256 rewardRate = blockRewardController.processRewards(pubkey, nextTimestamp, beraChef.isReady()); - if (rewardRate == 0) { - // If berachef is not ready (genesis) or there aren't rewards to distribute, skip. This will skip since - // there is no default reward allocation. - return; - } - if (address(dedicatedEmissionStreamManager) != address(0)) { - uint256 emissionPerc = dedicatedEmissionStreamManager.emissionPerc(); - IRewardAllocation.Weight[] memory rewardAllocation = dedicatedEmissionStreamManager.getRewardAllocation(); + if (rewardRate != 0) { + if (address(dedicatedEmissionStreamManager) != address(0)) { + uint256 emissionPerc = dedicatedEmissionStreamManager.emissionPerc(); + IRewardAllocation.Weight[] memory rewardAllocation = + dedicatedEmissionStreamManager.getRewardAllocation(); - if (emissionPerc > 0 && rewardAllocation.length > 0) { - uint256 graAmount = FixedPointMathLib.fullMulDiv(rewardRate, emissionPerc, ONE_HUNDRED_PERCENT); - uint256 excessEmission = _distributeRewards(graAmount, rewardAllocation, pubkey, nextTimestamp, true); + if (emissionPerc > 0 && rewardAllocation.length > 0) { + uint256 graAmount = FixedPointMathLib.fullMulDiv(rewardRate, emissionPerc, ONE_HUNDRED_PERCENT); + uint256 excessEmission = + _distributeRewards(graAmount, rewardAllocation, pubkey, nextTimestamp, true); - // Decrease the reward rate by the reward allocation amount. - rewardRate -= graAmount - excessEmission; + // Decrease the reward rate by the reward allocation amount. + rewardRate -= graAmount - excessEmission; + } } - } - // Activate the queued reward allocation if it is ready. - beraChef.activateReadyQueuedRewardAllocation(pubkey); + // Activate the queued reward allocation if it is ready. + beraChef.activateReadyQueuedRewardAllocation(pubkey); + + // Get the active reward allocation for the validator. + // This will return the default reward allocation if the validator does not have an active reward + // allocation. + IRewardAllocation.RewardAllocation memory ra = beraChef.getActiveRewardAllocation(pubkey); + _distributeRewards(rewardRate, ra.weights, pubkey, nextTimestamp, false); + } - // Get the active reward allocation for the validator. - // This will return the default reward allocation if the validator does not have an active reward allocation. - IRewardAllocation.RewardAllocation memory ra = beraChef.getActiveRewardAllocation(pubkey); - _distributeRewards(rewardRate, ra.weights, pubkey, nextTimestamp, false); + blockRewardController.burnExceedingBalance(); } /// @dev Accumulates any excess emission that cannot be distributed to a vault in the global reward allocation, @@ -236,8 +247,8 @@ contract Distributor is if (rewardAmount > 0) { // The reward vault will pull the rewards from this contract so we can keep the approvals for the - // soul bound token BGT clean. - bgt.safeIncreaseAllowance(receiver, rewardAmount); + // emission token. + emissionToken.safeIncreaseAllowance(receiver, rewardAmount); // Notify the receiver of the reward. IRewardVault(receiver).notifyRewardAmount(pubkey, rewardAmount); diff --git a/src/pol/rewards/RewardVault.sol b/src/pol/rewards/RewardVault.sol index 5b8a928..7648235 100644 --- a/src/pol/rewards/RewardVault.sol +++ b/src/pol/rewards/RewardVault.sol @@ -6,21 +6,23 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; +import { IWBERA } from "../interfaces/IWBERA.sol"; import { Utils } from "../../libraries/Utils.sol"; import { IBeaconDeposit } from "../interfaces/IBeaconDeposit.sol"; +import { IBGT } from "../interfaces/IBGT.sol"; import { IRewardVault } from "../interfaces/IRewardVault.sol"; import { FactoryOwnable } from "../../base/FactoryOwnable.sol"; import { StakingRewards } from "../../base/StakingRewards.sol"; import { IBeraChef } from "../interfaces/IBeraChef.sol"; import { IDistributor } from "../interfaces/IDistributor.sol"; -import { IBGTIncentiveDistributor } from "../interfaces/IBGTIncentiveDistributor.sol"; import { IRewardVaultFactory } from "../interfaces/IRewardVaultFactory.sol"; /// @title Rewards Vault /// @author Berachain Team -/// @notice This contract is the vault for the Berachain rewards, it handles the staking and rewards accounting of BGT. -/// @dev This contract is taken from the stable and tested: +/// @notice This contract is the vault for the Berachain rewards, it handles the staking and rewards accounting of +/// WBERA. +///@dev This contract is taken from the stable and tested: /// https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol /// We are using this model instead of 4626 because we want to incentivize staying in the vault for x period of time /// to be considered a 'miner' and not a 'trader'. @@ -42,8 +44,8 @@ contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, Factory } /// @notice Struct to hold an incentive data. - /// @param minIncentiveRate The minimum amount of the token to incentivize per BGT emission. - /// @param incentiveRate The amount of the token to incentivize per BGT emission. + /// @param minIncentiveRate The minimum amount of the token to incentivize per emission token emitted. + /// @param incentiveRate The amount of the token to incentivize per emission token emitted. /// @param amountRemaining The amount of the token remaining to incentivize. /// @param manager The address of the manager that can addIncentive for this incentive token. struct Incentive { @@ -54,12 +56,10 @@ contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, Factory } uint256 private constant MAX_INCENTIVE_RATE = 1e36; // for 18 decimal token, this will mean 1e18 incentiveTokens - // per BGT emission. + // per emission token emitted. - // Safe gas limit for low level call operations to avoid griefing. - // This is mostly for low level call like approve, receiveIncentive (IBGTIncentiveDistributor which uses - // transferFrom). - uint256 private constant SAFE_GAS_LIMIT = 500_000; + /// @notice The WBERA token address (genesis contract). + address private constant WBERA_ADDRESS = 0x6969696969696969696969696969696969696969; /// @notice The minimum reward duration. uint256 public constant MIN_REWARD_DURATION = 3 days; @@ -112,6 +112,10 @@ contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, Factory /// @dev must be between MIN_REWARD_DURATION and MAX_REWARD_DURATION and can be set only by reward vault manager. uint256 public minRewardDurationForTargetRate; + /// @notice The BGT token address, preserved during reward token migration from BGT to WBERA. + /// @dev address(0) means the vault has not yet migrated; rewardToken still points to BGT. + IERC20 private bgt; + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -139,6 +143,11 @@ contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, Factory emit MaxIncentiveTokensCountUpdated(maxIncentiveTokensCount); } + /// @notice Accept native BERA only from the BGT contract (during BGT redemption for the migration path). + receive() external payable { + if (msg.sender != address(_getBgtToken())) UnauthorizedETHTransfer.selector.revertWith(); + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* MODIFIERS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -149,8 +158,7 @@ contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, Factory } modifier onlyUserOrOperator(address account) { - IRewardVaultFactory factory = IRewardVaultFactory(factory()); - address rewardVaultHelper = factory.rewardVaultHelper(); + address rewardVaultHelper = _getRewardVaultHelper(); if (msg.sender != account && msg.sender != rewardVaultHelper) { if (msg.sender != _operators[account]) { @@ -160,6 +168,15 @@ contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, Factory _; } + modifier onlyRewardVaultHelper() { + address rewardVaultHelper = _getRewardVaultHelper(); + + if (msg.sender != rewardVaultHelper) { + NotRewardVaultHelper.selector.revertWith(); + } + _; + } + modifier checkSelfStakedBalance(address account, uint256 amount) { _checkSelfStakedBalance(account, amount); _; @@ -363,6 +380,20 @@ contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, Factory return _delegateStake[account].stakedByDelegate[delegate]; } + /// @notice Returns the current reward token, always WBERA after the beacon upgrade. + /// @dev Pre-migration (bgt == address(0)): returns the WBERA_ADDRESS constant so all vaults + /// advertise a uniform reward token from the moment the implementation is deployed, regardless + /// of when the lazy per-vault migration triggers. + /// Post-migration (bgt != address(0)): returns the storage-backed value set by + /// _migrateRewardToken(), so the getter reflects actual on-chain state rather than a hardcoded + /// constant. + function rewardToken() public view override returns (IERC20) { + if (address(bgt) == address(0)) { + return IERC20(WBERA_ADDRESS); + } + return super.rewardToken(); + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* WRITES */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -374,6 +405,7 @@ contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, Factory /// @inheritdoc IRewardVault function delegateStake(address account, uint256 amount) external nonReentrant whenNotPaused { + if (account == address(0)) ZeroAddress.selector.revertWith(); if (msg.sender == account) NotDelegate.selector.revertWith(); _stake(account, amount); @@ -391,6 +423,7 @@ contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, Factory /// @inheritdoc IRewardVault function stakeOnBehalf(address account, uint256 amount) external nonReentrant whenNotPaused { + if (account == address(0)) ZeroAddress.selector.revertWith(); _stake(account, amount); } @@ -399,6 +432,20 @@ contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, Factory _withdraw(msg.sender, amount); } + /// @inheritdoc IRewardVault + function withdrawAllFor(address account) + external + nonReentrant + onlyRewardVaultHelper + whenNotPaused + returns (uint256 selfStakedAmount) + { + selfStakedAmount = _accountInfo[account].balance - _delegateStake[account].delegateTotalStaked; + // Early return if there is no self-staked balance to withdraw. + if (selfStakedAmount == 0) return 0; + _withdraw(account, selfStakedAmount); + } + /// @inheritdoc IRewardVault function delegateWithdraw(address account, uint256 amount) external nonReentrant whenNotPaused { if (msg.sender == account) NotDelegate.selector.revertWith(); @@ -476,7 +523,7 @@ contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, Factory if (msg.sender != manager) NotIncentiveManager.selector.revertWith(); // The incentive amount should be equal to or greater than the `minIncentiveRate` to avoid spamming. - // If the `minIncentiveRate` is 100 USDC/BGT, the amount should be at least 100 USDC. + // If the `minIncentiveRate` is 100 USDC per emission token, the amount should be at least 100 USDC. if (amount < minIncentiveRate) AmountLessThanMinIncentiveRate.selector.revertWith(); // The incentive rate should be greater than or equal to the `minIncentiveRate`. @@ -531,6 +578,28 @@ contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, Factory /* INTERNAL FUNCTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + /// @dev One-shot migration: saves the current _rewardToken (BGT) into bgt and + /// switches _rewardToken to WBERA so integrators reading rewardToken() see the actual reward token. + function _migrateRewardToken() internal { + if (address(bgt) == address(0) && address(_rewardToken) != WBERA_ADDRESS) { + bgt = _rewardToken; + _rewardToken = IERC20(WBERA_ADDRESS); + emit RewardTokenMigrated(address(bgt), WBERA_ADDRESS); + } + } + + /// @dev Returns the BGT token regardless of migration state. + /// Post-migration: returns bgt. Pre-migration: returns _rewardToken (which IS BGT). + function _getBgtToken() internal view returns (IERC20) { + IERC20 cached = bgt; + return address(cached) != address(0) ? cached : _rewardToken; + } + + function _updateReward(address account) internal override { + _migrateRewardToken(); + super._updateReward(account); + } + /// @dev Check if the account has enough self-staked balance. /// @param account The account to check the self-staked balance for. /// @param amount The amount being withdrawn. @@ -541,41 +610,68 @@ contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, Factory } } - /// @dev The Distributor grants this contract the allowance to transfer the BGT in its balance. + /// @dev Transfers reward to the recipient, handling dual reward tokens (BGT and WBERA). + /// If BGT allowance is available on the Distributor, pulls BGT, redeems it for BERA, + /// wraps to WBERA, and sends WBERA to the recipient. Falls back to WBERA allowance otherwise. + /// Handles the partial case where BGT allowance covers only part of the amount. function _safeTransferRewardToken(address to, uint256 amount) internal override { - rewardToken.safeTransferFrom(distributor, to, amount); + uint256 bgtAllowance = _getBgtToken().allowance(distributor, address(this)); + if (bgtAllowance >= amount) { + // BGT allowance is greater then the emission amount + _redeemBGTAndTransferWBERA(to, amount); + } else if (bgtAllowance > 0) { + // BGT allowance is greater then 0, but less than the emission amount + uint256 wberaAmount = amount - bgtAllowance; + _redeemBGTAndTransferWBERA(to, bgtAllowance); + _rewardToken.safeTransferFrom(distributor, to, wberaAmount); + } else { + // BGT allowance is 0 + _rewardToken.safeTransferFrom(distributor, to, amount); + } } - // Ensure the provided reward amount is not more than the balance in the contract. + /// @dev Pulls BGT from the Distributor, redeems it for BERA, wraps to WBERA, and sends to recipient. + function _redeemBGTAndTransferWBERA(address to, uint256 amount) internal { + IERC20 bgt_ = _getBgtToken(); + bgt_.safeTransferFrom(distributor, address(this), amount); + IBGT(address(bgt_)).redeem(address(this), amount); + IWBERA(payable(address(_rewardToken))).deposit{ value: amount }(); + _rewardToken.safeTransfer(to, amount); + } + + // Ensure the provided reward amount is not more than the combined BGT + WBERA allowance. // This keeps the reward rate in the right range, preventing overflows due to // very high values of rewardRate in the earned and rewardsPerToken functions; // Reward + leftover must be less than 2^256 / 10^18 to avoid overflow. function _checkRewardSolvency() internal view override { - uint256 allowance = rewardToken.allowance(distributor, address(this)); - if (undistributedRewards / PRECISION > allowance) InsolventReward.selector.revertWith(); + uint256 bgtAllowance = _getBgtToken().allowance(distributor, address(this)); + uint256 wberaAllowance = _rewardToken.allowance(distributor, address(this)); + if (undistributedRewards / PRECISION > bgtAllowance + wberaAllowance) { + InsolventReward.selector.revertWith(); + } } /// @notice process the incentives for a validator. /// @notice If a token transfer consumes more than 500k gas units, the transfer alone will fail. /// @param pubkey The pubkey of the validator to process the incentives for. - /// @param bgtEmitted The amount of BGT emitted by the validator. - function _processIncentives(bytes calldata pubkey, uint256 bgtEmitted) internal { + /// @param rewardsEmitted The amount of rewards emitted by the validator. + function _processIncentives(bytes calldata pubkey, uint256 rewardsEmitted) internal { // Validator's operator corresponding to the pubkey receives the incentives. // The pubkey -> operator relationship is maintained by the BeaconDeposit contract. address _operator = beaconDepositContract.getOperator(pubkey); IBeraChef beraChef = IDistributor(distributor).beraChef(); - address bgtIncentiveDistributor = getBGTIncentiveDistributor(); uint256 whitelistedTokensCount = whitelistedTokens.length; + address incentiveTokensCollector = getIncentiveTokensCollector(); + bool incentiveTransferSuccess; unchecked { for (uint256 i; i < whitelistedTokensCount; ++i) { address token = whitelistedTokens[i]; Incentive storage incentive = incentives[token]; - uint256 amount = FixedPointMathLib.mulDiv(bgtEmitted, incentive.incentiveRate, PRECISION); uint256 amountRemaining = incentive.amountRemaining; + uint256 amount = FixedPointMathLib.mulDiv(rewardsEmitted, incentive.incentiveRate, PRECISION); + amount = FixedPointMathLib.min(amount, amountRemaining); - // collect the incentive fee. - (amount, amountRemaining) = _collectIncentiveFee(token, amount, amountRemaining); uint256 validatorShare; if (amount > 0) { @@ -586,44 +682,26 @@ contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, Factory if (validatorShare > 0) { // Transfer the validator share of the incentive to its operator address. // slither-disable-next-line arbitrary-send-erc20 - bool success = token.trySafeTransfer(_operator, validatorShare); - if (success) { + incentiveTransferSuccess = token.trySafeTransfer(_operator, validatorShare); + if (incentiveTransferSuccess) { // Update the remaining amount only if tokens were transferred. amountRemaining -= validatorShare; - emit IncentivesProcessed(pubkey, token, bgtEmitted, validatorShare); + emit IncentivesProcessed(pubkey, token, rewardsEmitted, validatorShare); } else { - emit IncentivesProcessFailed(pubkey, token, bgtEmitted, validatorShare); + emit IncentivesProcessFailed(pubkey, token, rewardsEmitted, validatorShare); } } if (amount > 0) { - // Transfer the remaining amount of the incentive to the bgtIncentiveDistributor contract for - // distribution among BGT boosters. - // give the bgtIncentiveDistributor the allowance to transfer the incentive token. - bytes memory data = abi.encodeCall(IERC20.approve, (bgtIncentiveDistributor, amount)); - (bool success,) = token.call{ gas: SAFE_GAS_LIMIT }(data); - if (success) { - // reuse the already defined data variable to avoid stack too deep error. - data = abi.encodeCall(IBGTIncentiveDistributor.receiveIncentive, (pubkey, token, amount)); - (success,) = bgtIncentiveDistributor.call{ gas: SAFE_GAS_LIMIT }(data); - if (success) { - amountRemaining -= amount; - emit BGTBoosterIncentivesProcessed(pubkey, token, bgtEmitted, amount); - } else { - // If the transfer fails, set the allowance back to 0. - // If we don't reset the allowance, the approved tokens remain unused, and future calls to - // _processIncentives would revert for tokens like USDT that require allowance to be 0 - // before setting a new value, blocking the entire incentive distribution process. - data = abi.encodeCall(IERC20.approve, (bgtIncentiveDistributor, 0)); - (success,) = token.call{ gas: SAFE_GAS_LIMIT }(data); - emit BGTBoosterIncentivesProcessFailed(pubkey, token, bgtEmitted, amount); - } - } - // if the approve fails, log the failure in sending the incentive to the bgtIncentiveDistributor. - else { - emit BGTBoosterIncentivesProcessFailed(pubkey, token, bgtEmitted, amount); + incentiveTransferSuccess = token.trySafeTransfer(incentiveTokensCollector, amount); + if (incentiveTransferSuccess) { + amountRemaining -= amount; + emit IncentivesCollected(pubkey, token, rewardsEmitted, amount); + } else { + emit IncentivesCollectionFailed(pubkey, token, rewardsEmitted, amount); } } + incentive.amountRemaining = amountRemaining; } } @@ -644,30 +722,6 @@ contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, Factory } } - function _collectIncentiveFee( - address token, - uint256 amount, - uint256 amountRemaining - ) - internal - returns (uint256, uint256) - { - // Computes the fee amount based on the incentive fee rate, and transfers it to the collector. - IRewardVaultFactory factory = IRewardVaultFactory(factory()); - uint256 feeAmount = factory.getIncentiveFeeAmount(amount); - if (feeAmount > 0) { - amount -= feeAmount; - bool success = token.trySafeTransfer(factory.bgtIncentiveFeeCollector(), feeAmount); - if (success) { - amountRemaining -= feeAmount; - emit IncentiveFeeCollected(token, feeAmount); - } else { - emit IncentiveFeeCollectionFailed(token, feeAmount); - } - } - return (amount, amountRemaining); - } - function _setRewardRate() internal override { // if the pending rewards duration is 0, use the current rewards duration, // otherwise use the pending rewards duration. @@ -712,4 +766,9 @@ contract RewardVault is PausableUpgradeable, ReentrancyGuardUpgradeable, Factory emit RewardPaid(account, recipient, amount); } } + + function _getRewardVaultHelper() internal view returns (address) { + IRewardVaultFactory factory = IRewardVaultFactory(factory()); + return factory.rewardVaultHelper(); + } } diff --git a/src/pol/rewards/RewardVaultFactory.sol b/src/pol/rewards/RewardVaultFactory.sol index fec7ff6..f6d85c9 100644 --- a/src/pol/rewards/RewardVaultFactory.sol +++ b/src/pol/rewards/RewardVaultFactory.sol @@ -49,15 +49,17 @@ contract RewardVaultFactory is IRewardVaultFactory, AccessControlUpgradeable, UU /// @notice Array of all vaults that have been created. address[] public allVaults; + /// @dev Deprecated. /// @notice The address of the BGTIncentiveDistributor contract to receive /// the BGT booster share of the incentive tokens. - address public bgtIncentiveDistributor; + address internal _bgtIncentiveDistributor; + /// @dev Deprecated. /// @notice Fee rate on incentives in basis points (e.g., 100 = 1%). - uint256 public bgtIncentiveFeeRate; + uint256 internal _bgtIncentiveFeeRate; - /// @notice The address of the BGTIncentiveFeeCollector contract to receive fees. - address public bgtIncentiveFeeCollector; + /// @notice The address of the incentive tokens collector contract. + address public incentiveTokensCollector; /// @notice The address of the RewardVaultHelper contract to help claim rewards. address public rewardVaultHelper; @@ -100,24 +102,10 @@ contract RewardVaultFactory is IRewardVaultFactory, AccessControlUpgradeable, UU function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) { } /// @inheritdoc IRewardVaultFactory - function setBGTIncentiveDistributor(address _bgtIncentiveDistributor) external onlyRole(DEFAULT_ADMIN_ROLE) { - if (_bgtIncentiveDistributor == address(0)) ZeroAddress.selector.revertWith(); - emit BGTIncentiveDistributorSet(_bgtIncentiveDistributor, bgtIncentiveDistributor); - bgtIncentiveDistributor = _bgtIncentiveDistributor; - } - - /// @inheritdoc IRewardVaultFactory - function setBGTIncentiveFeeRate(uint256 _bgtIncentiveFeeRate) external onlyRole(DEFAULT_ADMIN_ROLE) { - if (_bgtIncentiveFeeRate > MAX_INC_FEE_RATE) InvalidIncentiveFeeRate.selector.revertWith(); - emit IncentiveFeeRateUpdated(_bgtIncentiveFeeRate, bgtIncentiveFeeRate); - bgtIncentiveFeeRate = _bgtIncentiveFeeRate; - } - - /// @inheritdoc IRewardVaultFactory - function setBGTIncentiveFeeCollector(address _bgtIncentiveFeeCollector) external onlyRole(DEFAULT_ADMIN_ROLE) { - if (_bgtIncentiveFeeCollector == address(0)) ZeroAddress.selector.revertWith(); - emit IncentiveFeeCollectorUpdated(_bgtIncentiveFeeCollector, bgtIncentiveFeeCollector); - bgtIncentiveFeeCollector = _bgtIncentiveFeeCollector; + function setIncentiveTokensCollector(address _incentiveTokensCollector) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_incentiveTokensCollector == address(0)) ZeroAddress.selector.revertWith(); + emit IncentiveTokensCollectorUpdated(_incentiveTokensCollector, incentiveTokensCollector); + incentiveTokensCollector = _incentiveTokensCollector; } /// @inheritdoc IRewardVaultFactory @@ -153,7 +141,7 @@ contract RewardVaultFactory is IRewardVaultFactory, AccessControlUpgradeable, UU emit VaultCreated(stakingToken, vault); // Initialize the vault. - RewardVault(vault).initialize(beaconDepositContract, bgt, distributor, stakingToken); + RewardVault(payable(vault)).initialize(beaconDepositContract, bgt, distributor, stakingToken); return vault; } @@ -176,9 +164,4 @@ contract RewardVaultFactory is IRewardVaultFactory, AccessControlUpgradeable, UU function allVaultsLength() external view returns (uint256) { return allVaults.length; } - - /// @inheritdoc IRewardVaultFactory - function getIncentiveFeeAmount(uint256 incentiveAmount) external view returns (uint256) { - return (incentiveAmount * bgtIncentiveFeeRate) / ONE_HUNDRED_PERCENT; - } } diff --git a/src/pol/rewards/RewardVaultHelper.sol b/src/pol/rewards/RewardVaultHelper.sol index fb6f85d..62ceb77 100644 --- a/src/pol/rewards/RewardVaultHelper.sol +++ b/src/pol/rewards/RewardVaultHelper.sol @@ -6,18 +6,31 @@ import { IRewardVaultHelper } from "src/pol/interfaces/IRewardVaultHelper.sol"; import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { Utils } from "src/libraries/Utils.sol"; +import { IWBERA } from "../interfaces/IWBERA.sol"; +import { SafeTransferLib } from "solady/src/utils/SafeTransferLib.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; /// @title RewardVaultHelper /// @author Berachain Team /// @notice Helper contract that allows claiming rewards from multiple RewardVault contracts in a single transaction. contract RewardVaultHelper is IRewardVaultHelper, AccessControlUpgradeable, UUPSUpgradeable { using Utils for bytes4; + using SafeTransferLib for address; + + /// @notice The WBERA token address. + address payable public constant WBERA_ADDRESS = payable(0x6969696969696969696969696969696969696969); + + /// @notice The sWBERA token address. + address public sWBERA; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } + receive() external payable { } + function initialize(address governance) external initializer { if (governance == address(0)) ZeroAddress.selector.revertWith(); @@ -29,11 +42,64 @@ contract RewardVaultHelper is IRewardVaultHelper, AccessControlUpgradeable, UUPS function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) { } + /// @notice Set the sWBERA address. + /// @dev Can only be called by the DEFAULT_ADMIN_ROLE. + /// @param _sWBERA The address of the sWBERA token. + function setSWBERA(address _sWBERA) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (_sWBERA == address(0)) ZeroAddress.selector.revertWith(); + sWBERA = _sWBERA; + emit SWBERASet(_sWBERA); + } + /// @inheritdoc IRewardVaultHelper function claimAllRewards(address[] memory vaults, address receiver) external { + uint256 rewardsAmount = _claimRewards(vaults, receiver); + emit RewardsClaimed(rewardsAmount, receiver, WBERA_ADDRESS); + } + + /// @inheritdoc IRewardVaultHelper + function claimAllRewards(address[] memory vaults, address receiver, address outputToken) external { + if (outputToken != WBERA_ADDRESS && outputToken != sWBERA && outputToken != address(0)) { + InvalidToken.selector.revertWith(); + } + + address _receiver = address(this); + if (outputToken == WBERA_ADDRESS) _receiver = receiver; + + uint256 rewardsAmount = _claimRewards(vaults, _receiver); + + if (outputToken == address(0)) _unwrapWBERA(rewardsAmount, receiver); // native BERA + else if (outputToken == sWBERA) _stakeWBERA(rewardsAmount, receiver); + + emit RewardsClaimed(rewardsAmount, receiver, outputToken); + } + + /// @inheritdoc IRewardVaultHelper + function withdrawAllFromVaults(address[] memory vaults, address receiver) external { + if (receiver == address(0)) ZeroAddress.selector.revertWith(); + + for (uint256 i = 0; i < vaults.length; i++) { + IRewardVault vault = IRewardVault(vaults[i]); + uint256 amount = vault.withdrawAllFor(msg.sender); + if (amount > 0) address(vault.stakeToken()).safeTransfer(receiver, amount); + } + } + + function _claimRewards(address[] memory vaults, address receiver) internal returns (uint256 rewardsAmount) { for (uint256 i = 0; i < vaults.length; i++) { address vault = vaults[i]; - IRewardVault(vault).getReward(msg.sender, receiver); + rewardsAmount += IRewardVault(vault).getReward(msg.sender, receiver); } + return rewardsAmount; + } + + function _unwrapWBERA(uint256 amount, address receiver) internal { + IWBERA(WBERA_ADDRESS).withdraw(amount); + SafeTransferLib.safeTransferETH(receiver, amount); + } + + function _stakeWBERA(uint256 amount, address receiver) internal { + IERC20(WBERA_ADDRESS).approve(address(sWBERA), amount); + IERC4626(sWBERA).deposit(amount, receiver); } } diff --git a/test/gov/BerachainGovernance.t.sol b/test/gov/BerachainGovernance.t.sol index 835c603..e66b093 100644 --- a/test/gov/BerachainGovernance.t.sol +++ b/test/gov/BerachainGovernance.t.sol @@ -69,7 +69,7 @@ contract BerachainGovernanceTest is GovernanceBaseTest { _calldatas = new bytes[](2); _calldatas[0] = abi.encodeCall(BGT.whitelistSender, (address(this), true)); - _calldatas[1] = abi.encodeCall(IBlockRewardController.setRewardRate, (1000)); + _calldatas[1] = abi.encodeCall(IBlockRewardController.setDistributor, (makeAddr("newDistributor"))); // Mint and delegate more than the proposal threshold amount of BGT to self in order to create and vote // proposal @@ -269,7 +269,6 @@ contract BerachainGovernanceTest is GovernanceBaseTest { assertTrue(gov.state(proposalId) == IGovernor.ProposalState.Executed); assertEq(bgt.isWhitelistedSender(address(this)), true); - assertEq(blockRewardController.rewardRate(), 1000); } // Guardian tests diff --git a/test/gov/GovernanceBase.t.sol b/test/gov/GovernanceBase.t.sol index 0f87374..2097cad 100644 --- a/test/gov/GovernanceBase.t.sol +++ b/test/gov/GovernanceBase.t.sol @@ -21,7 +21,7 @@ abstract contract GovernanceBaseTest is POLTest { MockHoney consensusAsset = new MockHoney(); // The creation of the vault is permissionless. - vault = RewardVault(factory.createRewardVault(address(consensusAsset))); + vault = RewardVault(payable(factory.createRewardVault(address(consensusAsset)))); // Update the whitelisted vaults. address[] memory targets = new address[](1); diff --git a/test/integration/BGTIncentiveDistributorUpgrade.t.sol b/test/integration/BGTIncentiveDistributorUpgrade.t.sol deleted file mode 100644 index eba9d3e..0000000 --- a/test/integration/BGTIncentiveDistributorUpgrade.t.sol +++ /dev/null @@ -1,163 +0,0 @@ -// // SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import { Test } from "forge-std/Test.sol"; -import { ERC20 } from "solady/src/tokens/ERC20.sol"; -import { UpgradeableBeacon } from "solady/src/utils/UpgradeableBeacon.sol"; - -import { IPOLErrors } from "src/pol/interfaces/IPOLErrors.sol"; -import { BeraChef } from "src/pol/rewards/BeraChef.sol"; -import { IBeraChef } from "src/pol/interfaces/IBeraChef.sol"; -import { Create2Deployer } from "src/base/Create2Deployer.sol"; -import { Salt } from "src/base/Salt.sol"; -import { RewardVault } from "src/pol/rewards/RewardVault.sol"; -import { IDistributor } from "src/pol/interfaces/IDistributor.sol"; -import { MockHoney } from "../mock/honey/MockHoney.sol"; -import { RewardVaultFactory } from "src/pol/rewards/RewardVaultFactory.sol"; -import { BGTIncentiveDistributor } from "src/pol/rewards/BGTIncentiveDistributor.sol"; -import { BGTIncentiveDistributorDeployer } from "src/pol/BGTIncentiveDistributorDeployer.sol"; - -import { ChainType } from "script/base/Chain.sol"; -import { POLAddressBook } from "script/pol/POLAddresses.sol"; - -/// @title BGTIncentiveDistributorUpgradeTest -contract BGTIncentiveDistributorUpgradeTest is Create2Deployer, Test, POLAddressBook { - Salt BGT_INCENTIVE_DISTRIBUTOR_SALT = Salt({ implementation: 0, proxy: 2 }); - - address safeOwner = 0xD13948F99525FB271809F45c268D72a3C00a568D; - // pubkey of BicroStrategy validator, distribution at block 2286450 - // https://berascan.com/tx/0x16592f3381629cea5ada0b1c6fedf98f92088cbe32430cb6067a0b32aa102610 - bytes pubkey = - hex"83fd53710b75c2115bd0aac128b739eb9fa9e262603dacdf834030abb1bf4c8a6c00bb72b314c123d77f4ff40cd4d49a"; - - uint256 forkBlock = 2_269_800; - - // operator of BicroStrategy validator - address operator = 0x4595D079A06a9628F8384D7f568A29Cc95a14F1e; - - constructor() POLAddressBook(ChainType.Mainnet) { } - - function setUp() public virtual { - vm.createSelectFork("berachain"); - vm.rollFork(forkBlock); - } - - function test_Fork() public view { - assertEq(block.chainid, 80_094); - assertEq(block.number, forkBlock); - assertEq(block.timestamp, 1_741_841_001); - } - - function test_Upgrade() public { - // Deploy BGTIncentiveDistributor - BGTIncentiveDistributorDeployer bgtIncentiveDistributorDeployer = - new BGTIncentiveDistributorDeployer(safeOwner, BGT_INCENTIVE_DISTRIBUTOR_SALT); - address bgtIncentiveDistributor = address(bgtIncentiveDistributorDeployer.bgtIncentiveDistributor()); - // should be deployed at the precomputed address - // This check will only pass if code compiled with deploy profile, - //commenting it as CI does not compile with deploy profile. - // assertEq(bgtIncentiveDistributor, BGT_INCENTIVE_DISTRIBUTOR_ADDRESS); - - // deploy the new implementations - address newBeraChefImpl = deployWithCreate2(0, type(BeraChef).creationCode); - address newRewardVaultImpl = deployWithCreate2(0, type(RewardVault).creationCode); - address newRewardVaultFactoryImpl = deployWithCreate2(0, type(RewardVaultFactory).creationCode); - - // upgrade the contracts - vm.startPrank(safeOwner); - BeraChef(_polAddresses.beraChef) - .upgradeToAndCall(newBeraChefImpl, abi.encodeCall(BeraChef.setCommissionChangeDelay, 2 * 8191)); - assertEq(BeraChef(_polAddresses.beraChef).commissionChangeDelay(), 2 * 8191); - - // make sure no storage collision - assertEq(BeraChef(_polAddresses.beraChef).distributor(), _polAddresses.distributor); - assertEq(BeraChef(_polAddresses.beraChef).factory(), _polAddresses.rewardVaultFactory); - - RewardVaultFactory(_polAddresses.rewardVaultFactory) - .upgradeToAndCall( - newRewardVaultFactoryImpl, - abi.encodeCall(RewardVaultFactory.setBGTIncentiveDistributor, bgtIncentiveDistributor) - ); - assertEq( - RewardVaultFactory(_polAddresses.rewardVaultFactory).bgtIncentiveDistributor(), bgtIncentiveDistributor - ); - - // make sure no storage collision - assertEq(RewardVaultFactory(_polAddresses.rewardVaultFactory).bgt(), _polAddresses.bgt); - assertEq(RewardVaultFactory(_polAddresses.rewardVaultFactory).distributor(), _polAddresses.distributor); - - // get the beacon from the factory - address beacon = RewardVaultFactory(_polAddresses.rewardVaultFactory).beacon(); - UpgradeableBeacon(beacon).upgradeTo(newRewardVaultImpl); - assertEq(UpgradeableBeacon(beacon).implementation(), newRewardVaultImpl); - vm.stopPrank(); - - // deploy a new reward vault to check no storage collision - address stakingToken = address(new MockHoney()); - address mockRewardVault = RewardVaultFactory(_polAddresses.rewardVaultFactory).createRewardVault(stakingToken); - assertEq(RewardVault(mockRewardVault).distributor(), _polAddresses.distributor); - assertEq(address(RewardVault(mockRewardVault).beaconDepositContract()), _polAddresses.beaconDeposit); - - // test default commission post upgrade - assertEq(BeraChef(_polAddresses.beraChef).getValCommissionOnIncentiveTokens(pubkey), 0.05e4); - } - - function test_CommissionChange_PostUpgrade() public { - test_Upgrade(); - vm.prank(operator); - // queue new commission rate of 1 wei - BeraChef(_polAddresses.beraChef).queueValCommission(pubkey, 1); - - // check the queued commission - BeraChef.QueuedCommissionRateChange memory queuedCommission = - BeraChef(_polAddresses.beraChef).getValQueuedCommissionOnIncentiveTokens(pubkey); - assertEq(queuedCommission.commissionRate, 1); - assertEq(queuedCommission.blockNumberLast, block.number); - - // will revert if try to activate the queue before the delay - vm.expectRevert(IPOLErrors.CommissionNotQueuedOrDelayNotPassed.selector); - BeraChef(_polAddresses.beraChef).activateQueuedValCommission(pubkey); - - vm.roll(forkBlock + (2 * 8191)); - vm.expectEmit(true, true, true, true); - emit IBeraChef.ValCommissionSet(pubkey, 0.05e4, 1); - BeraChef(_polAddresses.beraChef).activateQueuedValCommission(pubkey); - // check the new commission - assertEq(BeraChef(_polAddresses.beraChef).getValCommissionOnIncentiveTokens(pubkey), 1); - - vm.prank(operator); - // queue new commission rate of 20% - BeraChef(_polAddresses.beraChef).queueValCommission(pubkey, 0.2e4); - - // check the queued commission - queuedCommission = BeraChef(_polAddresses.beraChef).getValQueuedCommissionOnIncentiveTokens(pubkey); - assertEq(queuedCommission.commissionRate, 0.2e4); - assertEq(queuedCommission.blockNumberLast, block.number); - - // will revert if try to activate the queue before the delay - vm.expectRevert(IPOLErrors.CommissionNotQueuedOrDelayNotPassed.selector); - BeraChef(_polAddresses.beraChef).activateQueuedValCommission(pubkey); - - vm.roll(vm.getBlockNumber() + (2 * 8191)); - vm.expectEmit(true, true, true, true); - emit IBeraChef.ValCommissionSet(pubkey, 1, 0.2e4); - BeraChef(_polAddresses.beraChef).activateQueuedValCommission(pubkey); - // check the new commission - assertEq(BeraChef(_polAddresses.beraChef).getValCommissionOnIncentiveTokens(pubkey), 0.2e4); - - // new 0 commission rate - vm.prank(operator); - BeraChef(_polAddresses.beraChef).queueValCommission(pubkey, 0); - vm.roll(vm.getBlockNumber() + (2 * 8191)); - vm.expectEmit(true, true, true, true); - emit IBeraChef.ValCommissionSet(pubkey, 0.2e4, 0); - BeraChef(_polAddresses.beraChef).activateQueuedValCommission(pubkey); - // should allow to set commission to 0. - // default 5% commission applies only if no custom commission has been set. - assertEq(BeraChef(_polAddresses.beraChef).getValCommissionOnIncentiveTokens(pubkey), 0); - - // activate commission change revert if not queued - vm.expectRevert(IPOLErrors.CommissionNotQueuedOrDelayNotPassed.selector); - BeraChef(_polAddresses.beraChef).activateQueuedValCommission(pubkey); - } -} diff --git a/test/integration/LSTStakerVaultInfraredBera.t.sol b/test/integration/LSTStakerVaultInfraredBera.t.sol index 9b87603..6ed7de5 100644 --- a/test/integration/LSTStakerVaultInfraredBera.t.sol +++ b/test/integration/LSTStakerVaultInfraredBera.t.sol @@ -5,7 +5,7 @@ import { Test } from "forge-std/Test.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; -import { BGTIncentiveFeeCollector } from "src/pol/BGTIncentiveFeeCollector.sol"; +import { IncentivesCollector } from "src/pol/IncentivesCollector.sol"; import { Create2Deployer } from "src/base/Create2Deployer.sol"; import { InfraredBeraAdapter, IInfraredBera } from "src/pol/lst/InfraredBeraAdapter.sol"; import { LSTStakerVault } from "src/pol/lst/LSTStakerVault.sol"; @@ -14,10 +14,10 @@ import { LSTStakerVaultFactoryDeployer } from "src/pol/lst/LSTStakerVaultFactory import { WBERA } from "src/WBERA.sol"; import { Salt } from "src/base/Salt.sol"; -/// @notice Fork tests for the BGTIncentiveFeeCollector adding InfraredBera as an LST. +/// @notice Fork tests for the IncentivesCollector adding InfraredBera as an LST. contract LSTStakerVaultInfraredBera is Create2Deployer, Test { WBERA wbera = WBERA(payable(0x6969696969696969696969696969696969696969)); - BGTIncentiveFeeCollector collector = BGTIncentiveFeeCollector(0x1984Baf659607Cc5f206c55BB3B00eb3E180190B); + IncentivesCollector collector = IncentivesCollector(0x1984Baf659607Cc5f206c55BB3B00eb3E180190B); LSTStakerVaultFactory factory; address iBera = 0x9b6761bf2397Bb5a6624a856cC84A3A14Dcd3fe5; @@ -63,7 +63,7 @@ contract LSTStakerVaultInfraredBera is Create2Deployer, Test { _upgradeCollector(); uint256 svBalanceBefore = wbera.balanceOf(wberaStakerVault); - _claimFees(); + _claim(); uint256 svDeltaBalance = wbera.balanceOf(wberaStakerVault) - svBalanceBefore; // Check payout amount has been transferred to the Staker Vault @@ -111,7 +111,7 @@ contract LSTStakerVaultInfraredBera is Create2Deployer, Test { uint256 wberaAmountToIbera = payoutAmount * iberaShare / 1e18; uint256 expectedPayoutIbera = IERC4626(iBera).previewMint(wberaAmountToIbera); - _claimFees(); + _claim(); uint256 mainDeltaBalance = wbera.balanceOf(wberaStakerVault) - mainBalanceBefore; uint256 iberaDeltaBalance = IERC20(iBera).balanceOf(infraredVault) - iberaBalanceBefore; @@ -133,10 +133,10 @@ contract LSTStakerVaultInfraredBera is Create2Deployer, Test { } function _upgradeCollector() internal { - // deploy the new implementation of BgtIncentiveFeeCollector - address impl = deployWithCreate2(0, type(BGTIncentiveFeeCollector).creationCode); + // deploy the new implementation of IncentivesCollector + address impl = deployWithCreate2(0, type(IncentivesCollector).creationCode); - // upgrade the BgtIncentiveFeeCollector implementation + // upgrade the IncentivesCollector implementation vm.prank(safeOwner); collector.upgradeToAndCall(impl, bytes("")); } @@ -160,6 +160,25 @@ contract LSTStakerVaultInfraredBera is Create2Deployer, Test { assertEq(honeyBalance, IERC20(honey).balanceOf(claimer)); } + function _claim() public { + address claimer = address(0x1234); + uint256 payoutAmount = collector.payoutAmount(); + vm.deal(claimer, payoutAmount); + + uint256 honeyBalance = IERC20(honey).balanceOf(address(collector)); + + vm.startPrank(claimer); + wbera.deposit{ value: payoutAmount }(); + wbera.approve(address(collector), payoutAmount); + address[] memory tokensToClaim = new address[](1); + tokensToClaim[0] = honey; + collector.claim(claimer, tokensToClaim); + vm.stopPrank(); + + assertEq(0, IERC20(honey).balanceOf(address(collector))); + assertEq(honeyBalance, IERC20(honey).balanceOf(claimer)); + } + function _addInfraredBeraStakerVault() internal returns (address vault) { address adapter = address(new InfraredBeraAdapter()); return _addIBeraLstVault(adapter); diff --git a/test/integration/ReduceRewardDuration.t.sol b/test/integration/ReduceRewardDuration.t.sol index 843f01e..5a49484 100644 --- a/test/integration/ReduceRewardDuration.t.sol +++ b/test/integration/ReduceRewardDuration.t.sol @@ -49,7 +49,7 @@ contract ReduceRewardDurationTest is Create2Deployer, Test, POLAddressBook { address rewardVault = RewardVaultFactory(_polAddresses.rewardVaultFactory).createRewardVault(stakingToken); // new reward duration is 7 days - assertEq(RewardVault(rewardVault).rewardsDuration(), 7 days); + assertEq(RewardVault(payable(rewardVault)).rewardsDuration(), 7 days); } function test_RewardVaultUpgradeOnWhitelistedVaults() public { @@ -62,7 +62,7 @@ contract ReduceRewardDurationTest is Create2Deployer, Test, POLAddressBook { _upgradeVaultImpl(); // verify that rewardVaultManager on such vault is equal to the rewardDurationManager for (uint256 i = 0; i < whitelistedVaults.length; i++) { - assertEq(RewardVault(whitelistedVaults[i]).rewardVaultManager(), rewardDurationManagers[i]); + assertEq(RewardVault(payable(whitelistedVaults[i])).rewardVaultManager(), rewardDurationManagers[i]); } } @@ -73,8 +73,8 @@ contract ReduceRewardDurationTest is Create2Deployer, Test, POLAddressBook { // upgrade the vault implementation to be able to set maxRewardsPerSecond _upgradeVaultImpl(); // default target rewards per second is 0 - assertEq(RewardVault(whitelistedVault).targetRewardsPerSecond(), 0); - assertEq(RewardVault(whitelistedVault).minRewardDurationForTargetRate(), 0); + assertEq(RewardVault(payable(whitelistedVault)).targetRewardsPerSecond(), 0); + assertEq(RewardVault(payable(whitelistedVault)).minRewardDurationForTargetRate(), 0); vm.prank(rewardDurationManager); // set the max rewards per second to 1 BGT per second i.e 1e18 per second and // with precision of 18, it becomes 1e36. @@ -82,11 +82,11 @@ contract ReduceRewardDurationTest is Create2Deployer, Test, POLAddressBook { vm.expectEmit(); emit IRewardVault.TargetRewardsPerSecondUpdated(1e36, 0); emit IRewardVault.MinRewardDurationForTargetRateUpdated(3 days, 0); - RewardVault(whitelistedVault).setTargetRewardsPerSecond(1e36); + RewardVault(payable(whitelistedVault)).setTargetRewardsPerSecond(1e36); // verify that the max rewards per second is 1e36 - assertEq(RewardVault(whitelistedVault).targetRewardsPerSecond(), 1e36); + assertEq(RewardVault(payable(whitelistedVault)).targetRewardsPerSecond(), 1e36); // verify min reward duration for target rate is set to 3 days - assertEq(RewardVault(whitelistedVault).minRewardDurationForTargetRate(), 3 days); + assertEq(RewardVault(payable(whitelistedVault)).minRewardDurationForTargetRate(), 3 days); } function _upgradeVaultImpl() internal { diff --git a/test/mock/pol/NoopBlockRewardController.sol b/test/mock/pol/NoopBlockRewardController.sol index 313b166..603647b 100644 --- a/test/mock/pol/NoopBlockRewardController.sol +++ b/test/mock/pol/NoopBlockRewardController.sol @@ -15,51 +15,24 @@ contract NoopBlockRewardController is IBlockRewardController { return 0; } - /// @inheritdoc IBlockRewardController - function minBoostedRewardRate() external pure returns (uint256) { - return 0; - } - - /// @inheritdoc IBlockRewardController - function boostMultiplier() external pure returns (uint256) { - return 0; - } - - /// @inheritdoc IBlockRewardController - function rewardConvexity() external pure returns (int256) { - return 0; - } - /// @inheritdoc IBlockRewardController function processRewards(bytes calldata, uint64, bool) external pure returns (uint256) { return 0; } /// @inheritdoc IBlockRewardController - function computeReward(uint256, uint256, uint256, int256) external pure returns (uint256) { + function getMaxBGTPerBlock() external pure returns (uint256) { return 0; } /// @inheritdoc IBlockRewardController - function getMaxBGTPerBlock() external pure returns (uint256) { + function getMaxEmissionPerBlock() external pure returns (uint256) { return 0; } /// @inheritdoc IBlockRewardController - function setBaseRate(uint256 _baseRate) external { } - - /// @inheritdoc IBlockRewardController - function setRewardRate(uint256 _rewardRate) external { } - - /// @inheritdoc IBlockRewardController - function setMinBoostedRewardRate(uint256 _minBoostedRewardRate) external { } - - /// @inheritdoc IBlockRewardController - function setBoostMultiplier(uint256 _boostMultiplier) external { } - - /// @inheritdoc IBlockRewardController - function setRewardConvexity(uint256 _rewardConvexity) external { } + function setDistributor(address _distributor) external { } /// @inheritdoc IBlockRewardController - function setDistributor(address _distributor) external { } + function burnExceedingBalance() external { } } diff --git a/test/pol/BGT.t.sol b/test/pol/BGT.t.sol index 1a26def..e1a4029 100644 --- a/test/pol/BGT.t.sol +++ b/test/pol/BGT.t.sol @@ -840,19 +840,8 @@ contract BGTTest is POLTest { bgt.setBgtTermsAndConditions("BGT TERMS AND CONDITIONS"); } - function testFuzz_BurnExcess( - address caller, - uint256 nativeTokenBalance, - uint256 bgtSupply, - uint256 minRewardPerBlock - ) - public - { - uint256 baseRate = 0.5 ether; - minRewardPerBlock = - _bound(minRewardPerBlock, 0, BlockRewardController(blockRewardController).MAX_MIN_BOOSTED_REWARD_RATE()); - - uint256 maxBgtPerBlock = minRewardPerBlock + baseRate; + function testFuzz_BurnExcess(address caller, uint256 nativeTokenBalance, uint256 bgtSupply) public { + uint256 maxBgtPerBlock = BlockRewardController(blockRewardController).getMaxBGTPerBlock(); uint256 potentialMintableAmountInBuffer = 8191 * maxBgtPerBlock; bgtSupply = _bound(bgtSupply, 0, 1e6 ether - potentialMintableAmountInBuffer); @@ -864,11 +853,6 @@ contract BGTTest is POLTest { // Simulate native token accumulated in BGT contract vm.deal(address(bgt), nativeTokenBalance); - vm.startPrank(governance); - BlockRewardController(blockRewardController).setMinBoostedRewardRate(minRewardPerBlock); - BlockRewardController(blockRewardController).setBaseRate(baseRate); - vm.stopPrank(); - vm.prank(caller); if (nativeTokenBalance > bgtSupply + potentialMintableAmountInBuffer) { vm.expectEmit(); diff --git a/test/pol/BGTIncentiveDistributor.t.sol b/test/pol/BGTIncentiveDistributor.t.sol index 8da8808..fd30386 100644 --- a/test/pol/BGTIncentiveDistributor.t.sol +++ b/test/pol/BGTIncentiveDistributor.t.sol @@ -5,6 +5,7 @@ import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC1967 } from "@openzeppelin/contracts/interfaces/IERC1967.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import { POLTest } from "./POL.t.sol"; import { MockERC20 } from "@mock/token/MockERC20.sol"; @@ -17,6 +18,7 @@ contract BGTIncentiveReceiverTest is POLTest { address claimUser = 0xEE8d5FD148c18e79927d8DE79d8E688Abf0b3af5; uint256 claimAmount = 62_452_330_923_580_633_174; + address internal pauser = makeAddr("pauser"); bytes32 internal merkleRoot = hex"9541b35c714a035b8ede23f1cd3daf0c9e7d3e9075ddc3438a42dea643afcafd"; bytes32 internal merkleRootUpdated = hex"0000000000000000000000000000000000000000000000000000000000000000"; bytes32[] internal validProof = [ @@ -30,6 +32,9 @@ contract BGTIncentiveReceiverTest is POLTest { function setUp() public override(POLTest) { super.setUp(); token = new MockERC20(); + bytes32 PAUSER_ROLE = BGTIncentiveDistributor(bgtIncentiveDistributor).PAUSER_ROLE(); + vm.prank(governance); + BGTIncentiveDistributor(bgtIncentiveDistributor).grantRole(PAUSER_ROLE, pauser); } function test_ReceiveIncentive(uint256 amount) public { @@ -91,6 +96,7 @@ contract BGTIncentiveReceiverTest is POLTest { BGTIncentiveDistributor(bgtIncentiveDistributor).updateRewardsMetadata(distributions); (address _token, bytes32 _merkleRoot, bytes32 _proof, uint256 _activeAt, bytes memory _pubkey) = BGTIncentiveDistributor(bgtIncentiveDistributor).rewards(identifier); + vm.stopPrank(); assertEq(_token, address(token)); assertEq(_merkleRoot, merkleRoot); assertEq(_proof, bytes32(0)); @@ -167,6 +173,38 @@ contract BGTIncentiveReceiverTest is POLTest { assertEq(BGTIncentiveDistributor(bgtIncentiveDistributor).claimed(identifier, claimUser), claimAmount); } + function test_ClaimReward_Paused() public { + _helperReceiveIncentive(claimAmount); + bytes32 identifier = test_updateRewardsMetadata(); + + vm.warp(vm.getBlockTimestamp() + BGTIncentiveDistributor(bgtIncentiveDistributor).rewardClaimDelay() + 1); + + // preconditions + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.balanceOf(address(bgtIncentiveDistributor)), claimAmount); + + IBGTIncentiveDistributor.Claim memory claim = IBGTIncentiveDistributor.Claim({ + identifier: identifier, account: claimUser, amount: claimAmount, merkleProof: validProof + }); + + IBGTIncentiveDistributor.Claim[] memory claims = new IBGTIncentiveDistributor.Claim[](1); + claims[0] = claim; + + vm.prank(pauser); + BGTIncentiveDistributor(bgtIncentiveDistributor).setPauseState(true); + + // should revert if not called by manager and contract is paused + vm.expectRevert(abi.encodeWithSelector(PausableUpgradeable.EnforcedPause.selector)); + BGTIncentiveDistributor(bgtIncentiveDistributor).claim(claims); + + // should go through if called by manager + vm.prank(bgtIncentiveReceiverManager); + BGTIncentiveDistributor(bgtIncentiveDistributor).claim(claims); + assertEq(token.balanceOf(claimUser), claimAmount); + assertEq(token.balanceOf(address(bgtIncentiveDistributor)), 0); + assertEq(BGTIncentiveDistributor(bgtIncentiveDistributor).claimed(identifier, claimUser), claimAmount); + } + function test_ClaimReward_Revert_NotClaimable() public { _helperReceiveIncentive(claimAmount); bytes32 identifier = test_updateRewardsMetadata(); @@ -209,6 +247,101 @@ contract BGTIncentiveReceiverTest is POLTest { BGTIncentiveDistributor(bgtIncentiveDistributor).claim(claims); } + function test_SetLastAggregationTimestamp() public { + uint64 ts = uint64(block.timestamp + 100); + vm.expectEmit(true, true, true, true, bgtIncentiveDistributor); + emit IBGTIncentiveDistributor.LastAggregationTimestampSet(ts); + vm.prank(governance); + BGTIncentiveDistributor(bgtIncentiveDistributor).setLastAggregationTimestamp(ts); + assertEq(BGTIncentiveDistributor(bgtIncentiveDistributor).lastAggregationTimestamp(), ts); + } + + function test_SetLastAggregationTimestamp_Revert_NotAdmin() public { + address notAdmin = makeAddr("notAdmin"); + vm.prank(notAdmin); + vm.expectRevert(); + BGTIncentiveDistributor(bgtIncentiveDistributor).setLastAggregationTimestamp(uint64(block.timestamp)); + } + + function test_ClaimReward_Revert_StaleDistribution() public { + _helperReceiveIncentive(claimAmount); + bytes32 identifier = test_updateRewardsMetadata(); + + uint256 claimDelay = BGTIncentiveDistributor(bgtIncentiveDistributor).rewardClaimDelay(); + vm.warp(vm.getBlockTimestamp() + claimDelay + 1); + + // Set lastAggregationTimestamp to after the distribution's activeAt, making it stale + uint64 futureTs = uint64(block.timestamp + 1); + vm.prank(governance); + BGTIncentiveDistributor(bgtIncentiveDistributor).setLastAggregationTimestamp(futureTs); + + IBGTIncentiveDistributor.Claim memory claim = IBGTIncentiveDistributor.Claim({ + identifier: identifier, account: claimUser, amount: claimAmount, merkleProof: validProof + }); + IBGTIncentiveDistributor.Claim[] memory claims = new IBGTIncentiveDistributor.Claim[](1); + claims[0] = claim; + + vm.expectRevert(IPOLErrors.InvalidDistribution.selector); + BGTIncentiveDistributor(bgtIncentiveDistributor).claim(claims); + } + + function test_AllocateIncentiveToValidator(uint256 amount) public { + amount = bound(amount, 1, 1_000_000 ether); + _helperReceiveIncentive(amount); + + bytes memory zeroPubkey = + hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + vm.expectEmit(true, true, true, true, bgtIncentiveDistributor); + emit IBGTIncentiveDistributor.IncentiveAllocated(zeroPubkey, address(token), amount); + + vm.prank(bgtIncentiveReceiverManager); + BGTIncentiveDistributor(bgtIncentiveDistributor).allocateIncentiveToValidator(address(token), amount); + + assertEq( + BGTIncentiveDistributor(bgtIncentiveDistributor).incentiveTokensPerValidator(zeroPubkey, address(token)), + amount + ); + } + + function test_AllocateIncentiveToValidator_OverwritesPreviousAllocation(uint256 first, uint256 second) public { + first = bound(first, 1, 500_000 ether); + second = bound(second, 0, 1_000_000 ether); + uint256 deposit = second > first ? second : first; + _helperReceiveIncentive(deposit); + + bytes memory zeroPubkey = + hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + vm.startPrank(bgtIncentiveReceiverManager); + BGTIncentiveDistributor(bgtIncentiveDistributor).allocateIncentiveToValidator(address(token), first); + BGTIncentiveDistributor(bgtIncentiveDistributor).allocateIncentiveToValidator(address(token), second); + vm.stopPrank(); + + assertEq( + BGTIncentiveDistributor(bgtIncentiveDistributor).incentiveTokensPerValidator(zeroPubkey, address(token)), + second + ); + } + + function test_AllocateIncentiveToValidator_Revert_NotManager() public { + _helperReceiveIncentive(1 ether); + + address notManager = makeAddr("notManager"); + vm.prank(notManager); + vm.expectRevert(); + BGTIncentiveDistributor(bgtIncentiveDistributor).allocateIncentiveToValidator(address(token), 1 ether); + } + + function test_AllocateIncentiveToValidator_Revert_InsufficientBalance(uint256 amount) public { + amount = bound(amount, 1, 1_000_000 ether); + _helperReceiveIncentive(amount - 1); + + vm.prank(bgtIncentiveReceiverManager); + vm.expectRevert(IPOLErrors.InsufficientIncentiveTokens.selector); + BGTIncentiveDistributor(bgtIncentiveDistributor).allocateIncentiveToValidator(address(token), amount); + } + function _helperReceiveIncentive(uint256 amount) internal { token.mint(address(this), amount); token.approve(address(bgtIncentiveDistributor), amount); diff --git a/test/pol/BeaconRootsHelper.t.sol b/test/pol/BeaconRootsHelper.t.sol index 4a598fe..13a5cf6 100644 --- a/test/pol/BeaconRootsHelper.t.sol +++ b/test/pol/BeaconRootsHelper.t.sol @@ -28,7 +28,7 @@ abstract contract BeaconRootsHelperTest is POLTest { assertEq(address(distributor.beraChef()), address(beraChef)); assertEq(address(distributor.blockRewardController()), address(blockRewardController)); - assertEq(address(distributor.bgt()), address(bgt)); + assertEq(address(distributor.emissionToken()), address(bgt)); // Mock calls to BeaconRoots.ADDRESS to use our mock contract. vm.etch(BeaconRoots.ADDRESS, address(new Mock4788BeaconRoots()).code); @@ -37,19 +37,22 @@ abstract contract BeaconRootsHelperTest is POLTest { mockBeaconRoots.setMockBeaconBlockRoot(valData.beaconBlockRoot); vm.startPrank(governance); - // Set the reward rate to be 5 bgt per block. - blockRewardController.setRewardRate(TEST_BGT_PER_BLOCK); - // Set the min boosted reward rate to be 5 bgt per block. - blockRewardController.setMinBoostedRewardRate(TEST_BGT_PER_BLOCK); - // Allow the distributor to send BGT. bgt.whitelistSender(address(distributor), true); // Setup the reward allocation and vault for the honey token. honey = new MockHoney(); - vault = RewardVault(factory.createRewardVault(address(honey))); + vault = RewardVault(payable(factory.createRewardVault(address(honey)))); vm.stopPrank(); + // Fund the BlockRewardController with native tokens for the WBERA wrapping flow. + vm.deal(address(blockRewardController), 100 ether); + // Pre-fund the distributor with BGT so it can distribute to vaults. + vm.deal(address(bgt), address(bgt).balance + 100 ether); + + vm.prank(address(blockRewardController)); + bgt.mint(address(distributor), 100 ether); + if (initDefaultRewardAllocation) { helper_SetDefaultRewardAllocation(); } diff --git a/test/pol/BlockRewardController.t.sol b/test/pol/BlockRewardController.t.sol index d6efcfc..d637036 100644 --- a/test/pol/BlockRewardController.t.sol +++ b/test/pol/BlockRewardController.t.sol @@ -7,8 +7,9 @@ import { ERC1967Utils } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils import { IBGT } from "src/pol/interfaces/IBGT.sol"; import { IBlockRewardController, IPOLErrors } from "src/pol/interfaces/IBlockRewardController.sol"; import { BlockRewardController } from "src/pol/rewards/BlockRewardController.sol"; +import { WBERA } from "src/WBERA.sol"; -import { BeaconDepositMock, POLTest } from "./POL.t.sol"; +import { BeaconDepositMock, POLTest, Vm } from "./POL.t.sol"; contract BlockRewardControllerTest is POLTest { /// @dev Ensure that the contract is owned by the governance. @@ -24,9 +25,6 @@ contract BlockRewardControllerTest is POLTest { vm.expectRevert(); blockRewardController.setDistributor(address(1)); - vm.expectRevert(); - blockRewardController.setRewardRate(255); - address newImpl = address(new BlockRewardController()); vm.expectRevert(); blockRewardController.upgradeToAndCall(newImpl, bytes("")); @@ -51,6 +49,29 @@ contract BlockRewardControllerTest is POLTest { blockRewardController.initialize(address(bgt), address(distributor), address(beraChef), address(governance)); } + /// @dev V2 reinitializer should fail if called again + function test_InitializeV2_FailIfCalledAgain() public { + vm.prank(governance); + vm.expectRevert(); + blockRewardController.initialize(); + } + + /// @dev V2 reinitializer wires WBERA and zeroes deprecated fields; rates are now constants. + function test_InitializeV2() public { + BlockRewardController freshController = new BlockRewardController(); + BlockRewardController freshProxy = + BlockRewardController(payable(deployProxyWithCreate2(address(freshController), 99))); + + freshProxy.initialize(address(bgt), address(distributor), beaconDepositContract, governance); + vm.prank(governance); + freshProxy.initialize(); + + assertEq(address(freshProxy.wbera()), address(wbera)); + assertEq(freshProxy.baseRate(), 0.4e18); + assertEq(freshProxy.rewardRate(), 1.305e18); + assertEq(freshProxy.VERSION(), 2); + } + function test_SetDistributor_FailIfZeroAddress() public { vm.prank(governance); vm.expectRevert(IPOLErrors.ZeroAddress.selector); @@ -67,270 +88,217 @@ contract BlockRewardControllerTest is POLTest { assertEq(blockRewardController.distributor(), _distributor); } - /// @dev Ensure that the base rate is set - function test_SetBaseRate() public { - testFuzz_SetBaseRate(1 ether); - } - - /// @dev Ensure that the reward rate is set - function test_SetRewardRate() public { - testFuzz_SetRewardRate(1 ether); + /// @dev Should fail if not the distributor + function test_FailIfNotDistributor() public { + vm.expectRevert(); + blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true); } - /// @dev Ensure that min boosted reward rate is also set - function test_SetMinBoostedRewardRate() public { - testFuzz_SetMinBoostedRewardRate(0.1 ether); - } + /// @dev Should process rewards via native balance, sending emission token to operator and distributor + function test_ProcessRewards() public { + test_SetDistributor(); - /// @dev Ensure that boost multiplier is also set - function test_SetBoostMultiplier() public { - testFuzz_SetBoostMultiplier(3 ether); - } + uint256 baseRate = blockRewardController.baseRate(); + uint256 rewardRate = blockRewardController.rewardRate(); + vm.deal(address(blockRewardController), baseRate + rewardRate); - /// @dev Ensure that reward convexity is also set - function test_SetRewardConvexity() public { - testFuzz_SetRewardConvexity(0.5 ether); - } + uint256 operatorEmissionTokenBefore = emissionToken.balanceOf(operator); + uint256 distributorEmissionTokenBefore = emissionToken.balanceOf(address(distributor)); - /// @dev Parameterized setter for the base rate - function testFuzz_SetBaseRate(uint256 baseRate) public { - baseRate = _bound(baseRate, 0, blockRewardController.MAX_BASE_RATE()); - vm.prank(governance); + vm.prank(address(distributor)); vm.expectEmit(true, true, true, true); - emit IBlockRewardController.BaseRateChanged(0, baseRate); - blockRewardController.setBaseRate(baseRate); - assertEq(blockRewardController.baseRate(), baseRate); - } + emit IBlockRewardController.BlockRewardProcessed( + valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, baseRate, rewardRate + ); - /// @dev Parameterized setter for the reward rate - function testFuzz_SetRewardRate(uint256 rewardRate) public { - rewardRate = _bound(rewardRate, 0, blockRewardController.MAX_REWARD_RATE()); - vm.prank(governance); - vm.expectEmit(true, true, true, true); - emit IBlockRewardController.RewardRateChanged(0, rewardRate); - blockRewardController.setRewardRate(rewardRate); - assertEq(blockRewardController.rewardRate(), rewardRate); - } + assertEq(blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true), rewardRate); - /// @dev Parameterized setter for min boosted reward rate - function testFuzz_SetMinBoostedRewardRate(uint256 min) public { - min = _bound(min, 0, blockRewardController.MAX_MIN_BOOSTED_REWARD_RATE()); - vm.prank(governance); - vm.expectEmit(true, true, true, true); - emit IBlockRewardController.MinBoostedRewardRateChanged(0, min); - blockRewardController.setMinBoostedRewardRate(min); - assertEq(blockRewardController.minBoostedRewardRate(), min); + assertEq(emissionToken.balanceOf(operator) - operatorEmissionTokenBefore, baseRate); + assertEq(emissionToken.balanceOf(address(distributor)) - distributorEmissionTokenBefore, rewardRate); } - /// @dev Parameterized setter for boost multiplier - function testFuzz_SetBoostMultiplier(uint256 multiplier) public { - multiplier = _bound(multiplier, 0, blockRewardController.MAX_BOOST_MULTIPLIER()); - vm.prank(governance); - vm.expectEmit(true, true, true, true); - emit IBlockRewardController.BoostMultiplierChanged(0, multiplier); - blockRewardController.setBoostMultiplier(multiplier); - assertEq(blockRewardController.boostMultiplier(), multiplier); - } + /// @dev Should process rewards via BGT mint/redeem when no native balance + function test_ProcessRewards_ViaMintRedeem() public { + test_SetDistributor(); - /// @dev Parameterized setter for reward convexity - function testFuzz_SetRewardConvexity(uint256 convexity) public { - convexity = _bound(convexity, 1, blockRewardController.MAX_REWARD_CONVEXITY()); - vm.prank(governance); - vm.expectEmit(true, true, true, true); - emit IBlockRewardController.RewardConvexityChanged(0, convexity); - blockRewardController.setRewardConvexity(convexity); - assertEq(uint256(blockRewardController.rewardConvexity()), convexity); - } + uint256 baseRate = blockRewardController.baseRate(); + uint256 rewardRate = blockRewardController.rewardRate(); - /// @dev Should fail if not the distributor - function test_FailIfNotDistributor() public { - vm.expectRevert(); - blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true); - } + // No native balance on the controller; fund BGT so mint/redeem can produce native tokens. + vm.deal(address(bgt), address(bgt).balance + baseRate + rewardRate); - /// @dev Should process zero rewards - function test_ProcessZeroRewards() public { - test_SetDistributor(); + uint256 operatorEmissionTokenBefore = emissionToken.balanceOf(operator); + uint256 distributorEmissionTokenBefore = emissionToken.balanceOf(address(distributor)); vm.prank(address(distributor)); vm.expectEmit(true, true, true, true); - emit IBlockRewardController.BlockRewardProcessed(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, 0, 0); - assertEq(blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true), 0); + emit IBlockRewardController.BlockRewardProcessed( + valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, baseRate, rewardRate + ); + + assertEq(blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true), rewardRate); + + assertEq(emissionToken.balanceOf(operator) - operatorEmissionTokenBefore, baseRate); + assertEq(emissionToken.balanceOf(address(distributor)) - distributorEmissionTokenBefore, rewardRate); } - /// @dev Should process rewards - function test_ProcessRewards() public { + /// @dev Fuzz over native balance to exercise both the direct-wrap and the BGT mint/redeem + /// fallback paths in _handleMinting. + function testFuzz_ProcessRewards(uint256 nativeBalance, uint256 boostVal0, uint256 boostVal1) public { + bytes memory valPubkey1 = "validator 1 pubkey"; + address operator1 = makeAddr("operator"); + BeaconDepositMock(beaconDepositContract).setOperator(valPubkey1, operator1); + test_SetDistributor(); - test_SetBaseRate(); - test_SetRewardRate(); - test_SetMinBoostedRewardRate(); - test_SetBoostMultiplier(); - test_SetRewardConvexity(); - // @dev should process min reward given no boosts + uint256 baseRate = blockRewardController.baseRate(); + uint256 rewardRate = blockRewardController.rewardRate(); + uint256 totalNeeded = baseRate + rewardRate; + nativeBalance = _bound(nativeBalance, 0, totalNeeded); + + vm.deal(address(blockRewardController), nativeBalance); + // Fund BGT contract to cover mint/redeem for any shortfall. + uint256 mintShortfall = totalNeeded - nativeBalance; + if (mintShortfall > 0) { + vm.deal(address(bgt), address(bgt).balance + mintShortfall); + } + + _helper_Boost(address(0x2), boostVal0, valData.pubkey); + _helper_Boost(address(0x3), boostVal1, valPubkey1); + + uint256 bgtSupplyBefore = bgt.totalSupply(); + uint256 operatorEmissionTokenBefore = emissionToken.balanceOf(operator); + uint256 distributorEmissionTokenBefore = emissionToken.balanceOf(address(distributor)); + vm.prank(address(distributor)); - vm.expectEmit(true, true, true, true); - emit IBlockRewardController.BlockRewardProcessed(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, 1 ether, 0.1 ether); + uint256 reward = blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true); + + assertEq(reward, rewardRate); + assertEq(emissionToken.balanceOf(operator) - operatorEmissionTokenBefore, baseRate); + assertEq(emissionToken.balanceOf(address(distributor)) - distributorEmissionTokenBefore, rewardRate); - // expect calls to mint BGT to the distributor and operator - vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (operator, 1.0 ether))); - vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (address(distributor), 0.1 ether))); - assertEq(blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true), 0.1 ether); + // BGT supply should remain unchanged: any minted BGT is immediately redeemed. + assertEq(bgt.totalSupply(), bgtSupplyBefore, "BGT supply should be unchanged after mint/redeem cycle"); } - /// @dev Should process the maximum number of rewards without reverting (100% boost to the validator) - function test_ProcessRewardsMax() public { - _helper_ControllerSetters(1.5 ether, 0, 3 ether, 0.5 ether); - _helper_Boost(address(0x1), 1 ether, valData.pubkey); + /// @dev When the controller has enough native balance, _handleMinting should NOT call bgt.mint/redeem + function test_ProcessRewards_SkipsMintRedeemWhenBalanceSufficient() public { + test_SetDistributor(); + + uint256 baseRate = blockRewardController.baseRate(); + uint256 rewardRate = blockRewardController.rewardRate(); + vm.deal(address(blockRewardController), baseRate + rewardRate); + uint256 bgtSupplyBefore = bgt.totalSupply(); - // @dev should process max reward given 100% boosts to the user vm.prank(address(distributor)); - vm.expectEmit(true, true, true, true); - emit IBlockRewardController.BlockRewardProcessed(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, 1 ether, 4.5 ether); + blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true); - // expect calls to mint BGT to the distributor and operator - vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (operator, 1.0 ether))); - vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (address(distributor), 4.5 ether))); - assertEq(blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true), 4.5 ether); + assertEq(bgt.totalSupply(), bgtSupplyBefore, "BGT supply should not change when balance is sufficient"); + assertEq(emissionToken.balanceOf(operator), baseRate); + assertEq(emissionToken.balanceOf(address(distributor)), rewardRate); } - /// @dev Should process the minimum number of rewards without reverting (0% boost to the validator) - function test_ProcessRewardsMin() public { - _helper_ControllerSetters(1.5 ether, 0.1 ether, 3 ether, 0.5 ether); + /// @dev When the controller has insufficient native balance, _handleMinting should mint BGT and redeem for native + function test_ProcessRewards_MintRedeemWhenBalanceInsufficient() public { + test_SetDistributor(); + + uint256 baseRate = blockRewardController.baseRate(); + uint256 rewardRate = blockRewardController.rewardRate(); + vm.deal(address(bgt), address(bgt).balance + baseRate + rewardRate); - // @dev should process min reward given no boosts vm.prank(address(distributor)); - vm.expectEmit(true, true, true, true); - emit IBlockRewardController.BlockRewardProcessed(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, 1 ether, 0.1 ether); + vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (address(blockRewardController), baseRate))); + vm.expectCall(address(bgt), abi.encodeCall(IBGT.redeem, (address(blockRewardController), baseRate))); + vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (address(blockRewardController), rewardRate))); + vm.expectCall(address(bgt), abi.encodeCall(IBGT.redeem, (address(blockRewardController), rewardRate))); + blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true); - // expect calls to mint BGT to the distributor and operator - vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (operator, 1.0 ether))); - vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (address(distributor), 0.1 ether))); - assertEq(blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true), 0.1 ether); + assertEq(emissionToken.balanceOf(operator), baseRate); + assertEq(emissionToken.balanceOf(address(distributor)), rewardRate); } - /// @dev Should compute rewards correctly (expected values are computed offline) - function test_ComputeRewards() public view { - uint256 rewardRate = 1.5 ether; - uint256 multiplier = 3 ether; - int256 convexity = 0.5 ether; - - uint256 maxDelta = 0.000_01 ether; + /// @dev When the controller has partial balance, only the shortfall should trigger mint/redeem + function test_ProcessRewards_PartialBalance() public { + test_SetDistributor(); - // check for different values of boosts + uint256 baseRate = blockRewardController.baseRate(); + uint256 rewardRate = blockRewardController.rewardRate(); + vm.deal(address(blockRewardController), baseRate); + vm.deal(address(bgt), address(bgt).balance + rewardRate); - uint256 reward = blockRewardController.computeReward(0, rewardRate, multiplier, convexity); - assertEq(reward, 0); + vm.prank(address(distributor)); + blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true); - reward = blockRewardController.computeReward(1 ether, rewardRate, multiplier, convexity); - uint256 expected = 4.5 ether; - assertEq(reward, expected); + assertEq(emissionToken.balanceOf(operator), baseRate); + assertEq(emissionToken.balanceOf(address(distributor)), rewardRate); + } - reward = blockRewardController.computeReward(0.1 ether, rewardRate, multiplier, convexity); - expected = 2.921 ether; - assertApproxEqAbs(reward, expected, maxDelta); + /// @dev When isReady=false, only baseRate goes to operator, nothing to distributor + function test_ProcessRewards_OnlyBaseRateWhenNotReady() public { + test_SetDistributor(); - reward = blockRewardController.computeReward(0.01 ether, rewardRate, multiplier, convexity); - expected = 1.384_62 ether; - assertApproxEqAbs(reward, expected, maxDelta); + uint256 baseRate = blockRewardController.baseRate(); + vm.deal(address(blockRewardController), baseRate); - reward = blockRewardController.computeReward(0.95 ether, rewardRate, multiplier, convexity); - expected = 4.470_96 ether; - assertApproxEqAbs(reward, expected, maxDelta); + vm.prank(address(distributor)); + vm.expectEmit(true, true, true, true); + emit IBlockRewardController.BlockRewardProcessed(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, baseRate, 0); + uint256 reward = blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, false); - // check for different values of convexity + assertEq(reward, 0); + assertEq(emissionToken.balanceOf(operator), baseRate); + assertEq(emissionToken.balanceOf(address(distributor)), 0); + } - reward = blockRewardController.computeReward(0, rewardRate, multiplier, 0); - expected = 0; // should be max, but look at docs inside the function - assertEq(reward, expected); + /// @dev Controller's native balance should decrease by the total minted amount + function test_ProcessRewards_NativeBalanceDecreasesAfterMinting() public { + test_SetDistributor(); - reward = blockRewardController.computeReward(0, rewardRate, multiplier, 1); - expected = 0; - assertEq(reward, expected); + uint256 totalRate = blockRewardController.baseRate() + blockRewardController.rewardRate(); + vm.deal(address(blockRewardController), 10 ether); + uint256 balanceBefore = address(blockRewardController).balance; - // for any boost value > 0, if reward convexity is small reward is almost flat on max value (minus delta) - reward = blockRewardController.computeReward(1, rewardRate, multiplier, 0); - expected = 4.5 ether; - assertApproxEqAbs(reward, expected, maxDelta); + vm.prank(address(distributor)); + blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true); - reward = blockRewardController.computeReward(1, rewardRate, multiplier, 0.000_000_1 ether); - expected = 4.5 ether; - assertApproxEqAbs(reward, expected, maxDelta); + assertEq(balanceBefore - address(blockRewardController).balance, totalRate); } - /// @dev Should bound compute rewards correctly to its theoretical limits - function testFuzz_ComputeRewards( - uint256 boostPower, - uint256 rewardRate, - uint256 multiplier, - uint256 _convexity - ) - public - view - { - rewardRate = _bound(rewardRate, 0, blockRewardController.MAX_REWARD_RATE()); - boostPower = _bound(boostPower, 0, 1 ether); - multiplier = _bound(multiplier, 0, blockRewardController.MAX_BOOST_MULTIPLIER()); - int256 convexity = int256(_bound(_convexity, 0, blockRewardController.MAX_REWARD_CONVEXITY())); - - uint256 reward = blockRewardController.computeReward(boostPower, rewardRate, multiplier, convexity); - uint256 maxReward = multiplier * rewardRate / 1e18; - assertGe(reward, 0); - assertLe(reward, maxReward); + /// @dev getMaxBGTPerBlock returns BASE_RATE + REWARD_RATE + function test_GetMaxBGTPerBlock() public view { + assertEq( + blockRewardController.getMaxBGTPerBlock(), + blockRewardController.baseRate() + blockRewardController.rewardRate() + ); } - /// @dev Should process rewards without reverting (boost distributed among 2 validators) - function testFuzz_ProcessRewards( - uint256 rewardRate, - uint256 minReward, - uint256 multiplier, - uint256 convexity, - uint256 boostVal0, - uint256 boostVal1 - ) - public - { - rewardRate = _bound(rewardRate, 0, blockRewardController.MAX_REWARD_RATE()); - minReward = _bound(minReward, 0, blockRewardController.MAX_MIN_BOOSTED_REWARD_RATE()); - multiplier = _bound(multiplier, 0, blockRewardController.MAX_BOOST_MULTIPLIER()); - convexity = _bound(convexity, 0, blockRewardController.MAX_REWARD_CONVEXITY()); - - bytes memory valPubkey1 = "validator 1 pubkey"; - address operator1 = makeAddr("operator"); - BeaconDepositMock(beaconDepositContract).setOperator(valPubkey1, operator1); + function test_BurnExceedingBalance_OnlyDistributor() public { + vm.expectRevert(IPOLErrors.NotDistributor.selector); + blockRewardController.burnExceedingBalance(); + } - _helper_ControllerSetters(rewardRate, minReward, multiplier, convexity); - _helper_Boost(address(0x2), boostVal0, valData.pubkey); - _helper_Boost(address(0x3), boostVal1, valPubkey1); + function test_BurnExceedingBalance() public { + test_SetDistributor(); + vm.deal(address(blockRewardController), 3 ether); vm.prank(address(distributor)); - // expect calls to mint BGT to the distributor - vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (operator, 1.0 ether))); - // expect reward between formula's min and max - uint256 reward = blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true); - assertGe(reward, minReward); - uint256 maxReward = multiplier * rewardRate / 1e18; - maxReward = maxReward > minReward ? maxReward : minReward; + vm.expectEmit(true, true, true, true); + emit IBlockRewardController.ExceedingBalanceBurnt(3 ether); + blockRewardController.burnExceedingBalance(); - assertLe(reward, maxReward); + assertEq(address(blockRewardController).balance, 0); } - function _helper_ControllerSetters( - uint256 rewardRate, - uint256 minReward, - uint256 multiplier, - uint256 convexity - ) - internal - { + function test_BurnExceedingBalance_ZeroBalance() public { test_SetDistributor(); - test_SetBaseRate(); - testFuzz_SetRewardRate(rewardRate); - testFuzz_SetMinBoostedRewardRate(minReward); - testFuzz_SetBoostMultiplier(multiplier); - testFuzz_SetRewardConvexity(convexity); - vm.deal(address(bgt), address(bgt).balance + rewardRate * multiplier / 1e18); // add max bgt minted in a block + vm.recordLogs(); + vm.prank(address(distributor)); + blockRewardController.burnExceedingBalance(); + + // No ExceedingBalanceBurnt event should have been emitted + Vm.Log[] memory logs = vm.getRecordedLogs(); + assertEq(logs.length, 0); } function _helper_Mint(address user, uint256 amount) internal { diff --git a/test/pol/DedicatedEmissionStreamManager.t.sol b/test/pol/DedicatedEmissionStreamManager.t.sol index cbca6c1..f77874e 100644 --- a/test/pol/DedicatedEmissionStreamManager.t.sol +++ b/test/pol/DedicatedEmissionStreamManager.t.sol @@ -275,7 +275,7 @@ contract DedicatedEmissionStreamManagerTest is DistributorTest { DISTRIBUTE_FOR_TIMESTAMP, valData.index, valData.pubkey, valData.proposerIndexProof, valData.pubkeyProof ); - assertEq(bgt.allowance(address(distributor), address(vault)), TEST_BGT_PER_BLOCK); + assertEq(bgt.allowance(address(distributor), address(vault)), blockRewardController.rewardRate()); assertEq(bgt.allowance(address(distributor), graVault1), 0); assertEq(bgt.allowance(address(distributor), graVault2), 0); assertEq(dedicatedEmissionStreamManager.debt(graVault1), 0); @@ -296,7 +296,7 @@ contract DedicatedEmissionStreamManagerTest is DistributorTest { DISTRIBUTE_FOR_TIMESTAMP, valData.index, valData.pubkey, valData.proposerIndexProof, valData.pubkeyProof ); - assertEq(bgt.allowance(address(distributor), address(vault)), TEST_BGT_PER_BLOCK); + assertEq(bgt.allowance(address(distributor), address(vault)), blockRewardController.rewardRate()); assertEq(bgt.allowance(address(distributor), graVault1), 0); assertEq(bgt.allowance(address(distributor), graVault2), 0); assertEq(dedicatedEmissionStreamManager.debt(graVault1), 0); @@ -312,8 +312,9 @@ contract DedicatedEmissionStreamManagerTest is DistributorTest { IRewardAllocation.Weight[] memory rewardAllocation = dedicatedEmissionStreamManager.getRewardAllocation(); - uint256 bgtToRewardAllocation = TEST_BGT_PER_BLOCK * 1000 / 10_000; // 0.5 ether - uint256 bgtToDefaultRewardAllocation = TEST_BGT_PER_BLOCK - bgtToRewardAllocation; // 4.5 ether + uint256 bgtToRewardAllocation = blockRewardController.rewardRate() * 1000 / 10_000; // 0.5 ether + uint256 bgtToDefaultRewardAllocation = blockRewardController.rewardRate() - bgtToRewardAllocation; // 4.5 + // ether vm.expectEmit(true, true, true, true); // expect events for notifying the emission and distributing the rewards to the reward allocation vaults @@ -342,15 +343,15 @@ contract DedicatedEmissionStreamManagerTest is DistributorTest { function test_DistributeWithDedicatedEmissionStream_StopDistributing() public { helper_SetDefaultRewardAllocation(); - // Percentage 10% - // Emission target 0.25 ether for each vault - // This will stop distributing to the reward allocation vaults after the first distribution. - _helper_setUpRewardAllocation(1000, 0.25 ether); + // Percentage 10%, two receivers split 50/50 → each receives bgtToRewardAllocation / 2 per block. + // Target = bgtToRewardAllocation / 2 stops distributing after the first block. + uint256 perReceiverTarget = blockRewardController.rewardRate() / 20; + _helper_setUpRewardAllocation(1000, perReceiverTarget); IRewardAllocation.Weight[] memory rewardAllocation = dedicatedEmissionStreamManager.getRewardAllocation(); - uint256 bgtToRewardAllocation = TEST_BGT_PER_BLOCK * 1000 / 10_000; // 0.5 ether - uint256 bgtToDefaultRewardAllocation = TEST_BGT_PER_BLOCK - bgtToRewardAllocation; // 4.5 ether + uint256 bgtToRewardAllocation = blockRewardController.rewardRate() * 1000 / 10_000; + uint256 bgtToDefaultRewardAllocation = blockRewardController.rewardRate() - bgtToRewardAllocation; for (uint8 i; i < 2; ++i) { if (i == 0) { @@ -372,7 +373,7 @@ contract DedicatedEmissionStreamManagerTest is DistributorTest { vm.expectEmit(true, true, true, true); // vault receives 4.5 ether emit IDistributor.Distributed( - valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP + i, address(vault), TEST_BGT_PER_BLOCK + valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP + i, address(vault), blockRewardController.rewardRate() ); } @@ -386,41 +387,33 @@ contract DedicatedEmissionStreamManagerTest is DistributorTest { } assertEq( - bgt.allowance(address(distributor), address(vault)), bgtToDefaultRewardAllocation + TEST_BGT_PER_BLOCK + bgt.allowance(address(distributor), address(vault)), + bgtToDefaultRewardAllocation + blockRewardController.rewardRate() ); for (uint256 i; i < rewardAllocation.length; ++i) { - assertEq(bgt.allowance(address(distributor), rewardAllocation[i].receiver), 0.25 ether); - assertEq(dedicatedEmissionStreamManager.debt(rewardAllocation[i].receiver), 0.25 ether); + assertEq(bgt.allowance(address(distributor), rewardAllocation[i].receiver), perReceiverTarget); + assertEq(dedicatedEmissionStreamManager.debt(rewardAllocation[i].receiver), perReceiverTarget); } } function test_DistributeWithDedicatedEmissionStream_StopDistributing_DifferentTargetEmissions() public { helper_SetDefaultRewardAllocation(); + // 10% pct, two receivers split 50/50 → each receives perBlock per block. + uint256 perBlock = blockRewardController.rewardRate() / 20; vm.startPrank(allocationManager); - dedicatedEmissionStreamManager.setEmissionPerc(1000); // 10% + dedicatedEmissionStreamManager.setEmissionPerc(1000); dedicatedEmissionStreamManager.setRewardAllocation(_helper_getWeights()); - dedicatedEmissionStreamManager.setTargetEmission(graVault1, 0.25 ether); - dedicatedEmissionStreamManager.setTargetEmission(graVault2, 0.5 ether); + dedicatedEmissionStreamManager.setTargetEmission(graVault1, perBlock); // 1 block worth + dedicatedEmissionStreamManager.setTargetEmission(graVault2, perBlock * 2); // 2 blocks worth vm.stopPrank(); - uint256 bgtToRewardAllocation = TEST_BGT_PER_BLOCK * 1000 / 10_000; // 0.5 ether - uint256 bgtToDefaultRewardAllocation = TEST_BGT_PER_BLOCK - bgtToRewardAllocation; // 4.5 ether - - // 1st distribution - // -- graVault1 -> 0.25 ether - // -- graVault2 -> 0.25 ether - // -- vault -> 4.5 ether + uint256 bgtToRewardAllocation = blockRewardController.rewardRate() * 1000 / 10_000; + uint256 bgtToDefaultRewardAllocation = blockRewardController.rewardRate() - bgtToRewardAllocation; - // 2nd distribution - // -- graVault1 -> 0 - // -- graVault2 -> 0.25 ether - // -- vault -> 4.75 ether - - // 3rd distribution - // -- graVault1 -> 0 - // -- graVault2 -> 0 - // -- vault -> 5 ether + // 1st distribution: graVault1 +perBlock, graVault2 +perBlock, vault +bgtToDefaultRewardAllocation + // 2nd distribution: graVault1 capped, graVault2 +perBlock, vault +(bgtToDefaultRewardAllocation + perBlock) + // 3rd distribution: both capped, vault +blockRewardController.rewardRate() for (uint8 i; i < 3; ++i) { distributor.distributeFor( DISTRIBUTE_FOR_TIMESTAMP + i, @@ -433,31 +426,32 @@ contract DedicatedEmissionStreamManagerTest is DistributorTest { assertEq( bgt.allowance(address(distributor), address(vault)), - bgtToDefaultRewardAllocation * 2 + 0.25 ether + TEST_BGT_PER_BLOCK + bgtToDefaultRewardAllocation * 2 + perBlock + blockRewardController.rewardRate() ); - assertEq(bgt.allowance(address(distributor), graVault1), 0.25 ether); - assertEq(bgt.allowance(address(distributor), graVault2), 0.5 ether); - assertEq(dedicatedEmissionStreamManager.debt(graVault1), 0.25 ether); - assertEq(dedicatedEmissionStreamManager.debt(graVault2), 0.5 ether); + assertEq(bgt.allowance(address(distributor), graVault1), perBlock); + assertEq(bgt.allowance(address(distributor), graVault2), perBlock * 2); + assertEq(dedicatedEmissionStreamManager.debt(graVault1), perBlock); + assertEq(dedicatedEmissionStreamManager.debt(graVault2), perBlock * 2); } function test_DistributeWithDedicatedEmissionStream_RestartDistributing() public { test_DistributeWithDedicatedEmissionStream_StopDistributing_DifferentTargetEmissions(); + uint256 perBlock = blockRewardController.rewardRate() / 20; + uint256 bgtToRewardAllocation = blockRewardController.rewardRate() * 1000 / 10_000; + uint256 bgtToDefaultRewardAllocation = blockRewardController.rewardRate() - bgtToRewardAllocation; + // Increase the target emission for the vaults after the first target was reached. vm.startPrank(allocationManager); - dedicatedEmissionStreamManager.setTargetEmission(graVault1, 0.5 ether); // 0.25 ether -> 0.5 ether - dedicatedEmissionStreamManager.setTargetEmission(graVault2, 0.75 ether); // 0.5 ether -> 0.75 ether + dedicatedEmissionStreamManager.setTargetEmission(graVault1, perBlock * 2); + dedicatedEmissionStreamManager.setTargetEmission(graVault2, perBlock * 3); vm.stopPrank(); - uint256 graVault1PreviousDebt = dedicatedEmissionStreamManager.debt(graVault1); // 0.25 ether - uint256 graVault2PreviousDebt = dedicatedEmissionStreamManager.debt(graVault2); // 0.5 ether - uint256 vaultPreviousAllowance = bgt.allowance(address(distributor), address(vault)); // 14.25 ether + uint256 graVault1PreviousDebt = dedicatedEmissionStreamManager.debt(graVault1); + uint256 graVault2PreviousDebt = dedicatedEmissionStreamManager.debt(graVault2); + uint256 vaultPreviousAllowance = bgt.allowance(address(distributor), address(vault)); - // Distribute again - // -- graVault1 -> 0.25 ether - // -- graVault2 -> 0.25 ether - // -- vault -> 4.5 ether + // Distribute again: each gra vault has 1 block of headroom, vault gets bgtToDefaultRewardAllocation. distributor.distributeFor( DISTRIBUTE_FOR_TIMESTAMP + 3, valData.index, @@ -466,11 +460,13 @@ contract DedicatedEmissionStreamManagerTest is DistributorTest { valData.pubkeyProof ); - assertEq(bgt.allowance(address(distributor), address(vault)), vaultPreviousAllowance + 4.5 ether); - assertEq(bgt.allowance(address(distributor), graVault1), graVault1PreviousDebt + 0.25 ether); - assertEq(bgt.allowance(address(distributor), graVault2), graVault2PreviousDebt + 0.25 ether); - assertEq(dedicatedEmissionStreamManager.debt(graVault1), graVault1PreviousDebt + 0.25 ether); - assertEq(dedicatedEmissionStreamManager.debt(graVault2), graVault2PreviousDebt + 0.25 ether); + assertEq( + bgt.allowance(address(distributor), address(vault)), vaultPreviousAllowance + bgtToDefaultRewardAllocation + ); + assertEq(bgt.allowance(address(distributor), graVault1), graVault1PreviousDebt + perBlock); + assertEq(bgt.allowance(address(distributor), graVault2), graVault2PreviousDebt + perBlock); + assertEq(dedicatedEmissionStreamManager.debt(graVault1), graVault1PreviousDebt + perBlock); + assertEq(dedicatedEmissionStreamManager.debt(graVault2), graVault2PreviousDebt + perBlock); } function test_DistributeWithDedicatedEmissionStream_OneHundredPercent() public { @@ -488,10 +484,10 @@ contract DedicatedEmissionStreamManagerTest is DistributorTest { ); assertEq(bgt.allowance(address(distributor), address(vault)), 0); - assertEq(bgt.allowance(address(distributor), graVault1), TEST_BGT_PER_BLOCK / 2); - assertEq(bgt.allowance(address(distributor), graVault2), TEST_BGT_PER_BLOCK / 2); - assertEq(dedicatedEmissionStreamManager.debt(graVault1), TEST_BGT_PER_BLOCK / 2); - assertEq(dedicatedEmissionStreamManager.debt(graVault2), TEST_BGT_PER_BLOCK / 2); + assertEq(bgt.allowance(address(distributor), graVault1), blockRewardController.rewardRate() / 2); + assertEq(bgt.allowance(address(distributor), graVault2), blockRewardController.rewardRate() / 2); + assertEq(dedicatedEmissionStreamManager.debt(graVault1), blockRewardController.rewardRate() / 2); + assertEq(dedicatedEmissionStreamManager.debt(graVault2), blockRewardController.rewardRate() / 2); } /// @dev target emission will be the same for all vaults diff --git a/test/pol/DistributeForGasUsage.t.sol b/test/pol/DistributeForGasUsage.t.sol index 5d10afb..433749b 100644 --- a/test/pol/DistributeForGasUsage.t.sol +++ b/test/pol/DistributeForGasUsage.t.sol @@ -46,10 +46,6 @@ contract DistributeForGasUsageTest is BeaconRootsHelperTest { assertEq(ra.weights.length, NUMBER_OF_WEIGHTS); assertEq(incentiveTokens.length, NUMBER_OF_WEIGHTS * NUMBER_OF_INCENTIVE_TOKENS); - // expect a call to mint the BGT to the distributor - bytes memory data = abi.encodeCall(IBGT.mint, (address(distributor), TEST_BGT_PER_BLOCK)); - vm.expectCall(address(bgt), data, 1); - distributor.distributeFor( DISTRIBUTE_FOR_TIMESTAMP, valData.index, valData.pubkey, valData.proposerIndexProof, valData.pubkeyProof ); @@ -70,9 +66,6 @@ contract DistributeForGasUsageTest is BeaconRootsHelperTest { IBlockRewardController.processRewards, (valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP + 2, true) ); vm.expectCall(address(blockRewardController), data, 1); - // expect 3 calls to mint the BGT to the distributor - data = abi.encodeCall(IBGT.mint, (address(distributor), TEST_BGT_PER_BLOCK)); - vm.expectCall(address(bgt), data, 3); // call distributeFor 3 times in a single multicall bytes[] memory callData = new bytes[](3); @@ -114,7 +107,7 @@ contract DistributeForGasUsageTest is BeaconRootsHelperTest { } function _helper_CreateRewardVault() internal returns (address) { - RewardVault vault = RewardVault(factory.createRewardVault(_helper_CreateStakingToken())); + RewardVault vault = RewardVault(payable(factory.createRewardVault(_helper_CreateStakingToken()))); vm.prank(governance); vault.setMaxIncentiveTokensCount(uint8(NUMBER_OF_INCENTIVE_TOKENS)); return address(vault); @@ -141,7 +134,7 @@ contract DistributeForGasUsageTest is BeaconRootsHelperTest { } function _helper_WhitelistIncentiveTokens(address vault) public { - uint256 count = RewardVault(vault).maxIncentiveTokensCount(); + uint256 count = RewardVault(payable(vault)).maxIncentiveTokensCount(); for (uint256 i = 0; i < count; i++) { MockERC20 incentiveToken = new MockERC20(); @@ -149,7 +142,8 @@ contract DistributeForGasUsageTest is BeaconRootsHelperTest { // Whitelist the token vm.prank(governance); - RewardVault(vault).whitelistIncentiveToken(address(incentiveToken), MIN_INCENTIVE_RATE, address(this)); + RewardVault(payable(vault)) + .whitelistIncentiveToken(address(incentiveToken), MIN_INCENTIVE_RATE, address(this)); _helper_AddIncentives(vault, address(incentiveToken), 100 * 1e18); incentiveTokens.push(address(incentiveToken)); @@ -160,6 +154,6 @@ contract DistributeForGasUsageTest is BeaconRootsHelperTest { MockERC20(token).mint(address(this), type(uint256).max); MockERC20(token).approve(vault, type(uint256).max); - RewardVault(vault).addIncentive(token, amount, INCENTIVE_RATE); + RewardVault(payable(vault)).addIncentive(token, amount, INCENTIVE_RATE); } } diff --git a/test/pol/Distributor.t.sol b/test/pol/Distributor.t.sol index 0e6a7f5..ff0bfaa 100644 --- a/test/pol/Distributor.t.sol +++ b/test/pol/Distributor.t.sol @@ -91,49 +91,23 @@ contract DistributorTest is BeaconRootsHelperTest { ); } - /// @dev Test when the reward rate is zero. - function test_ZeroRewards() public { - vm.startPrank(governance); - blockRewardController.setRewardRate(0); - blockRewardController.setMinBoostedRewardRate(0); - vm.stopPrank(); - - // expect a call to process the rewards - bytes memory data = - abi.encodeCall(IBlockRewardController.processRewards, (valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, false)); - vm.expectCall(address(blockRewardController), data, 1); - // expect no call to mint BGT - data = abi.encodeCall(IBGT.mint, (address(distributor), TEST_BGT_PER_BLOCK)); - vm.expectCall(address(bgt), data, 0); - - distributor.distributeFor( - DISTRIBUTE_FOR_TIMESTAMP, valData.index, valData.pubkey, valData.proposerIndexProof, valData.pubkeyProof - ); - assertEq(bgt.allowance(address(distributor), address(vault)), 0); - } - - /// @dev Test that in genesis no bgts are left unallocated in the distributor. + /// @dev Test that in genesis no rewards are left unallocated in the distributor. function test_DistributeDuringGenesisNoBgtWaste() public { - vm.startPrank(governance); - blockRewardController.setRewardRate(1e18); - blockRewardController.setMinBoostedRewardRate(1e18); - vm.stopPrank(); - - BlockRewardController brc = BlockRewardController(address(distributor.blockRewardController())); + BlockRewardController brc = BlockRewardController(payable(address(distributor.blockRewardController()))); address valOperator = IBeaconDeposit(brc.beaconDepositContract()).getOperator(valData.pubkey); - uint256 distributorBgtBefore = bgt.balanceOf(address(distributor)); - uint256 valOperatorBgtBefore = bgt.balanceOf(valOperator); + uint256 distributorWberaBefore = wbera.balanceOf(address(distributor)); + uint256 valOperatorWberaBefore = wbera.balanceOf(valOperator); distributor.distributeFor( DISTRIBUTE_FOR_TIMESTAMP, valData.index, valData.pubkey, valData.proposerIndexProof, valData.pubkeyProof ); assertEq(bgt.allowance(address(distributor), address(vault)), 0); - // distributor should have same bgts as before - assertEq(bgt.balanceOf(address(distributor)), distributorBgtBefore); - // validator operator should receive base rate as well in genesis - assertEq(bgt.balanceOf(valOperator), valOperatorBgtBefore + blockRewardController.baseRate()); + // distributor should have same WBERA as before + assertEq(wbera.balanceOf(address(distributor)), distributorWberaBefore); + // validator operator should receive base rate as WBERA in genesis + assertEq(wbera.balanceOf(valOperator), valOperatorWberaBefore + blockRewardController.baseRate()); } /// @dev Distribute using the default reward allocation if none is set. @@ -143,16 +117,15 @@ contract DistributorTest is BeaconRootsHelperTest { bytes memory data = abi.encodeCall(IBlockRewardController.processRewards, (valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true)); vm.expectCall(address(blockRewardController), data, 1); - // expect a call to mint the BGT to the distributor - data = abi.encodeCall(IBGT.mint, (address(distributor), TEST_BGT_PER_BLOCK)); - vm.expectCall(address(bgt), data, 1); // expect single call to check if ready then activate the queued reward allocation // although it wont activate the queued reward allocation since it nothing is queued. data = abi.encodeCall(IBeraChef.activateReadyQueuedRewardAllocation, (valData.pubkey)); vm.expectCall(address(beraChef), data, 1); vm.expectEmit(true, true, true, true); - emit IDistributor.Distributed(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, address(vault), TEST_BGT_PER_BLOCK); + emit IDistributor.Distributed( + valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, address(vault), blockRewardController.rewardRate() + ); distributor.distributeFor( DISTRIBUTE_FOR_TIMESTAMP, valData.index, valData.pubkey, valData.proposerIndexProof, valData.pubkeyProof ); @@ -162,7 +135,7 @@ contract DistributorTest is BeaconRootsHelperTest { // queued by the validator // the default reward allocation was set with `1` as startBlock in RootHelperTest. assertEq(beraChef.getActiveRewardAllocation(valData.pubkey).startBlock, 1); - assertEq(bgt.allowance(address(distributor), address(vault)), TEST_BGT_PER_BLOCK); + assertEq(bgt.allowance(address(distributor), address(vault)), blockRewardController.rewardRate()); } /// @dev Test the `multicall` function for distributeFor. @@ -180,9 +153,6 @@ contract DistributorTest is BeaconRootsHelperTest { IBlockRewardController.processRewards, (valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP + 2, true) ); vm.expectCall(address(blockRewardController), data, 1); - // expect 3 calls to mint the BGT to the distributor - data = abi.encodeCall(IBGT.mint, (address(distributor), TEST_BGT_PER_BLOCK)); - vm.expectCall(address(bgt), data, 3); // expect 3 calls to check if ready then activate the queued reward allocation // although it wont activate the queued reward allocation since it nothing is queued. data = abi.encodeCall(IBeraChef.activateReadyQueuedRewardAllocation, (valData.pubkey)); @@ -219,7 +189,7 @@ contract DistributorTest is BeaconRootsHelperTest { // check that all BGT were distributed assertEq(beraChef.getActiveRewardAllocation(valData.pubkey).startBlock, 1); - assertEq(bgt.allowance(address(distributor), address(vault)), 3 * TEST_BGT_PER_BLOCK); + assertEq(bgt.allowance(address(distributor), address(vault)), 3 * blockRewardController.rewardRate()); } /// @dev Activate the queued reward allocation if it is ready and distribute the rewards. @@ -240,22 +210,21 @@ contract DistributorTest is BeaconRootsHelperTest { bytes memory data = abi.encodeCall(IBlockRewardController.processRewards, (valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true)); vm.expectCall(address(blockRewardController), data, 1); - // expect a call to mint the BGT to the distributor - data = abi.encodeCall(IBGT.mint, (address(distributor), TEST_BGT_PER_BLOCK)); - vm.expectCall(address(bgt), data, 1); // expect a call to activate the queued reward allocation data = abi.encodeCall(IBeraChef.activateReadyQueuedRewardAllocation, (valData.pubkey)); vm.expectCall(address(beraChef), data, 1); vm.expectEmit(true, true, true, true); - emit IDistributor.Distributed(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, address(vault), TEST_BGT_PER_BLOCK); + emit IDistributor.Distributed( + valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, address(vault), blockRewardController.rewardRate() + ); distributor.distributeFor( DISTRIBUTE_FOR_TIMESTAMP, valData.index, valData.pubkey, valData.proposerIndexProof, valData.pubkeyProof ); // check that the queued reward allocation was activated assertEq(beraChef.getActiveRewardAllocation(valData.pubkey).startBlock, startBlock); - assertEq(bgt.allowance(address(distributor), address(vault)), TEST_BGT_PER_BLOCK); + assertEq(bgt.allowance(address(distributor), address(vault)), blockRewardController.rewardRate()); } function test_DistributeForNonReentrant() public { @@ -353,32 +322,6 @@ contract DistributorTest is BeaconRootsHelperTest { assertEq(vaultRewards + vault2Rewards, rewardDistributed); } - function testFuzz_DistributeDoesNotLeaveDust( - uint256 weight, - uint256 rewardRate, - uint256 minReward, - uint256 multiplier, - uint256 convexity - ) - public - { - rewardRate = _bound(rewardRate, 0, blockRewardController.MAX_REWARD_RATE()); - minReward = _bound(minReward, 0, blockRewardController.MAX_MIN_BOOSTED_REWARD_RATE()); - multiplier = _bound(multiplier, 0, blockRewardController.MAX_BOOST_MULTIPLIER()); - convexity = _bound(convexity, 1, blockRewardController.MAX_REWARD_CONVEXITY()); - - vm.startPrank(governance); - blockRewardController.setRewardRate(rewardRate); - blockRewardController.setMinBoostedRewardRate(minReward); - blockRewardController.setBoostMultiplier(multiplier); - blockRewardController.setRewardConvexity(convexity); - vm.stopPrank(); - - vm.deal(address(bgt), address(bgt).balance + rewardRate * multiplier / 1e18); // add max bgt minted in a block - - testFuzz_DistributeDoesNotLeaveDust(weight); - } - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* SYSTEM CALL TESTS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -392,22 +335,20 @@ contract DistributorTest is BeaconRootsHelperTest { abi.encodeCall(IBlockRewardController.processRewards, (valData.pubkey, uint64(block.timestamp), true)); vm.expectCall(address(blockRewardController), data, 1); - // expect a call to mint the BGT to the distributor - data = abi.encodeCall(IBGT.mint, (address(distributor), TEST_BGT_PER_BLOCK)); - vm.expectCall(address(bgt), data, 1); - // expect a call to activate the queued reward allocation data = abi.encodeCall(IBeraChef.activateReadyQueuedRewardAllocation, (valData.pubkey)); vm.expectCall(address(beraChef), data, 1); vm.expectEmit(true, true, true, true); - emit IDistributor.Distributed(valData.pubkey, uint64(block.timestamp), address(vault), TEST_BGT_PER_BLOCK); + emit IDistributor.Distributed( + valData.pubkey, uint64(block.timestamp), address(vault), blockRewardController.rewardRate() + ); // Call as system address vm.prank(0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE); distributor.distributeFor(valData.pubkey); - assertEq(bgt.allowance(address(distributor), address(vault)), TEST_BGT_PER_BLOCK); + assertEq(bgt.allowance(address(distributor), address(vault)), blockRewardController.rewardRate()); } /// @dev Test that the new distributeFor function fails when called by non-system address. @@ -445,17 +386,17 @@ contract DistributorTest is BeaconRootsHelperTest { abi.encodeCall(IBlockRewardController.processRewards, (valData.pubkey, uint64(block.timestamp), true)); vm.expectCall(address(blockRewardController), data, 1); - // expect a call to mint the BGT to the distributor - data = abi.encodeCall(IBGT.mint, (address(distributor), TEST_BGT_PER_BLOCK)); - vm.expectCall(address(bgt), data, 1); - // expect a call to activate the queued reward allocation data = abi.encodeCall(IBeraChef.activateReadyQueuedRewardAllocation, (valData.pubkey)); vm.expectCall(address(beraChef), data, 1); vm.expectEmit(true, true, true, true); - emit IDistributor.Distributed(valData.pubkey, uint64(block.timestamp), address(vault), TEST_BGT_PER_BLOCK / 2); - emit IDistributor.Distributed(valData.pubkey, uint64(block.timestamp), vault2, TEST_BGT_PER_BLOCK / 2); + emit IDistributor.Distributed( + valData.pubkey, uint64(block.timestamp), address(vault), blockRewardController.rewardRate() / 2 + ); + emit IDistributor.Distributed( + valData.pubkey, uint64(block.timestamp), vault2, blockRewardController.rewardRate() / 2 + ); // Call as system address vm.prank(0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE); @@ -463,8 +404,8 @@ contract DistributorTest is BeaconRootsHelperTest { // check that the queued reward allocation was activated assertEq(beraChef.getActiveRewardAllocation(valData.pubkey).startBlock, uint64(block.number)); - assertEq(bgt.allowance(address(distributor), address(vault)), TEST_BGT_PER_BLOCK / 2); - assertEq(bgt.allowance(address(distributor), vault2), TEST_BGT_PER_BLOCK / 2); + assertEq(bgt.allowance(address(distributor), address(vault)), blockRewardController.rewardRate() / 2); + assertEq(bgt.allowance(address(distributor), vault2), blockRewardController.rewardRate() / 2); } /// @dev Test that permissionless distributeFor function fails after Pectra11 hard fork. @@ -488,4 +429,58 @@ contract DistributorTest is BeaconRootsHelperTest { // Verify no BGT was distributed assertEq(bgt.allowance(address(distributor), address(vault)), 0); } + + /// @dev Test that setEmissionToken updates the emission token to WBERA and emits the event. + function test_SetEmissionToken() public { + assertEq(distributor.emissionToken(), address(bgt)); + + vm.prank(governance); + vm.expectEmit(true, true, true, true); + emit IDistributor.EmissionTokenSet(address(wbera)); + distributor.setEmissionToken(); + + assertEq(distributor.emissionToken(), address(wbera)); + } + + /// @dev Test that setEmissionToken reverts when called by a non-admin. + function test_SetEmissionToken_RevertIfNotAdmin() public { + vm.expectRevert(); + distributor.setEmissionToken(); + + vm.prank(manager); + vm.expectRevert(); + distributor.setEmissionToken(); + + assertEq(distributor.emissionToken(), address(bgt)); + } + + /// @dev Test that distribution approves WBERA (not BGT) after migration. + function test_DistributeAfterEmissionTokenMigration() public { + helper_SetDefaultRewardAllocation(); + + vm.prank(governance); + distributor.setEmissionToken(); + + vm.deal(address(wbera), 1000 ether); + vm.prank(address(wbera)); + wbera.deposit{ value: 1000 ether }(); + vm.prank(address(wbera)); + wbera.transfer(address(distributor), 1000 ether); + + // Mock the vault's notifyRewardAmount to succeed — the vault still pulls BGT internally, + // but we're testing that the Distributor approves WBERA. + // TODO: remove this mock once the vault is updated to use WBERA. + vm.mockCall(address(vault), abi.encodeWithSelector(RewardVault.notifyRewardAmount.selector), abi.encode()); + + vm.expectEmit(true, true, true, true); + emit IDistributor.Distributed( + valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, address(vault), blockRewardController.rewardRate() + ); + distributor.distributeFor( + DISTRIBUTE_FOR_TIMESTAMP, valData.index, valData.pubkey, valData.proposerIndexProof, valData.pubkeyProof + ); + + assertEq(wbera.allowance(address(distributor), address(vault)), blockRewardController.rewardRate()); + assertEq(bgt.allowance(address(distributor), address(vault)), 0); + } } diff --git a/test/pol/BGTIncentiveFeeCollector.t.sol b/test/pol/IncentivesCollector.t.sol similarity index 76% rename from test/pol/BGTIncentiveFeeCollector.t.sol rename to test/pol/IncentivesCollector.t.sol index 3eb47a4..24a9e16 100644 --- a/test/pol/BGTIncentiveFeeCollector.t.sol +++ b/test/pol/IncentivesCollector.t.sol @@ -13,9 +13,9 @@ import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/P import { DistributorTest } from "./Distributor.t.sol"; import { MockERC20 } from "../mock/token/MockERC20.sol"; import { Salt } from "src/base/Salt.sol"; -import { BGTIncentiveFeeDeployer } from "src/pol/BGTIncentiveFeeDeployer.sol"; -import { BGTIncentiveFeeCollector } from "src/pol/BGTIncentiveFeeCollector.sol"; -import { IBGTIncentiveFeeCollector, IPOLErrors } from "src/pol/interfaces/IBGTIncentiveFeeCollector.sol"; +import { IncentivesCollectorDeployer } from "src/pol/IncentivesCollectorDeployer.sol"; +import { IncentivesCollector } from "src/pol/IncentivesCollector.sol"; +import { IIncentivesCollector, IPOLErrors } from "src/pol/interfaces/IIncentivesCollector.sol"; import { WBERAStakerVault } from "src/pol/WBERAStakerVault.sol"; import { MockLST } from "test/mock/pol/lst/MockLST.sol"; @@ -39,10 +39,10 @@ contract MockNon18DecimalsERC20 is ERC20 { } } -contract BGTIncentiveFeeCollectorTest is DistributorTest { - Salt public BGT_INCENTIVE_FEE_DEPLOYER_SALT = Salt({ implementation: 0, proxy: 1 }); +contract IncentivesCollectorTest is DistributorTest { + Salt public INCENTIVES_COLLECTOR_DEPLOYER_SALT = Salt({ implementation: 0, proxy: 1 }); Salt public WBERA_STAKER_VAULT_SALT = Salt({ implementation: 0, proxy: 1 }); - Salt public BGT_INCENTIVE_FEE_COLLECTOR_SALT = Salt({ implementation: 0, proxy: 1 }); + Salt public INCENTIVES_COLLECTOR_SALT = Salt({ implementation: 0, proxy: 1 }); bytes32 internal pauserRole; address internal pauser = makeAddr("pauser"); @@ -50,7 +50,7 @@ contract BGTIncentiveFeeCollectorTest is DistributorTest { MockERC20 internal feeToken1; MockERC20 internal feeToken2; address internal wberaStakerVault; - BGTIncentiveFeeCollector public incentiveFeeCollector; + IncentivesCollector public incentiveFeeCollector; function setUp() public virtual override { // deploy pol @@ -59,17 +59,15 @@ contract BGTIncentiveFeeCollectorTest is DistributorTest { // Deal WBERA tokens to this contract for the deployer's initial deposit deal(address(wbera), address(this), 10 ether); - address bgtIncentiveFeeDeployer = getCreate2AddressWithArgs( - BGT_INCENTIVE_FEE_DEPLOYER_SALT.implementation, - type(BGTIncentiveFeeDeployer).creationCode, - abi.encode( - governance, address(this), PAYOUT_AMOUNT, WBERA_STAKER_VAULT_SALT, BGT_INCENTIVE_FEE_COLLECTOR_SALT - ) + address incentivesCollectorDeployer = getCreate2AddressWithArgs( + INCENTIVES_COLLECTOR_DEPLOYER_SALT.implementation, + type(IncentivesCollectorDeployer).creationCode, + abi.encode(governance, address(this), PAYOUT_AMOUNT, WBERA_STAKER_VAULT_SALT, INCENTIVES_COLLECTOR_SALT) ); - wbera.approve(bgtIncentiveFeeDeployer, 10 ether); + wbera.approve(incentivesCollectorDeployer, 10 ether); // deploy incentive fee collector - _deployBGTIncentiveFee(); + _deployIncentivesCollector(); // deploy fee tokens feeToken1 = new MockERC20(); @@ -114,7 +112,7 @@ contract BGTIncentiveFeeCollectorTest is DistributorTest { function test_QueuePayoutAmount() public { vm.prank(governance); vm.expectEmit(true, true, true, true); - emit IBGTIncentiveFeeCollector.QueuedPayoutAmount(2e18, 1e18); + emit IIncentivesCollector.QueuedPayoutAmount(2e18, 1e18); incentiveFeeCollector.queuePayoutAmountChange(2e18); assertEq(incentiveFeeCollector.queuedPayoutAmount(), 2e18); } @@ -129,7 +127,7 @@ contract BGTIncentiveFeeCollectorTest is DistributorTest { incentiveFeeCollector.claimFees(address(this), feeTokens); } - function test_ClaimsFees_FailsIfNotApproved() public { + function test_ClaimFees_FailsIfNotApproved() public { _mintTokensToIncentiveFeeCollector(); // approve wbera token for incentive fee collector less than payout amount wbera.approve(address(incentiveFeeCollector), PAYOUT_AMOUNT - 1); @@ -168,6 +166,55 @@ contract BGTIncentiveFeeCollectorTest is DistributorTest { assertEq(incentiveFeeCollector.queuedPayoutAmount(), 0); } + function test_Claim_FailsIfPaused() public { + vm.prank(pauser); + incentiveFeeCollector.pause(); + address[] memory feeTokens = new address[](2); + feeTokens[0] = address(feeToken1); + feeTokens[1] = address(feeToken2); + vm.expectRevert(abi.encodeWithSelector(PausableUpgradeable.EnforcedPause.selector)); + incentiveFeeCollector.claim(address(this), feeTokens); + } + + function test_Claim_FailsIfNotApproved() public { + _mintTokensToIncentiveFeeCollector(); + // approve wbera token for incentive fee collector less than payout amount + wbera.approve(address(incentiveFeeCollector), PAYOUT_AMOUNT - 1); + + address[] memory feeTokens = new address[](2); + feeTokens[0] = address(feeToken1); + feeTokens[1] = address(feeToken2); + vm.expectRevert(ERC20.InsufficientAllowance.selector); + incentiveFeeCollector.claim(address(this), feeTokens); + } + + function test_Claim() public { + uint256 preWberaStakerVaultBalance = wbera.balanceOf(address(wberaStakerVault)); + _claim(); + + // post claim check + assertEq(feeToken1.balanceOf(address(incentiveFeeCollector)), 0); + assertEq(feeToken2.balanceOf(address(incentiveFeeCollector)), 0); + + assertEq(feeToken1.balanceOf(address(this)), 1e18); + assertEq(feeToken2.balanceOf(address(this)), 1e18); + // wbera balance should be 0 for this contract and for incentive fee collector + assertEq(wbera.balanceOf(address(this)), 0); + assertEq(wbera.balanceOf(address(incentiveFeeCollector)), 0); + // wbera balance of wberaStakerVault should increase by payout amount. + assertEq(wbera.balanceOf(address(wberaStakerVault)), PAYOUT_AMOUNT + preWberaStakerVaultBalance); + } + + function test_Claim_ActivateQueuedPayoutAmount() public { + vm.prank(governance); + incentiveFeeCollector.queuePayoutAmountChange(2e18); + + // claim fees should activate queued payout amount + test_Claim(); + assertEq(incentiveFeeCollector.payoutAmount(), 2e18); + assertEq(incentiveFeeCollector.queuedPayoutAmount(), 0); + } + function test_Pause_FailIfNotPauser() public { vm.expectRevert( abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), pauserRole) @@ -216,7 +263,7 @@ contract BGTIncentiveFeeCollectorTest is DistributorTest { } function test_Upgrade_FailsIfNotGovernance() public { - address newImplementation = address(new BGTIncentiveFeeCollector()); + address newImplementation = address(new IncentivesCollector()); vm.expectRevert( abi.encodeWithSelector( IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), defaultAdminRole @@ -226,7 +273,7 @@ contract BGTIncentiveFeeCollectorTest is DistributorTest { } function test_Upgrade() public { - address newImplementation = address(new BGTIncentiveFeeCollector()); + address newImplementation = address(new IncentivesCollector()); vm.prank(governance); incentiveFeeCollector.upgradeToAndCall(newImplementation, ""); assertEq( @@ -245,18 +292,18 @@ contract BGTIncentiveFeeCollectorTest is DistributorTest { } // Helper function to deploy the incentive fee collector. - function _deployBGTIncentiveFee() internal { - BGTIncentiveFeeDeployer bgtIncentiveFeeDeployer = BGTIncentiveFeeDeployer( + function _deployIncentivesCollector() internal { + IncentivesCollectorDeployer incentivesCollectorDeployer = IncentivesCollectorDeployer( deployWithCreate2WithArgs( - BGT_INCENTIVE_FEE_DEPLOYER_SALT.implementation, - type(BGTIncentiveFeeDeployer).creationCode, + INCENTIVES_COLLECTOR_DEPLOYER_SALT.implementation, + type(IncentivesCollectorDeployer).creationCode, abi.encode( - governance, address(this), PAYOUT_AMOUNT, WBERA_STAKER_VAULT_SALT, BGT_INCENTIVE_FEE_COLLECTOR_SALT + governance, address(this), PAYOUT_AMOUNT, WBERA_STAKER_VAULT_SALT, INCENTIVES_COLLECTOR_SALT ) ) ); - incentiveFeeCollector = bgtIncentiveFeeDeployer.bgtIncentiveFeeCollector(); - wberaStakerVault = address(bgtIncentiveFeeDeployer.wberaStakerVault()); + incentiveFeeCollector = incentivesCollectorDeployer.incentivesCollector(); + wberaStakerVault = address(incentivesCollectorDeployer.wberaStakerVault()); } function _claimFees() internal { @@ -268,12 +315,27 @@ contract BGTIncentiveFeeCollectorTest is DistributorTest { feeTokens[0] = address(feeToken1); feeTokens[1] = address(feeToken2); vm.expectEmit(true, true, true, true); - emit IBGTIncentiveFeeCollector.IncentiveFeeTokenClaimed(address(this), address(this), address(feeToken1), 1e18); - emit IBGTIncentiveFeeCollector.IncentiveFeeTokenClaimed(address(this), address(this), address(feeToken2), 1e18); - emit IBGTIncentiveFeeCollector.IncentiveFeesClaimed(address(this), address(this)); + emit IIncentivesCollector.IncentiveTokenClaimed(address(this), address(this), address(feeToken1), 1e18); + emit IIncentivesCollector.IncentiveTokenClaimed(address(this), address(this), address(feeToken2), 1e18); + emit IIncentivesCollector.IncentivesClaimed(address(this), address(this)); incentiveFeeCollector.claimFees(address(this), feeTokens); } + function _claim() internal { + _mintTokensToIncentiveFeeCollector(); + // approve wbera token for incentive fee collector + wbera.approve(address(incentiveFeeCollector), PAYOUT_AMOUNT); + + address[] memory feeTokens = new address[](2); + feeTokens[0] = address(feeToken1); + feeTokens[1] = address(feeToken2); + vm.expectEmit(true, true, true, true); + emit IIncentivesCollector.IncentiveTokenClaimed(address(this), address(this), address(feeToken1), 1e18); + emit IIncentivesCollector.IncentiveTokenClaimed(address(this), address(this), address(feeToken2), 1e18); + emit IIncentivesCollector.IncentivesClaimed(address(this), address(this)); + incentiveFeeCollector.claim(address(this), feeTokens); + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /*. LST */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -285,7 +347,7 @@ contract BGTIncentiveFeeCollectorTest is DistributorTest { vm.prank(governance); vm.expectEmit(true, true, true, true); - emit IBGTIncentiveFeeCollector.LstStakerVaultAdded(lstStakerVault, lstAdapter); + emit IIncentivesCollector.LstStakerVaultAdded(lstStakerVault, lstAdapter); incentiveFeeCollector.addLstStakerVault(lstStakerVault, lstAdapter); assertEq(incentiveFeeCollector.lstAdapters(lstStakerVault), lstAdapter); @@ -356,7 +418,7 @@ contract BGTIncentiveFeeCollectorTest is DistributorTest { vm.prank(governance); vm.expectEmit(true, true, true, true); - emit IBGTIncentiveFeeCollector.LstStakerVaultRemoved(lstStakerVault); + emit IIncentivesCollector.LstStakerVaultRemoved(lstStakerVault); incentiveFeeCollector.removeLstStakerVault(lstStakerVault); assertEq(incentiveFeeCollector.lstStakerVaultsLength(), 0); @@ -396,16 +458,16 @@ contract BGTIncentiveFeeCollectorTest is DistributorTest { uint256 payoutAmount = incentiveFeeCollector.payoutAmount(); - // _claimFees() expansion to test RewardConverted event + // _claim() expansion to test RewardConverted event _mintTokensToIncentiveFeeCollector(); wbera.approve(address(incentiveFeeCollector), payoutAmount); address[] memory feeTokens = new address[](2); feeTokens[0] = address(feeToken1); feeTokens[1] = address(feeToken2); vm.expectEmit(true, true, true, true); - emit IBGTIncentiveFeeCollector.RewardConverted(lstStakerVault, payoutAmount / 2, payoutAmount / 2); - incentiveFeeCollector.claimFees(address(this), feeTokens); - // _claimFees() end + emit IIncentivesCollector.RewardConverted(lstStakerVault, payoutAmount / 2, payoutAmount / 2); + incentiveFeeCollector.claim(address(this), feeTokens); + // _claim() end uint256 balanceAfterLst = IERC20(lst).balanceOf(lstStakerVault); uint256 balanceAfterWbera = wbera.balanceOf(wberaStakerVault); @@ -430,7 +492,7 @@ contract BGTIncentiveFeeCollectorTest is DistributorTest { uint256 balanceBeforeLst1 = IERC20(lst1).balanceOf(lstStakerVault1); uint256 balanceBeforeWbera = wbera.balanceOf(wberaStakerVault); - _claimFees(); + _claim(); uint256 balanceAfterLst0 = IERC20(lst0).balanceOf(lstStakerVault0); uint256 balanceAfterLst1 = IERC20(lst1).balanceOf(lstStakerVault1); diff --git a/test/pol/LSTStakerVaultFactory.t.sol b/test/pol/LSTStakerVaultFactory.t.sol index 47eb4cb..f61ca18 100644 --- a/test/pol/LSTStakerVaultFactory.t.sol +++ b/test/pol/LSTStakerVaultFactory.t.sol @@ -135,6 +135,21 @@ contract LSTStakerVaultFactoryTest is Test, Create2Deployer { assertEq(address(withdrawal721), secondCreation.withdrawal721); } + function test_CreateLSTStakerVaultSystem_FailsOnInflationAttack() public { + mockLst = new MockLST(); + + // Seed predicted vault address before deployment to simulate an inflation attack. + address predictedVault = factory.predictStakerVaultAddress(address(mockLst)); + mockLst.mint(predictedVault, 1 ether); + mockLst.mint(governance, 10 ether); + + vm.startPrank(governance); + mockLst.approve(address(factory), 10 ether); + vm.expectRevert(bytes("Inflation attack detected")); + factory.createLSTStakerVaultSystem(address(mockLst)); + vm.stopPrank(); + } + function test_CreateLSTStakerVaultSystem() public { mockLst = new MockLST(); _deployVault(address(mockLst)); diff --git a/test/pol/POL.t.sol b/test/pol/POL.t.sol index 03b1a2c..b8e4d2d 100644 --- a/test/pol/POL.t.sol +++ b/test/pol/POL.t.sol @@ -26,7 +26,6 @@ import { DedicatedEmissionStreamManager } from "src/pol/rewards/DedicatedEmissio import { DedicatedEmissionStreamManagerDeployer } from "src/pol/DedicatedEmissionStreamManagerDeployer.sol"; abstract contract POLTest is Test, Create2Deployer { - uint256 internal constant TEST_BGT_PER_BLOCK = 5 ether; uint64 internal constant DISTRIBUTE_FOR_TIMESTAMP = 1_234_567_890; uint256 internal constant PAYOUT_AMOUNT = 1e18; uint64 internal constant HISTORY_BUFFER_LENGTH = 8191; @@ -40,6 +39,9 @@ abstract contract POLTest is Test, Create2Deployer { address internal operator = makeAddr("operator"); address internal bgtIncentiveReceiverManager = makeAddr("bgtIncentiveReceiverManager"); + // pol-v-next: WBERA is the emission token + WBERA internal emissionToken = wbera; + struct ValData { bytes32 beaconBlockRoot; uint64 index; @@ -84,6 +86,9 @@ abstract contract POLTest is Test, Create2Deployer { deployPOL(governance); deployCodeTo("WBERA.sol", address(wbera)); + + vm.prank(governance); + blockRewardController.initialize(); deployBGTFees(governance); vm.startPrank(governance); @@ -91,7 +96,6 @@ abstract contract POLTest is Test, Create2Deployer { bgt.setStaker(address(bgtStaker)); bgt.whitelistSender(address(distributor), true); - factory.setBGTIncentiveDistributor(bgtIncentiveDistributor); beraChef.setCommissionChangeDelay(2 * 8191); beraChef.setMaxWeightPerVault(1e4); beraChef.setRewardAllocatorFactory(address(rewardAllocatorFactory)); diff --git a/test/pol/RewardVault.t.sol b/test/pol/RewardVault.t.sol index f2e7389..5d99b83 100644 --- a/test/pol/RewardVault.t.sol +++ b/test/pol/RewardVault.t.sol @@ -21,7 +21,6 @@ import { PausableERC20 } from "@mock/token/PausableERC20.sol"; import { MockERC20 } from "@mock/token/MockERC20.sol"; import { ApprovalPauseERC20 } from "@mock/token/ApprovalPauseERC20.sol"; import { MaxGasConsumeERC20 } from "@mock/token/MaxGasConsumeERC20.sol"; -import { IBGTIncentiveDistributor } from "src/pol/interfaces/IBGTIncentiveDistributor.sol"; import { IRewardVaultHelper } from "src/pol/interfaces/IRewardVaultHelper.sol"; import { IRewardAllocation } from "src/pol/interfaces/IRewardAllocation.sol"; @@ -35,7 +34,7 @@ contract RewardVaultTest is DistributorTest, StakingTest { address internal usdtIncentiveManager = makeAddr("usdtIncentiveManager"); address internal honeyIncentiveManager = makeAddr("honeyIncentiveManager"); address internal honeyVaultManager = makeAddr("honeyVaultManager"); - address internal bgtIncentiveFeeCollector = makeAddr("bgtIncentiveFeeCollector"); + address internal incetiveTokensCollector = makeAddr("incetiveTokensCollector"); MockDAI internal dai = new MockDAI(); MockUSDT internal usdt = new MockUSDT(); PausableERC20 internal pausableERC20 = new PausableERC20(); @@ -66,8 +65,7 @@ contract RewardVaultTest is DistributorTest, StakingTest { // set the reward vault manager for honey vault. vault.setRewardVaultManager(honeyVaultManager); vm.stopPrank(); - // set the incentive fee as 33% and incentiveFeeCollector in the factory. - _setIncentiveFeeRateAndCollector(3300, bgtIncentiveFeeCollector); + _setIncentiveTokensCollector(incetiveTokensCollector); _setRewardVaultHelper(rewardVaultHelper); } @@ -110,7 +108,9 @@ contract RewardVaultTest is DistributorTest, StakingTest { /// @dev helper function to perform reward notification function performNotify(uint256 _amount) internal override { - deal(address(bgt), address(distributor), bgt.balanceOf(address(distributor)) + _amount); + vm.deal(address(bgt), address(bgt).balance + _amount); + vm.prank(address(blockRewardController)); + bgt.mint(address(distributor), _amount); uint256 allowance = bgt.allowance(address(distributor), address(vault)); vm.prank(address(distributor)); IERC20(bgt).approve(address(vault), allowance + _amount); @@ -502,6 +502,11 @@ contract RewardVaultTest is DistributorTest, StakingTest { vault.delegateStake(user, 1 ether); } + function test_DelegateStakeFailsWithZeroAddress() public { + vm.expectRevert(IPOLErrors.ZeroAddress.selector); + vault.delegateStake(address(0), 1 ether); + } + function performDelegateStake(address _delegate, address _user, uint256 _amount) internal { // Mint honey tokens to the delegate honey.mint(_delegate, _amount); @@ -535,6 +540,7 @@ contract RewardVaultTest is DistributorTest, StakingTest { function testFuzz_DelegateStake(address _delegate, address _user, uint256 _stakeAmount) public { vm.assume(_stakeAmount > 0); + vm.assume(_user != address(0)); vm.assume(_delegate != _user); performDelegateStake(_delegate, _user, _stakeAmount); assertEq(vault.totalSupply(), _stakeAmount); @@ -570,7 +576,7 @@ contract RewardVaultTest is DistributorTest, StakingTest { uint256 rewardAmount = _getReward(user, user, user); assertEq(rewardAmount, accumulatedBGTRewards); - assertEq(bgt.balanceOf(user), accumulatedBGTRewards); + assertEq(emissionToken.balanceOf(user), accumulatedBGTRewards); } function test_GetRewardWithStakeOnBehalf() public { @@ -581,7 +587,7 @@ contract RewardVaultTest is DistributorTest, StakingTest { uint256 rewardAmount = _getReward(user, user, user); assertEq(rewardAmount, accumulatedBGTRewards); - assertEq(bgt.balanceOf(user), accumulatedBGTRewards); + assertEq(emissionToken.balanceOf(user), accumulatedBGTRewards); } function test_GetRewardWithRewardVaultHelper() public { @@ -597,13 +603,13 @@ contract RewardVaultTest is DistributorTest, StakingTest { vm.prank(user); IRewardVaultHelper(rewardVaultHelper).claimAllRewards(vaults, alice); - assertEq(bgt.balanceOf(alice), accumulatedBGTRewards); - assertEq(bgt.balanceOf(user), 0); + assertEq(emissionToken.balanceOf(alice), accumulatedBGTRewards); + assertEq(emissionToken.balanceOf(user), 0); } function test_GetRewardWithRewardVaultHelper_MultipleVaults() public { // Create a second vault - RewardVault vault2 = RewardVault(factory.createRewardVault(address(dai))); + RewardVault vault2 = RewardVault(payable(factory.createRewardVault(address(dai)))); // Set up reward allocation with equal weights between vaults vm.startPrank(governance); @@ -646,7 +652,7 @@ contract RewardVaultTest is DistributorTest, StakingTest { IRewardVaultHelper(rewardVaultHelper).claimAllRewards(vaults, user); // Verify rewards were received - assertEq(bgt.balanceOf(user), totalExpectedRewards); + assertEq(emissionToken.balanceOf(user), totalExpectedRewards); } function testFuzz_GetRewardToRecipient(address _recipient) public { @@ -656,12 +662,12 @@ contract RewardVaultTest is DistributorTest, StakingTest { test_Distribute(); performStake(user, 100 ether); vm.warp(block.timestamp + 1 weeks); - uint256 initialBal = bgt.balanceOf(_recipient); + uint256 initialBal = emissionToken.balanceOf(_recipient); uint256 accumulatedBGTRewards = vault.earned(user); uint256 rewardAmount = _getReward(user, user, _recipient); assertEq(rewardAmount, accumulatedBGTRewards); - assertEq(bgt.balanceOf(_recipient), initialBal + accumulatedBGTRewards); + assertEq(emissionToken.balanceOf(_recipient), initialBal + accumulatedBGTRewards); } function test_GetRewardNotOperator() public { @@ -728,6 +734,7 @@ contract RewardVaultTest is DistributorTest, StakingTest { public { vm.assume(_stakeAmount > 0); + vm.assume(_user != address(0)); vm.assume(_delegate != _user); _withdrawAmount = bound(_withdrawAmount, 1, _stakeAmount); performDelegateStake(_delegate, _user, _stakeAmount); @@ -772,7 +779,7 @@ contract RewardVaultTest is DistributorTest, StakingTest { // Check the reward amount was correctly paid out assertTrue(rewardAmount > 0, "Should collect more than 0 rewards"); assertTrue( - bgt.balanceOf(operator) > 0, + emissionToken.balanceOf(operator) > 0, "Operator is the one calling this method then the reward will be credited to that address" ); @@ -797,18 +804,18 @@ contract RewardVaultTest is DistributorTest, StakingTest { // Record balances before exit uint256 initialTokenBalance = honey.balanceOf(user); - uint256 initialRewardBalance = bgt.balanceOf(otherUser); + uint256 initialRewardBalance = emissionToken.balanceOf(otherUser); uint256 userRewards = vault.earned(user); // User calls exit, will only clear out self staked amount and rewards. vm.prank(user); - // transfer BGT rewards to `otherUser` address + // transfer rewards to `otherUser` address vault.exit(otherUser); // Verify user's token balance increased by the `selfStake` amount. assertEq(honey.balanceOf(user), initialTokenBalance + selfStake); // Verify otherUser's reward balance increased. - assertEq(bgt.balanceOf(otherUser), initialRewardBalance + userRewards); + assertEq(emissionToken.balanceOf(otherUser), initialRewardBalance + userRewards); // Verify user's balance in the vault is `delegateStake`. assertEq(vault.balanceOf(user), delegateStake); } @@ -1168,17 +1175,14 @@ contract RewardVaultTest is DistributorTest, StakingTest { (,, uint256 amountRemainingHoney,) = vault.incentives(address(honey)); assertEq(amountRemainingUSDC, 100 * 1e18 - tokenToIncentivize); assertEq(amountRemainingHoney, 100 * 1e18 - tokenToIncentivize); - uint256 incentiveFee = tokenToIncentivize * 33 / 100; - uint256 validatorShare = (tokenToIncentivize - incentiveFee) * 5 / 100; - uint256 bgtBoosterShare = tokenToIncentivize - incentiveFee - validatorShare; - // given default validator commission on incentive token is 5%, 95% of incentive tokens are transferred to - // bgtIncentiveDistributor post incentive fees which is 33% of the incentive tokens. - assertEq(dai.balanceOf(bgtIncentiveFeeCollector), incentiveFee); - assertEq(honey.balanceOf(bgtIncentiveFeeCollector), incentiveFee); - assertEq(dai.balanceOf(bgtIncentiveDistributor), bgtBoosterShare); - assertEq(honey.balanceOf(bgtIncentiveDistributor), bgtBoosterShare); - - // 5% of remaining incentive tokens are transferred to the operator. + uint256 validatorShare = tokenToIncentivize * 5 / 100; + uint256 feeCollectorShare = tokenToIncentivize - validatorShare; + // given default validator commission on incentive token is 5%, the validator's operator gets 5% + // and the remaining 95% is transferred to the incetiveTokensCollector. + assertEq(dai.balanceOf(incetiveTokensCollector), feeCollectorShare); + assertEq(honey.balanceOf(incetiveTokensCollector), feeCollectorShare); + + // 5% of incentive tokens are transferred to the operator. assertEq(dai.balanceOf(address(operator)), validatorShare); assertEq(honey.balanceOf(address(operator)), validatorShare); } @@ -1196,17 +1200,16 @@ contract RewardVaultTest is DistributorTest, StakingTest { performNotify(bgtEmitted); uint256 tokenToIncentivize = (bgtEmitted * 200); tokenToIncentivize = tokenToIncentivize > 100 * 1e18 ? 100 * 1e18 : tokenToIncentivize; - uint256 incentiveFee = tokenToIncentivize * 33 / 100; - uint256 validatorShare = ((tokenToIncentivize - incentiveFee) * commission) / 1e4; - uint256 bgtBoosterShare = tokenToIncentivize - incentiveFee - validatorShare; + uint256 validatorShare = (tokenToIncentivize * commission) / 1e4; + uint256 feeCollectorShare = tokenToIncentivize - validatorShare; (,, uint256 amountRemainingUSDC,) = vault.incentives(address(dai)); (,, uint256 amountRemainingHoney,) = vault.incentives(address(honey)); assertEq(amountRemainingUSDC, 100 * 1e18 - tokenToIncentivize); assertEq(amountRemainingHoney, 100 * 1e18 - tokenToIncentivize); - // BGTIncentiveDistributor should get the bgtBoosterShare of the incentive tokens - assertEq(dai.balanceOf(bgtIncentiveDistributor), bgtBoosterShare); - assertEq(honey.balanceOf(bgtIncentiveDistributor), bgtBoosterShare); + // incetiveTokensCollector should get the remaining incentive tokens + assertEq(dai.balanceOf(incetiveTokensCollector), feeCollectorShare); + assertEq(honey.balanceOf(incetiveTokensCollector), feeCollectorShare); // Operator should get the validatorShare of the incentive tokens assertEq(dai.balanceOf(address(operator)), validatorShare); @@ -1222,22 +1225,13 @@ contract RewardVaultTest is DistributorTest, StakingTest { // validator emit 1 BGT to the vault and will get all the incentives vm.startPrank(address(distributor)); IERC20(bgt).safeIncreaseAllowance(address(vault), 1 ether); - // given incentive fee rate is 33%, 67% of total incentives will be distributed amount validator and - // bgtIncentiveDistributor. - uint256 incentivePostFee = 100 * 1e18 * 67 / 100; - uint256 validatorShare = incentivePostFee * 20 / 100; - uint256 bgtIncentiveDistributorShare = incentivePostFee - validatorShare; + uint256 validatorShare = 100 * 1e18 * 20 / 100; + uint256 feeCollectorShare = 100 * 1e18 - validatorShare; vm.expectEmit(); - emit IRewardVault.BGTBoosterIncentivesProcessed( - valData.pubkey, address(dai), 1e18, bgtIncentiveDistributorShare - ); - emit IRewardVault.BGTBoosterIncentivesProcessed( - valData.pubkey, address(honey), 1e18, bgtIncentiveDistributorShare - ); emit IRewardVault.IncentivesProcessed(valData.pubkey, address(dai), 1e18, validatorShare); emit IRewardVault.IncentivesProcessed(valData.pubkey, address(honey), 1e18, validatorShare); - emit IRewardVault.IncentiveFeeCollected(address(dai), 33 * 100 * 1e18 / 100); - emit IRewardVault.IncentiveFeeCollected(address(honey), 33 * 100 * 1e18 / 100); + emit IRewardVault.IncentivesCollected(valData.pubkey, address(dai), 1e18, feeCollectorShare); + emit IRewardVault.IncentivesCollected(valData.pubkey, address(honey), 1e18, feeCollectorShare); vault.notifyRewardAmount(valData.pubkey, 1e18); // check the incentive data @@ -1246,29 +1240,13 @@ contract RewardVaultTest is DistributorTest, StakingTest { assertEq(amountRemainingUSDC, 0); assertEq(amountRemainingHoney, 0); - // check the incentive fee collector's balance - assertEq(dai.balanceOf(bgtIncentiveFeeCollector), 33 * 100 * 1e18 / 100); - assertEq(honey.balanceOf(bgtIncentiveFeeCollector), 33 * 100 * 1e18 / 100); + // check the fee collector's balance + assertEq(dai.balanceOf(incetiveTokensCollector), feeCollectorShare); + assertEq(honey.balanceOf(incetiveTokensCollector), feeCollectorShare); // check the operator's balance assertEq(dai.balanceOf(address(operator)), validatorShare); assertEq(honey.balanceOf(address(operator)), validatorShare); - - // check the bgtIncentiveDistributor's balance - assertEq(dai.balanceOf(bgtIncentiveDistributor), bgtIncentiveDistributorShare); - assertEq(honey.balanceOf(bgtIncentiveDistributor), bgtIncentiveDistributorShare); - - // make sure the book keeping is correct inside bgtIncentiveDistributor - assertEq( - IBGTIncentiveDistributor(bgtIncentiveDistributor) - .incentiveTokensPerValidator(valData.pubkey, address(dai)), - bgtIncentiveDistributorShare - ); - assertEq( - IBGTIncentiveDistributor(bgtIncentiveDistributor) - .incentiveTokensPerValidator(valData.pubkey, address(honey)), - bgtIncentiveDistributorShare - ); } function test_ProcessIncentives_WithMultipleNotify() public { @@ -1276,31 +1254,16 @@ contract RewardVaultTest is DistributorTest, StakingTest { addIncentives(200 * 1e18, 100 * 1e18); performNotify(1e18); performNotify(1e18); - // After 2nd notify, total incentive tokens distributed is 200 and out of which 33% is incentive fee and rest - // moves to bgtIncentiveDistributor and validator based on validator commission. - uint256 incentiveFee1 = 100 * 1e18 * 33 / 100; - uint256 incentiveFee2 = 100 * 1e18 * 33 / 100; - uint256 incentiveFee = incentiveFee1 + incentiveFee2; // for keeping exact calculation and not have not - // rounding mismatch during assert. - uint256 validatorShare1 = (100 * 1e18 - incentiveFee1) * 5 / 100; - uint256 validatorShare2 = (100 * 1e18 - incentiveFee2) * 5 / 100; + // After 2nd notify, total incentive tokens distributed is 200 and out of which the validator's operator + // gets the commission share and the rest goes to incetiveTokensCollector. + uint256 validatorShare1 = 100 * 1e18 * 5 / 100; + uint256 validatorShare2 = 100 * 1e18 * 5 / 100; uint256 validatorShare = validatorShare1 + validatorShare2; - uint256 bgtIncentiveDistributorShare = 200 * 1e18 - incentiveFee - validatorShare; - assertEq(dai.balanceOf(bgtIncentiveFeeCollector), incentiveFee); - assertEq(honey.balanceOf(bgtIncentiveFeeCollector), incentiveFee); - assertEq(dai.balanceOf(bgtIncentiveDistributor), bgtIncentiveDistributorShare); - assertEq(honey.balanceOf(bgtIncentiveDistributor), bgtIncentiveDistributorShare); - // make sure the book keeping is correct inside bgtIncentiveDistributor - assertEq( - IBGTIncentiveDistributor(bgtIncentiveDistributor) - .incentiveTokensPerValidator(valData.pubkey, address(dai)), - bgtIncentiveDistributorShare - ); - assertEq( - IBGTIncentiveDistributor(bgtIncentiveDistributor) - .incentiveTokensPerValidator(valData.pubkey, address(honey)), - bgtIncentiveDistributorShare - ); + uint256 feeCollectorShare = 200 * 1e18 - validatorShare; + assertEq(dai.balanceOf(incetiveTokensCollector), feeCollectorShare); + assertEq(honey.balanceOf(incetiveTokensCollector), feeCollectorShare); + assertEq(dai.balanceOf(address(operator)), validatorShare); + assertEq(honey.balanceOf(address(operator)), validatorShare); } function test_ProcessIncentives() public { @@ -1308,32 +1271,23 @@ contract RewardVaultTest is DistributorTest, StakingTest { // validator emit 1 BGT to the vault and will get all the incentives vm.startPrank(address(distributor)); IERC20(bgt).safeIncreaseAllowance(address(vault), 1 ether); - // given a 33% fee on incentive tokens, 33% moves to bgtIncentiveFeeCollector and 67% moves to - // bgtIncentiveDistributor and validator based on validator commission. - uint256 incentiveFee = 100 * 1e18 * 33 / 100; - uint256 validatorShare = (100 * 1e18 - incentiveFee) * 5 / 100; - uint256 bgtIncentiveDistributorShare = 100 * 1e18 - incentiveFee - validatorShare; + // validator's operator gets 5% commission, remaining 95% goes to incetiveTokensCollector. + uint256 validatorShare = 100 * 1e18 * 5 / 100; + uint256 feeCollectorShare = 100 * 1e18 - validatorShare; vm.expectEmit(); - emit IRewardVault.BGTBoosterIncentivesProcessed( - valData.pubkey, address(dai), 1e18, bgtIncentiveDistributorShare - ); - emit IRewardVault.BGTBoosterIncentivesProcessed( - valData.pubkey, address(honey), 1e18, bgtIncentiveDistributorShare - ); emit IRewardVault.IncentivesProcessed(valData.pubkey, address(dai), 1e18, validatorShare); emit IRewardVault.IncentivesProcessed(valData.pubkey, address(honey), 1e18, validatorShare); + emit IRewardVault.IncentivesCollected(valData.pubkey, address(dai), 1e18, feeCollectorShare); + emit IRewardVault.IncentivesCollected(valData.pubkey, address(honey), 1e18, feeCollectorShare); vault.notifyRewardAmount(valData.pubkey, 1e18); (,, uint256 amountRemainingUSDC,) = vault.incentives(address(dai)); (,, uint256 amountRemainingHoney,) = vault.incentives(address(honey)); // total incentive tokens = min(200(incentiveRate) * 1, 100(amountRemaining)) = 100 tokens of dai and honey assertEq(amountRemainingUSDC, 0); assertEq(amountRemainingHoney, 0); - assertEq(dai.balanceOf(bgtIncentiveFeeCollector), incentiveFee); - assertEq(honey.balanceOf(bgtIncentiveFeeCollector), incentiveFee); - // bgtIncentiveDistributor should get 95% of remaining incentive tokens post incentive fees. - assertEq(dai.balanceOf(bgtIncentiveDistributor), bgtIncentiveDistributorShare); - assertEq(honey.balanceOf(bgtIncentiveDistributor), bgtIncentiveDistributorShare); - // 5% of remaining incentive tokens are transferred to the operator. + assertEq(dai.balanceOf(incetiveTokensCollector), feeCollectorShare); + assertEq(honey.balanceOf(incetiveTokensCollector), feeCollectorShare); + // 5% of incentive tokens are transferred to the operator. assertEq(dai.balanceOf(address(operator)), validatorShare); assertEq(honey.balanceOf(address(operator)), validatorShare); vm.stopPrank(); @@ -1346,37 +1300,28 @@ contract RewardVaultTest is DistributorTest, StakingTest { assertEq(amountRemaining, 100 * 1e18); } - function test_ProcessIncentives_WithZeroIncentiveRate() public { + function test_ProcessIncentives_WithZeroIncentiveFeeRate() public { addIncentives(100 * 1e18, 200 * 1e18); - IRewardVaultFactory factory = IRewardVaultFactory(vault.factory()); - vm.prank(governance); - factory.setBGTIncentiveFeeRate(0); vm.startPrank(address(distributor)); IERC20(bgt).safeIncreaseAllowance(address(vault), 1 ether); - // given a 0% fee on incentive tokens, 0% moves to bgtIncentiveFeeCollector and 100% moves to - // bgtIncentiveDistributor and validator based on validator commission. + // fee rate on factory doesn't affect _processIncentives anymore; + // validator's operator gets 5% commission, remaining 95% goes to incetiveTokensCollector. uint256 validatorShare = (100 * 1e18) * 5 / 100; - uint256 bgtIncentiveDistributorShare = 100 * 1e18 - validatorShare; + uint256 feeCollectorShare = 100 * 1e18 - validatorShare; vm.expectEmit(); - emit IRewardVault.BGTBoosterIncentivesProcessed( - valData.pubkey, address(dai), 1e18, bgtIncentiveDistributorShare - ); - emit IRewardVault.BGTBoosterIncentivesProcessed( - valData.pubkey, address(honey), 1e18, bgtIncentiveDistributorShare - ); emit IRewardVault.IncentivesProcessed(valData.pubkey, address(dai), 1e18, validatorShare); emit IRewardVault.IncentivesProcessed(valData.pubkey, address(honey), 1e18, validatorShare); + emit IRewardVault.IncentivesCollected(valData.pubkey, address(dai), 1e18, feeCollectorShare); + emit IRewardVault.IncentivesCollected(valData.pubkey, address(honey), 1e18, feeCollectorShare); vault.notifyRewardAmount(valData.pubkey, 1e18); (,, uint256 amountRemainingUSDC,) = vault.incentives(address(dai)); (,, uint256 amountRemainingHoney,) = vault.incentives(address(honey)); assertEq(amountRemainingUSDC, 0); assertEq(amountRemainingHoney, 0); - assertEq(dai.balanceOf(bgtIncentiveFeeCollector), 0); - assertEq(honey.balanceOf(bgtIncentiveFeeCollector), 0); - assertEq(dai.balanceOf(bgtIncentiveDistributor), bgtIncentiveDistributorShare); - assertEq(honey.balanceOf(bgtIncentiveDistributor), bgtIncentiveDistributorShare); + assertEq(dai.balanceOf(incetiveTokensCollector), feeCollectorShare); + assertEq(honey.balanceOf(incetiveTokensCollector), feeCollectorShare); assertEq(dai.balanceOf(address(operator)), validatorShare); assertEq(honey.balanceOf(address(operator)), validatorShare); } @@ -1391,55 +1336,42 @@ contract RewardVaultTest is DistributorTest, StakingTest { vm.startPrank(address(distributor)); IERC20(bgt).safeIncreaseAllowance(address(vault), 1e18); - uint256 incentiveFee = 100 * 1e18 * 33 / 100; - uint256 validatorShare = (100 * 1e18 - incentiveFee) * 20 / 100; - uint256 bgtIncentiveDistributorShare = 100 * 1e18 - incentiveFee - validatorShare; + uint256 validatorShare = 100 * 1e18 * 20 / 100; + uint256 feeCollectorShare = 100 * 1e18 - validatorShare; vm.expectEmit(true, true, true, true); - emit IRewardVault.BGTBoosterIncentivesProcessFailed( - valData.pubkey, address(pausableERC20), 1e18, bgtIncentiveDistributorShare - ); emit IRewardVault.IncentivesProcessFailed(valData.pubkey, address(pausableERC20), 1e18, validatorShare); - emit IRewardVault.IncentiveFeeCollectionFailed(address(pausableERC20), incentiveFee); + emit IRewardVault.IncentivesCollectionFailed(valData.pubkey, address(pausableERC20), 1e18, feeCollectorShare); vault.notifyRewardAmount(valData.pubkey, 1e18); (,, uint256 amountRemainingPausableERC20,) = vault.incentives(address(pausableERC20)); assertEq(amountRemainingPausableERC20, 100 * 1e18); // Amount remaining should not change - assertEq(pausableERC20.balanceOf(bgtIncentiveDistributor), 0); assertEq(pausableERC20.balanceOf(address(operator)), 0); - assertEq(pausableERC20.balanceOf(bgtIncentiveFeeCollector), 0); - // if transfer fails, allowance should be 0. - assertEq(pausableERC20.allowance(address(vault), address(bgtIncentiveDistributor)), 0); + assertEq(pausableERC20.balanceOf(incetiveTokensCollector), 0); } function test_ProcessIncentives_WithApprovalPauseERC20() public { addMaliciusIncentive(approvalPauseERC20, 100 * 1e18, 100 * 1e18); // Set the commission on the validator to 20% setValCommission(2e3); - // Pause the contract in order to make it revert on approval + // Pause the contract in order to make it revert on approval. + // Since the new flow uses trySafeTransfer (no approve step), all transfers succeed. approvalPauseERC20.pause(); - uint256 incentiveFee = 100 * 1e18 * 33 / 100; - uint256 validatorShare = (100 * 1e18 - incentiveFee) * 20 / 100; - uint256 bgtIncentiveDistributorShare = 100 * 1e18 - incentiveFee - validatorShare; + uint256 validatorShare = 100 * 1e18 * 20 / 100; + uint256 feeCollectorShare = 100 * 1e18 - validatorShare; vm.startPrank(address(distributor)); IERC20(bgt).safeIncreaseAllowance(address(vault), 1e18); vm.expectEmit(); - emit IRewardVault.BGTBoosterIncentivesProcessFailed( - valData.pubkey, address(approvalPauseERC20), 1e18, bgtIncentiveDistributorShare - ); emit IRewardVault.IncentivesProcessed(valData.pubkey, address(approvalPauseERC20), 1e18, validatorShare); - emit IRewardVault.IncentiveFeeCollected(address(approvalPauseERC20), incentiveFee); + emit IRewardVault.IncentivesCollected(valData.pubkey, address(approvalPauseERC20), 1e18, feeCollectorShare); vault.notifyRewardAmount(valData.pubkey, 1e18); (,, uint256 amountRemainingApprovalPauseERC20,) = vault.incentives(address(approvalPauseERC20)); - // Only fee and validator share are processed successfully - // BGT booster share fails due to approval failure, so it remains in the contract - assertEq(amountRemainingApprovalPauseERC20, 100 * 1e18 - incentiveFee - validatorShare); - assertEq(approvalPauseERC20.balanceOf(bgtIncentiveDistributor), 0); + assertEq(amountRemainingApprovalPauseERC20, 0); assertEq(approvalPauseERC20.balanceOf(address(operator)), validatorShare); - assertEq(approvalPauseERC20.balanceOf(bgtIncentiveFeeCollector), incentiveFee); + assertEq(approvalPauseERC20.balanceOf(incetiveTokensCollector), feeCollectorShare); } function test_ProcessIncentives_NotFailWithMaliciusIncentive() public { @@ -1452,28 +1384,18 @@ contract RewardVaultTest is DistributorTest, StakingTest { vm.startPrank(address(distributor)); IERC20(bgt).safeIncreaseAllowance(address(vault), 1e17); - // given 33% fee on incentive tokens, 33% moves to bgtIncentiveFeeCollector and rest moves to - // bgtIncentiveDistributor and validator based on validator commission. - uint256 incentiveFee = 20 * 1e18 * 33 / 100; - uint256 validatorShare = (20 * 1e18 - incentiveFee) * 5 / 100; - uint256 bgtIncentiveDistributorShare = 20 * 1e18 - incentiveFee - validatorShare; + // validator's operator gets 5% commission, remaining goes to incetiveTokensCollector. + // incentive amount = min(200 * 1e17, 100 * 1e18) = 20 * 1e18 + uint256 validatorShare = 20 * 1e18 * 5 / 100; + uint256 feeCollectorShare = 20 * 1e18 - validatorShare; vm.expectEmit(); - emit IRewardVault.BGTBoosterIncentivesProcessed( - valData.pubkey, address(dai), 1e17, bgtIncentiveDistributorShare - ); - emit IRewardVault.BGTBoosterIncentivesProcessed( - valData.pubkey, address(honey), 1e17, bgtIncentiveDistributorShare - ); - emit IRewardVault.BGTBoosterIncentivesProcessFailed( - valData.pubkey, address(pausableERC20), 1e17, bgtIncentiveDistributorShare - ); emit IRewardVault.IncentivesProcessed(valData.pubkey, address(dai), 1e17, validatorShare); emit IRewardVault.IncentivesProcessed(valData.pubkey, address(honey), 1e17, validatorShare); emit IRewardVault.IncentivesProcessFailed(valData.pubkey, address(pausableERC20), 1e17, validatorShare); - emit IRewardVault.IncentiveFeeCollected(address(dai), incentiveFee); - emit IRewardVault.IncentiveFeeCollected(address(honey), incentiveFee); - emit IRewardVault.IncentiveFeeCollectionFailed(address(pausableERC20), incentiveFee); + emit IRewardVault.IncentivesCollected(valData.pubkey, address(dai), 1e17, feeCollectorShare); + emit IRewardVault.IncentivesCollected(valData.pubkey, address(honey), 1e17, feeCollectorShare); + emit IRewardVault.IncentivesCollectionFailed(valData.pubkey, address(pausableERC20), 1e17, feeCollectorShare); vault.notifyRewardAmount(valData.pubkey, 1e17); (,, uint256 amountRemainingDAI,) = vault.incentives(address(dai)); @@ -1481,11 +1403,11 @@ contract RewardVaultTest is DistributorTest, StakingTest { assertEq(amountRemainingDAI, 80 * 1e18); assertEq(amountRemainingPausableERC20, 100 * 1e18); // Amount remaining should not change - assertEq(dai.balanceOf(bgtIncentiveDistributor), bgtIncentiveDistributorShare); + assertEq(dai.balanceOf(incetiveTokensCollector), feeCollectorShare); assertEq(dai.balanceOf(operator), validatorShare); // No tokens should be transferred for malicious incentive token - assertEq(pausableERC20.balanceOf(bgtIncentiveDistributor), 0); assertEq(pausableERC20.balanceOf(address(operator)), 0); + assertEq(pausableERC20.balanceOf(incetiveTokensCollector), 0); } function test_WithdrawFailsIfPaused() public { @@ -1518,6 +1440,76 @@ contract RewardVaultTest is DistributorTest, StakingTest { vault.withdraw(withdrawAmount); } + function testFuzz_WithdrawAllFor(uint256 selfStake, uint256 delegateStake) public { + selfStake = bound(selfStake, 1, type(uint256).max - 1); + delegateStake = bound(delegateStake, 1, type(uint256).max - selfStake); + performStake(user, selfStake); + performDelegateStake(operator, user, delegateStake); + + // the reward vault helper is the only authorized caller and receives the withdrawn tokens. + uint256 helperBalanceBefore = honey.balanceOf(rewardVaultHelper); + + vm.prank(rewardVaultHelper); + uint256 withdrawn = vault.withdrawAllFor(user); + + assertEq(withdrawn, selfStake); + assertEq(honey.balanceOf(rewardVaultHelper) - helperBalanceBefore, selfStake); + assertEq(vault.balanceOf(user), delegateStake); + assertEq(vault.getTotalDelegateStaked(user), delegateStake); + } + + function test_WithdrawAllFor() public { + testFuzz_WithdrawAllFor(100 ether, 200 ether); + } + + function testFuzz_WithdrawAllFor_MultipleStakes(uint256 stakeAmount) public { + stakeAmount = bound(stakeAmount, 1, 1_000_000_000 ether); + address staker = makeAddr("staker"); + performStake(staker, stakeAmount); + + testFuzz_WithdrawAllFor(100 ether, 200 ether); + + assertEq(vault.balanceOf(staker), stakeAmount); + assertEq(vault.totalSupply(), stakeAmount + 200 ether); + } + + function test_WithdrawAllFor_RevertsIfNotHelper() public { + performStake(user, 100 ether); + + vm.prank(user); + vm.expectRevert(IPOLErrors.NotRewardVaultHelper.selector); + vault.withdrawAllFor(user); + } + + function test_WithdrawAllFor_ReturnsZeroWhenNoStake() public { + vm.prank(rewardVaultHelper); + uint256 amount = vault.withdrawAllFor(user); + assertEq(amount, 0); + } + + function test_WithdrawAllFor_OnlyDelegateStake() public { + testFuzz_WithdrawAllFor_OnlyDelegateStake(200 ether); + } + + // account has only delegate stake (balance > 0 but self-staked == 0): withdraws nothing and leaves state intact. + function testFuzz_WithdrawAllFor_OnlyDelegateStake(uint256 delegateStake) public { + delegateStake = bound(delegateStake, 1, type(uint128).max); + performDelegateStake(operator, user, delegateStake); + + // pre-condition: the whole balance is delegate stake, nothing self-staked. + assertEq(vault.balanceOf(user), delegateStake); + assertEq(vault.getTotalDelegateStaked(user), delegateStake); + uint256 totalSupplyBefore = vault.totalSupply(); + + vm.prank(rewardVaultHelper); + uint256 withdrawn = vault.withdrawAllFor(user); + + assertEq(withdrawn, 0); + assertEq(vault.totalSupply(), totalSupplyBefore); // delta 0 + assertEq(vault.balanceOf(user), delegateStake); + assertEq(vault.getTotalDelegateStaked(user), delegateStake); + } + // incentive rate changes if undistributed incentive amount is 0. function addIncentives(uint256 amount, uint256 _incentiveRate) internal { _addIncentiveToken(address(dai), daiIncentiveManager, amount, _incentiveRate); @@ -1597,25 +1589,23 @@ contract RewardVaultTest is DistributorTest, StakingTest { beraChef.activateQueuedValCommission(valData.pubkey); } - function test_ProcessIncentives_FailsIfApprovalCrossesSafeGasLimit() public { + function test_ProcessIncentives_WithMaxGasConsumeERC20() public { MaxGasConsumeERC20 maxGasConsumeERC20 = new MaxGasConsumeERC20(); addMaliciusIncentive(maxGasConsumeERC20, 100 * 1e18, 200 * 1e18); vm.startPrank(address(distributor)); IERC20(bgt).safeIncreaseAllowance(address(vault), 1e17); - // given 33% fee on incentive tokens, 33% moves to bgtIncentiveFeeCollector and 67% moves to - // bgtIncentiveDistributor and validator based on validator commission. - uint256 incentiveFee = 20 * 1e18 * 33 / 100; - uint256 validatorShare = (20 * 1e18 - incentiveFee) * 5 / 100; - uint256 bgtIncentiveDistributorShare = 20 * 1e18 - incentiveFee - validatorShare; + // incentive amount = min(200 * 1e17, 100 * 1e18) = 20 * 1e18 + // Both trySafeTransfer calls fail because MaxGasConsumeERC20's transfer exceeds the gas limit. + uint256 validatorShare = 20 * 1e18 * 5 / 100; + uint256 feeCollectorShare = 20 * 1e18 - validatorShare; vm.expectEmit(); - emit IRewardVault.BGTBoosterIncentivesProcessFailed( - valData.pubkey, address(maxGasConsumeERC20), 1e17, bgtIncentiveDistributorShare + emit IRewardVault.IncentivesProcessFailed(valData.pubkey, address(maxGasConsumeERC20), 1e17, validatorShare); + emit IRewardVault.IncentivesCollectionFailed( + valData.pubkey, address(maxGasConsumeERC20), 1e17, feeCollectorShare ); - emit IRewardVault.IncentivesProcessed(valData.pubkey, address(maxGasConsumeERC20), 1e17, validatorShare); - emit IRewardVault.IncentiveFeeCollected(address(maxGasConsumeERC20), incentiveFee); vault.notifyRewardAmount(valData.pubkey, 1e17); } @@ -1737,6 +1727,11 @@ contract RewardVaultTest is DistributorTest, StakingTest { vault.stakeOnBehalf(user, 0); } + function test_StakeOnBehalfFailsWithZeroAddress() public { + vm.expectRevert(IPOLErrors.ZeroAddress.selector); + vault.stakeOnBehalf(address(0), 1 ether); + } + function test_StakeOnBehalfWithInsufficientAllowance() public { honey.mint(address(this), 100 ether); honey.approve(address(vault), 50 ether); // Only approve 50 ether @@ -1820,11 +1815,11 @@ contract RewardVaultTest is DistributorTest, StakingTest { uint256 totalRewards = vault.earned(_account); _partialAmount = bound(_partialAmount, 1, totalRewards); - uint256 initialBalance = bgt.balanceOf(_account); + uint256 initialBalance = emissionToken.balanceOf(_account); vm.expectEmit(); emit IStakingRewards.RewardPaid(_account, _account, _partialAmount); _getPartialReward(_account, _account, _partialAmount); - assertEq(bgt.balanceOf(_account), initialBalance + _partialAmount); + assertEq(emissionToken.balanceOf(_account), initialBalance + _partialAmount); assertEq(vault.earned(_account), totalRewards - _partialAmount); } @@ -1852,9 +1847,9 @@ contract RewardVaultTest is DistributorTest, StakingTest { uint256 totalRewards = vault.earned(_account); _partialAmount = bound(_partialAmount, 1, totalRewards); - uint256 initialRecipientBalance = bgt.balanceOf(_recipient); + uint256 initialRecipientBalance = emissionToken.balanceOf(_recipient); _getPartialReward(_account, _account, _recipient, _partialAmount); - assertEq(bgt.balanceOf(_recipient), initialRecipientBalance + _partialAmount); + assertEq(emissionToken.balanceOf(_recipient), initialRecipientBalance + _partialAmount); assertEq(vault.earned(_account), totalRewards - _partialAmount); } @@ -1909,9 +1904,9 @@ contract RewardVaultTest is DistributorTest, StakingTest { uint256 totalRewards = vault.earned(user); uint256 partialAmount = totalRewards / 2; - uint256 initialOperatorBalance = bgt.balanceOf(operator); + uint256 initialOperatorBalance = emissionToken.balanceOf(operator); _getPartialReward(operator, user, operator, partialAmount); - assertEq(bgt.balanceOf(operator), initialOperatorBalance + partialAmount); + assertEq(emissionToken.balanceOf(operator), initialOperatorBalance + partialAmount); assertEq(vault.earned(user), totalRewards - partialAmount); } @@ -2536,7 +2531,7 @@ contract RewardVaultTest is DistributorTest, StakingTest { // check the incentive fees (,, uint256 amountRemaining,) = vault.incentives(address(dai)); - assertEq(0, IERC20(address(dai)).balanceOf(bgtIncentiveFeeCollector)); + assertEq(0, IERC20(address(dai)).balanceOf(incetiveTokensCollector)); assertEq(incentiveAmount, IERC20(address(dai)).balanceOf(address(vault))); assertEq(incentiveAmount, amountRemaining); } @@ -2548,7 +2543,7 @@ contract RewardVaultTest is DistributorTest, StakingTest { // check the incentive fees (,, uint256 amountRemaining,) = vault.incentives(address(dai)); - assertEq(0, IERC20(address(dai)).balanceOf(bgtIncentiveFeeCollector)); + assertEq(0, IERC20(address(dai)).balanceOf(incetiveTokensCollector)); assertEq(incentiveAmount, IERC20(address(dai)).balanceOf(address(vault))); assertEq(incentiveAmount, amountRemaining); } @@ -2561,14 +2556,11 @@ contract RewardVaultTest is DistributorTest, StakingTest { vault.accountIncentives(address(dai), amount); } - function _setIncentiveFeeRateAndCollector(uint256 rate, address collector) internal { - vm.startPrank(governance); + function _setIncentiveTokensCollector(address collector) internal { IRewardVaultFactory factory = IRewardVaultFactory(vault.factory()); - factory.setBGTIncentiveFeeRate(rate); - factory.setBGTIncentiveFeeCollector(collector); - vm.stopPrank(); - assertEq(factory.bgtIncentiveFeeRate(), rate); - assertEq(factory.bgtIncentiveFeeCollector(), collector); + vm.prank(governance); + factory.setIncentiveTokensCollector(collector); + assertEq(factory.incentiveTokensCollector(), collector); } function _setRewardVaultHelper(address helper) internal { @@ -2578,4 +2570,163 @@ contract RewardVaultTest is DistributorTest, StakingTest { vm.stopPrank(); assertEq(factory.rewardVaultHelper(), helper); } + + function test_GetRewards() public override { + test_NotifyRewardsSetRewardRate(); + vm.warp(block.timestamp + VAULT.rewardsDuration()); + uint256 earned = VAULT.earned(address(this)); + _getReward(address(this), address(this), address(this)); + assertEq(emissionToken.balanceOf(address(this)), earned); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* REWARD TOKEN MIGRATION TESTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function test_RewardTokenMigration_TriggeredOnNotify() public { + // rewardToken() returns the emission token (pol-v-next: WBERA) immediately after the beacon upgrade, even + // before the per-vault lazy migration triggers. + assertEq(address(vault.rewardToken()), address(emissionToken)); + + vm.deal(address(bgt), address(bgt).balance + 10 ether); + vm.prank(address(blockRewardController)); + bgt.mint(address(distributor), 10 ether); + vm.prank(address(distributor)); + IERC20(address(bgt)).approve(address(vault), 10 ether); + + vm.expectEmit(true, true, false, false, address(vault)); + emit IRewardVault.RewardTokenMigrated(address(bgt), address(emissionToken)); + _notifyRewardAmount(10 ether); + + assertEq(address(vault.rewardToken()), address(emissionToken)); + } + + function test_RewardTokenMigration_TriggeredOnStake() public { + assertEq(address(vault.rewardToken()), address(emissionToken)); + + deal(address(honey), user, 10 ether); + vm.prank(user); + honey.approve(address(vault), 10 ether); + + vm.expectEmit(true, true, false, false, address(vault)); + emit IRewardVault.RewardTokenMigrated(address(bgt), address(emissionToken)); + _stake(user, 10 ether); + + assertEq(address(vault.rewardToken()), address(emissionToken)); + } + + function test_RewardTokenMigration_Idempotent() public { + performNotify(10 ether); + assertEq(address(vault.rewardToken()), address(emissionToken)); + + // subsequent interactions should not re-emit + vm.recordLogs(); + performStake(user, 10 ether); + Vm.Log[] memory logs = vm.getRecordedLogs(); + bytes32 migrationTopic = IRewardVault.RewardTokenMigrated.selector; + for (uint256 i; i < logs.length; ++i) { + assertTrue(logs[i].topics[0] != migrationTopic, "Migration event should not fire again"); + } + } + + function test_RewardTokenMigration_FullRewardFlow() public { + assertEq(address(vault.rewardToken()), address(emissionToken)); + + performNotify(100 ether); + assertEq(address(vault.rewardToken()), address(emissionToken)); + + performStake(user, 10 ether); + vm.warp(block.timestamp + vault.rewardsDuration()); + + uint256 earned = vault.earned(user); + assertTrue(earned > 0, "User should have earned rewards"); + + uint256 emissionTokenBalBefore = emissionToken.balanceOf(user); + _getReward(user, user, user); + assertEq(emissionToken.balanceOf(user), emissionTokenBalBefore + earned); + } + + function test_RewardTokenMigration_ReceiveStillAcceptsBGT() public { + performNotify(10 ether); + assertEq(address(vault.rewardToken()), address(emissionToken)); + + // vault should still accept native BERA from BGT contract (for redeem flow) + vm.deal(address(bgt), 1 ether); + vm.prank(address(bgt)); + (bool success,) = address(vault).call{ value: 1 ether }(""); + assertTrue(success, "Vault should accept ETH from BGT after migration"); + } + + function test_RewardTokenMigration_ReceiveRejectsOtherSenders() public { + performNotify(10 ether); + assertEq(address(vault.rewardToken()), address(emissionToken)); + + vm.deal(user, 1 ether); + vm.prank(user); + (bool success,) = address(vault).call{ value: 1 ether }(""); + assertFalse(success, "Vault should reject ETH from non-BGT after migration"); + } + + function test_RewardTokenMigration_ConsumeBGTAllowanceFirst() public { + // SETUP + uint256 bgtAmountMinted = 20 ether; + uint256 emissionTokenAmount = 10 ether; + uint256 rewardAmount = 15 ether; + uint256 stakingTokenAmount = 100 ether; + + // Mint BGT to distributor + vm.deal(address(bgt), address(bgt).balance + bgtAmountMinted); + vm.prank(address(blockRewardController)); + bgt.mint(address(distributor), bgtAmountMinted); + + // increase the BGT allowance to the vault + vm.prank(address(distributor)); + IERC20(address(bgt)).approve(address(vault), bgtAmountMinted); + + // mint emission token to the distributor + vm.deal(address(this), emissionTokenAmount); + emissionToken.deposit{ value: emissionTokenAmount }(); + emissionToken.transfer(address(distributor), emissionTokenAmount); + + vm.prank(address(distributor)); + IERC20(address(emissionToken)).approve(address(vault), emissionTokenAmount); + + // 1. Notify with rewardAmount which should consume only BGT at first + _notifyRewardAmount(rewardAmount); + + performStake(user, stakingTokenAmount); + + // Accrue rewards for user + vm.warp(vm.getBlockTimestamp() + vault.rewardsDuration()); + uint256 earned = vault.earned(user); + _getReward(user, user, user); + + // spend only BGT allowance + assertEq(IERC20(address(bgt)).allowance(address(distributor), address(vault)), bgtAmountMinted - earned); + assertEq(IERC20(address(emissionToken)).allowance(address(distributor), address(vault)), emissionTokenAmount); + + // 2. Notify again, which should spend remaining BGT allowance and start spending emission token allowance next + _notifyRewardAmount(rewardAmount); + + vm.warp(vm.getBlockTimestamp() + vault.rewardsDuration()); + + earned = vault.earned(user); + uint256 bgtRemainingAllowance = IERC20(address(bgt)).allowance(address(distributor), address(vault)); + _getReward(user, user, user); + + uint256 emissionTokenRemainingAllowance = + IERC20(address(emissionToken)).allowance(address(distributor), address(vault)); + assertEq(IERC20(address(bgt)).allowance(address(distributor), address(vault)), 0); + assertEq(emissionTokenRemainingAllowance, emissionTokenAmount - (earned - bgtRemainingAllowance)); + + // 3. Use up remaining emission token allowance completely + _notifyRewardAmount(emissionTokenRemainingAllowance); // Use up remainder + + vm.warp(block.timestamp + vault.rewardsDuration()); + _getReward(user, user, user); + + // spend remaining emission token allowance + assertEq(IERC20(address(bgt)).allowance(address(distributor), address(vault)), 0); + assertEq(IERC20(address(emissionToken)).allowance(address(distributor), address(vault)), 0); + } } diff --git a/test/pol/RewardVaultFactory.t.sol b/test/pol/RewardVaultFactory.t.sol index 64c8f81..fdc2771 100644 --- a/test/pol/RewardVaultFactory.t.sol +++ b/test/pol/RewardVaultFactory.t.sol @@ -176,63 +176,29 @@ contract RewardVaultFactoryTest is POLTest { assert(factory.hasRole(VAULT_PAUSER_ROLE, newVaultPauser)); } - function test_SetBGTIncentiveFeeRate_FailIfNotAdmin() public { + function test_SetIncentiveTokensCollector_FailIfNotAdmin() public { vm.expectRevert( abi.encodeWithSelector( IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), factory.DEFAULT_ADMIN_ROLE() ) ); - factory.setBGTIncentiveFeeRate(1000); + factory.setIncentiveTokensCollector(address(this)); } - function test_SetBGTIncentiveFeeRate_FailInvalidRate() public { - vm.prank(governance); - vm.expectRevert(abi.encodeWithSelector(IPOLErrors.InvalidIncentiveFeeRate.selector)); - factory.setBGTIncentiveFeeRate(10_001); - } - - function testFuzz_SetBGTIncentiveFeeRate(uint256 rate) public { - rate = bound(rate, 0, 7000); - assertEq(0, factory.bgtIncentiveFeeRate()); - vm.prank(governance); - vm.expectEmit(true, true, true, true); - emit IRewardVaultFactory.IncentiveFeeRateUpdated(rate, 0); - factory.setBGTIncentiveFeeRate(rate); - assertEq(rate, factory.bgtIncentiveFeeRate()); - } - - function test_SetBGTIncentiveFeeCollector_FailIfNotAdmin() public { - vm.expectRevert( - abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), factory.DEFAULT_ADMIN_ROLE() - ) - ); - factory.setBGTIncentiveFeeCollector(address(this)); - } - - function test_SetBGTIncentiveFeeCollector_FailIfZero() public { + function test_SetIncentiveTokensCollector_FailIfZero() public { vm.prank(governance); vm.expectRevert(abi.encodeWithSelector(IPOLErrors.ZeroAddress.selector)); - factory.setBGTIncentiveFeeCollector(address(0)); + factory.setIncentiveTokensCollector(address(0)); } - function testFuzz_SetBGTIncentiveFeeCollector(uint256 collectorInt) public { + function testFuzz_SetIncentiveTokensCollector(uint256 collectorInt) public { collectorInt = bound(collectorInt, 1, type(uint160).max); address collector = address(uint160(collectorInt)); - assertEq(address(0), factory.bgtIncentiveFeeCollector()); + assertEq(address(0), factory.incentiveTokensCollector()); vm.prank(governance); vm.expectEmit(true, true, true, true); - emit IRewardVaultFactory.IncentiveFeeCollectorUpdated(collector, address(0)); - factory.setBGTIncentiveFeeCollector(collector); - assertEq(collector, factory.bgtIncentiveFeeCollector()); - } - - function testFuzz_GetIncentiveFeeAmount(uint256 rate, uint256 incentiveAmount) public { - incentiveAmount = bound(incentiveAmount, 1, type(uint256).max / 10_000); - testFuzz_SetBGTIncentiveFeeRate(rate); - rate = factory.bgtIncentiveFeeRate(); - - uint256 feeAmount = factory.getIncentiveFeeAmount(incentiveAmount); - assertEq(rate * incentiveAmount / 10_000, feeAmount); + emit IRewardVaultFactory.IncentiveTokensCollectorUpdated(collector, address(0)); + factory.setIncentiveTokensCollector(collector); + assertEq(collector, factory.incentiveTokensCollector()); } } diff --git a/test/pol/RewardVaultHelper.t.sol b/test/pol/RewardVaultHelper.t.sol new file mode 100644 index 0000000..e1d7836 --- /dev/null +++ b/test/pol/RewardVaultHelper.t.sol @@ -0,0 +1,570 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { RewardVault } from "src/pol/rewards/RewardVault.sol"; +import { RewardVaultHelper } from "src/pol/rewards/RewardVaultHelper.sol"; +import { IStakingRewards } from "src/base/IStakingRewards.sol"; +import { IRewardVaultHelper, IPOLErrors } from "src/pol/interfaces/IRewardVaultHelper.sol"; +import { IRewardAllocation } from "src/pol/interfaces/IRewardAllocation.sol"; +import { DistributorTest } from "./Distributor.t.sol"; +import { MockDAI } from "@mock/honey/MockAssets.sol"; +import { MockERC4626 } from "@mock/token/MockERC4626.sol"; + +contract RewardVaultHelperTest is DistributorTest { + address internal user = makeAddr("user"); + MockDAI internal dai = new MockDAI(); + MockERC4626 internal mockSWBERA; + RewardVaultHelper internal helper; + + address internal constant WBERA_ADDR = 0x6969696969696969696969696969696969696969; + + function setUp() public override { + super.setUp(); + + helper = RewardVaultHelper(payable(rewardVaultHelper)); + + vm.prank(governance); + factory.setRewardVaultHelper(rewardVaultHelper); + + mockSWBERA = new MockERC4626(); + mockSWBERA.initialize(IERC20(WBERA_ADDR), "Staked WBERA", "sWBERA"); + + vm.prank(governance); + helper.setSWBERA(address(mockSWBERA)); + } + + receive() external payable { } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INITIALIZATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function test_Initialize_SetsGovernanceAsAdmin() public view { + assertTrue(helper.hasRole(helper.DEFAULT_ADMIN_ROLE(), governance)); + } + + function test_Initialize_RevertsOnZeroAddress() public { + RewardVaultHelper impl = new RewardVaultHelper(); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), ""); + RewardVaultHelper uninitHelper = RewardVaultHelper(payable(address(proxy))); + vm.expectRevert(IPOLErrors.ZeroAddress.selector); + uninitHelper.initialize(address(0)); + } + + function test_Initialize_CannotBeCalledTwice() public { + vm.expectRevert(); + helper.initialize(governance); + } + + function test_Constructor_DisablesInitializers() public { + RewardVaultHelper impl = new RewardVaultHelper(); + vm.expectRevert(); + impl.initialize(governance); + } + + function test_SetSWBERA_Success() public { + address newSWBERA = makeAddr("newSWBERA"); + vm.prank(governance); + vm.expectEmit(true, true, true, true); + emit IRewardVaultHelper.SWBERASet(newSWBERA); + helper.setSWBERA(newSWBERA); + assertEq(helper.sWBERA(), newSWBERA); + } + + function test_SetSWBERA_RevertsOnZeroAddress() public { + vm.prank(governance); + vm.expectRevert(IPOLErrors.ZeroAddress.selector); + helper.setSWBERA(address(0)); + } + + function test_SetSWBERA_RevertsIfNotAdmin() public { + vm.prank(user); + vm.expectRevert(); + helper.setSWBERA(makeAddr("sWBERA")); + } + + function test_Upgrade_RevertsIfNotAdmin() public { + address newImpl = address(new RewardVaultHelper()); + vm.prank(user); + vm.expectRevert(); + helper.upgradeToAndCall(newImpl, bytes("")); + } + + function test_Upgrade_SucceedsAsAdmin() public { + address newImpl = address(new RewardVaultHelper()); + vm.prank(governance); + helper.upgradeToAndCall(newImpl, bytes("")); + } + + function test_ClaimAllRewards_SingleVault() public { + _distributeRewards(); + _stakeInVault(user, address(vault), address(honey), 100 ether); + vm.warp(block.timestamp + 1 weeks); + + uint256 earned = vault.earned(user); + assertGt(earned, 0); + + address receiver = makeAddr("receiver"); + address[] memory vaults = _singleVaultArray(address(vault)); + + vm.prank(user); + helper.claimAllRewards(vaults, receiver); + + assertEq(wbera.balanceOf(receiver), earned); + assertEq(wbera.balanceOf(user), 0); + } + + function test_ClaimAllRewards_MultipleVaults() public { + RewardVault vault2 = _createAndSetupSecondVault(); + + _stakeInVault(user, address(vault), address(honey), 100 ether); + dai.mint(user, 100 ether); + _stakeInVault(user, address(vault2), address(dai), 100 ether); + + vm.warp(block.timestamp + 1 weeks); + + uint256 earned1 = vault.earned(user); + uint256 earned2 = vault2.earned(user); + uint256 totalExpected = earned1 + earned2; + assertGt(totalExpected, 0); + + address[] memory vaults = new address[](2); + vaults[0] = address(vault); + vaults[1] = address(vault2); + + vm.prank(user); + helper.claimAllRewards(vaults, user); + + assertEq(wbera.balanceOf(user), totalExpected); + } + + function test_ClaimAllRewards_EmptyVaultArray() public { + address[] memory vaults = new address[](0); + + vm.prank(user); + helper.claimAllRewards(vaults, user); + + assertEq(wbera.balanceOf(user), 0); + } + + function test_ClaimAllRewards_EmitsRewardsClaimedEvent() public { + _distributeRewards(); + _stakeInVault(user, address(vault), address(honey), 100 ether); + vm.warp(block.timestamp + 1 weeks); + + uint256 earned = vault.earned(user); + address receiver = makeAddr("receiver"); + address[] memory vaults = _singleVaultArray(address(vault)); + + vm.expectEmit(true, true, true, true); + emit IRewardVaultHelper.RewardsClaimed(earned, receiver, WBERA_ADDR); + + vm.prank(user); + helper.claimAllRewards(vaults, receiver); + } + + function test_ClaimAllRewards_NoRewardsEarned() public { + _distributeRewards(); + _stakeInVault(user, address(vault), address(honey), 100 ether); + + address[] memory vaults = _singleVaultArray(address(vault)); + + vm.prank(user); + helper.claimAllRewards(vaults, user); + + assertEq(wbera.balanceOf(user), 0); + } + + function test_ClaimAllRewards_ReceiverIsDifferentFromCaller() public { + _distributeRewards(); + _stakeInVault(user, address(vault), address(honey), 100 ether); + vm.warp(block.timestamp + 1 weeks); + + uint256 earned = vault.earned(user); + address alice = makeAddr("alice"); + address[] memory vaults = _singleVaultArray(address(vault)); + + vm.prank(user); + helper.claimAllRewards(vaults, alice); + + assertEq(wbera.balanceOf(alice), earned); + assertEq(wbera.balanceOf(user), 0); + } + + function test_ClaimAllRewards_CallerHasNoStake() public { + _distributeRewards(); + _stakeInVault(user, address(vault), address(honey), 100 ether); + vm.warp(block.timestamp + 1 weeks); + + address nobody = makeAddr("nobody"); + address[] memory vaults = _singleVaultArray(address(vault)); + + vm.prank(nobody); + helper.claimAllRewards(vaults, nobody); + + assertEq(wbera.balanceOf(nobody), 0); + } + + function test_ClaimAllRewards_ClaimingTwiceYieldsZeroSecondTime() public { + _distributeRewards(); + _stakeInVault(user, address(vault), address(honey), 100 ether); + vm.warp(block.timestamp + 1 weeks); + + address[] memory vaults = _singleVaultArray(address(vault)); + + vm.prank(user); + helper.claimAllRewards(vaults, user); + uint256 balanceAfterFirstClaim = wbera.balanceOf(user); + assertGt(balanceAfterFirstClaim, 0); + + vm.prank(user); + helper.claimAllRewards(vaults, user); + assertEq(wbera.balanceOf(user), balanceAfterFirstClaim); + } + + function test_ClaimAllRewardsWithOutput_InvalidToken_Reverts() public { + address[] memory vaults = new address[](0); + address invalidToken = makeAddr("invalidToken"); + + vm.prank(user); + vm.expectRevert(IPOLErrors.InvalidToken.selector); + helper.claimAllRewards(vaults, user, invalidToken); + } + + function test_ClaimAllRewardsWithOutput_SWBERA_DepositsIntoVault() public { + _distributeRewards(); + _stakeInVault(user, address(vault), address(honey), 100 ether); + vm.warp(block.timestamp + 1 weeks); + + uint256 earned = vault.earned(user); + assertGt(earned, 0); + + address receiver = makeAddr("receiver"); + address[] memory vaults = _singleVaultArray(address(vault)); + + vm.prank(user); + helper.claimAllRewards(vaults, receiver, address(mockSWBERA)); + + assertEq(wbera.balanceOf(address(helper)), 0, "helper should have no leftover WBERA"); + assertEq(mockSWBERA.balanceOf(receiver), earned, "receiver should have sWBERA shares"); + } + + function test_ClaimAllRewardsWithOutput_SWBERA_EmitsEvent() public { + _distributeRewards(); + _stakeInVault(user, address(vault), address(honey), 100 ether); + vm.warp(block.timestamp + 1 weeks); + + uint256 earned = vault.earned(user); + address receiver = makeAddr("receiver"); + address[] memory vaults = _singleVaultArray(address(vault)); + + vm.expectEmit(true, true, true, true); + emit IRewardVaultHelper.RewardsClaimed(earned, receiver, address(mockSWBERA)); + + vm.prank(user); + helper.claimAllRewards(vaults, receiver, address(mockSWBERA)); + } + + function test_ClaimAllRewardsWithOutput_SWBERA_MultipleVaults() public { + RewardVault vault2 = _createAndSetupSecondVault(); + + _stakeInVault(user, address(vault), address(honey), 100 ether); + dai.mint(user, 100 ether); + _stakeInVault(user, address(vault2), address(dai), 100 ether); + + vm.warp(block.timestamp + 1 weeks); + + uint256 earned1 = vault.earned(user); + uint256 earned2 = vault2.earned(user); + uint256 totalExpected = earned1 + earned2; + + address[] memory vaults = new address[](2); + vaults[0] = address(vault); + vaults[1] = address(vault2); + + address receiver = makeAddr("receiver"); + + vm.prank(user); + helper.claimAllRewards(vaults, receiver, address(mockSWBERA)); + + assertEq(mockSWBERA.balanceOf(receiver), totalExpected); + assertEq(wbera.balanceOf(address(helper)), 0); + } + + function test_ClaimAllRewardsWithOutput_WBERA_SendsDirectly() public { + _distributeRewards(); + _stakeInVault(user, address(vault), address(honey), 100 ether); + vm.warp(block.timestamp + 1 weeks); + + uint256 earned = vault.earned(user); + assertGt(earned, 0); + + address[] memory vaults = _singleVaultArray(address(vault)); + address receiver = makeAddr("receiver"); + + vm.prank(user); + helper.claimAllRewards(vaults, receiver, WBERA_ADDR); + + assertEq(wbera.balanceOf(receiver), earned, "receiver should have WBERA"); + assertEq(wbera.balanceOf(address(helper)), 0, "helper should have no leftover WBERA"); + } + + function test_ClaimAllRewardsWithOutput_WBERA_EmitsEvent() public { + _distributeRewards(); + _stakeInVault(user, address(vault), address(honey), 100 ether); + vm.warp(block.timestamp + 1 weeks); + + uint256 earned = vault.earned(user); + address receiver = makeAddr("receiver"); + address[] memory vaults = _singleVaultArray(address(vault)); + + vm.expectEmit(true, true, true, true); + emit IRewardVaultHelper.RewardsClaimed(earned, receiver, WBERA_ADDR); + + vm.prank(user); + helper.claimAllRewards(vaults, receiver, WBERA_ADDR); + } + + function test_ClaimAllRewardsWithOutput_NativeBERA() public { + _distributeRewards(); + _stakeInVault(user, address(vault), address(honey), 100 ether); + vm.warp(block.timestamp + 1 weeks); + + uint256 earned = vault.earned(user); + assertGt(earned, 0); + + address[] memory vaults = _singleVaultArray(address(vault)); + address receiver = address(this); + + uint256 balBefore = receiver.balance; + + vm.prank(user); + helper.claimAllRewards(vaults, receiver, address(0)); + + assertEq(receiver.balance - balBefore, earned); + assertEq(wbera.balanceOf(address(helper)), 0); + } + + function test_ClaimAllRewardsWithOutput_EmptyVaults_NoRevert() public { + address[] memory vaults = new address[](0); + + vm.prank(user); + helper.claimAllRewards(vaults, user, address(mockSWBERA)); + + assertEq(mockSWBERA.balanceOf(user), 0); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* WITHDRAW FROM VAULTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function test_WithdrawAllFromVaults_SingleVault() public { + _stakeInVault(user, address(vault), address(honey), 100 ether); + assertEq(vault.balanceOf(user), 100 ether); + + address receiver = makeAddr("receiver"); + address[] memory vaults = _singleVaultArray(address(vault)); + + vm.prank(user); + helper.withdrawAllFromVaults(vaults, receiver); + + assertEq(vault.balanceOf(user), 0); + assertEq(honey.balanceOf(receiver), 100 ether); + assertEq(honey.balanceOf(address(helper)), 0); + } + + function test_WithdrawAllFromVaults_ReceiverIsCaller() public { + _stakeInVault(user, address(vault), address(honey), 100 ether); + + address[] memory vaults = _singleVaultArray(address(vault)); + + vm.prank(user); + helper.withdrawAllFromVaults(vaults, user); + + assertEq(vault.balanceOf(user), 0); + assertEq(honey.balanceOf(user), 100 ether); + } + + function test_WithdrawAllFromVaults_MultipleVaults() public { + RewardVault vault2 = _createAndSetupSecondVault(); + _stakeInVault(user, address(vault), address(honey), 100 ether); + _stakeInVault(user, address(vault2), address(dai), 50 ether); + + address receiver = makeAddr("receiver"); + address[] memory vaults = new address[](2); + vaults[0] = address(vault); + vaults[1] = address(vault2); + + vm.prank(user); + helper.withdrawAllFromVaults(vaults, receiver); + + assertEq(vault.balanceOf(user), 0); + assertEq(vault2.balanceOf(user), 0); + assertEq(honey.balanceOf(receiver), 100 ether); + assertEq(dai.balanceOf(receiver), 50 ether); + assertEq(honey.balanceOf(address(helper)), 0); + assertEq(dai.balanceOf(address(helper)), 0); + } + + function test_WithdrawAllFromVaults_SkipsVaultWithNoStake() public { + RewardVault vault2 = _createAndSetupSecondVault(); + // user only stakes in `vault`, not `vault2` + _stakeInVault(user, address(vault), address(honey), 100 ether); + + address receiver = makeAddr("receiver"); + address[] memory vaults = new address[](2); + vaults[0] = address(vault2); // no stake here: must be skipped, not revert + vaults[1] = address(vault); + + vm.prank(user); + helper.withdrawAllFromVaults(vaults, receiver); + + assertEq(honey.balanceOf(receiver), 100 ether); + assertEq(dai.balanceOf(receiver), 0); + assertEq(vault.balanceOf(user), 0); + } + + function test_WithdrawAllFromVaults_ExcludesDelegateStake() public { + address delegate = makeAddr("delegate"); + _stakeInVault(user, address(vault), address(honey), 100 ether); + _delegateStakeInVault(delegate, user, address(vault), address(honey), 40 ether); + + assertEq(vault.balanceOf(user), 140 ether); + assertEq(vault.getTotalDelegateStaked(user), 40 ether); + + address receiver = makeAddr("receiver"); + address[] memory vaults = _singleVaultArray(address(vault)); + + vm.prank(user); + helper.withdrawAllFromVaults(vaults, receiver); + + // only the self-staked 100 is withdrawn; the delegate-staked 40 stays put + assertEq(honey.balanceOf(receiver), 100 ether); + assertEq(vault.balanceOf(user), 40 ether); + assertEq(vault.getTotalDelegateStaked(user), 40 ether); + } + + function test_WithdrawAllFromVaults_PreservesEarnedRewards() public { + _distributeRewards(); + _stakeInVault(user, address(vault), address(honey), 100 ether); + vm.warp(block.timestamp + 1 weeks); + + uint256 earnedBefore = vault.earned(user); + assertGt(earnedBefore, 0); + + address[] memory vaults = _singleVaultArray(address(vault)); + + vm.prank(user); + helper.withdrawAllFromVaults(vaults, user); + + assertEq(vault.balanceOf(user), 0); + assertEq(vault.earned(user), earnedBefore); + } + + function test_WithdrawAllFromVaults_SecondCallWithdrawsNothing() public { + _stakeInVault(user, address(vault), address(honey), 100 ether); + address[] memory vaults = _singleVaultArray(address(vault)); + + vm.prank(user); + helper.withdrawAllFromVaults(vaults, user); + uint256 balanceAfterFirst = honey.balanceOf(user); + assertEq(balanceAfterFirst, 100 ether); + + vm.prank(user); + helper.withdrawAllFromVaults(vaults, user); + assertEq(honey.balanceOf(user), balanceAfterFirst); + } + + function test_WithdrawAllFromVaults_EmptyArray_NoRevert() public { + address[] memory vaults = new address[](0); + + vm.prank(user); + helper.withdrawAllFromVaults(vaults, user); + + assertEq(honey.balanceOf(user), 0); + } + + function test_WithdrawAllFromVaults_RevertsOnZeroReceiver() public { + _stakeInVault(user, address(vault), address(honey), 100 ether); + address[] memory vaults = _singleVaultArray(address(vault)); + + vm.prank(user); + vm.expectRevert(IPOLErrors.ZeroAddress.selector); + helper.withdrawAllFromVaults(vaults, address(0)); + } + + function test_WithdrawAllFromVaults_RevertsWhenVaultPaused() public { + _stakeInVault(user, address(vault), address(honey), 100 ether); + + // grant this contract the manager role, then the pauser role, then pause the vault. + bytes32 managerRole = factory.VAULT_MANAGER_ROLE(); + bytes32 pauserRole = factory.VAULT_PAUSER_ROLE(); + vm.prank(governance); + factory.grantRole(managerRole, address(this)); + factory.grantRole(pauserRole, address(this)); + vault.pause(); + + address[] memory vaults = _singleVaultArray(address(vault)); + + vm.prank(user); + vm.expectRevert(); // PausableUpgradeable.EnforcedPause + helper.withdrawAllFromVaults(vaults, user); + } + + function _delegateStakeInVault( + address _delegate, + address _account, + address _vault, + address _token, + uint256 _amount + ) + internal + { + deal(_token, _delegate, _amount); + vm.startPrank(_delegate); + IERC20(_token).approve(_vault, _amount); + RewardVault(payable(_vault)).delegateStake(_account, _amount); + vm.stopPrank(); + } + + function _distributeRewards() internal { + helper_SetDefaultRewardAllocation(); + distributor.distributeFor( + DISTRIBUTE_FOR_TIMESTAMP, valData.index, valData.pubkey, valData.proposerIndexProof, valData.pubkeyProof + ); + } + + function _stakeInVault(address _user, address _vault, address _token, uint256 _amount) internal { + deal(_token, _user, _amount); + vm.startPrank(_user); + IERC20(_token).approve(_vault, _amount); + RewardVault(payable(_vault)).stake(_amount); + vm.stopPrank(); + } + + function _singleVaultArray(address _vault) internal pure returns (address[] memory vaults) { + vaults = new address[](1); + vaults[0] = _vault; + } + + function _createAndSetupSecondVault() internal returns (RewardVault vault2) { + vm.prank(governance); + vault2 = RewardVault(payable(factory.createRewardVault(address(dai)))); + + vm.startPrank(governance); + IRewardAllocation.Weight[] memory weights = new IRewardAllocation.Weight[](2); + weights[0] = IRewardAllocation.Weight(address(vault), 5000); + weights[1] = IRewardAllocation.Weight(address(vault2), 5000); + beraChef.setVaultWhitelistedStatus(address(vault), true, ""); + beraChef.setVaultWhitelistedStatus(address(vault2), true, ""); + beraChef.setDefaultRewardAllocation(IRewardAllocation.RewardAllocation(1, weights)); + vm.stopPrank(); + + vm.prank(0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE); + distributor.distributeFor(valData.pubkey); + } +} diff --git a/test/pol/e2e/POLE2EFuzz.t.sol b/test/pol/e2e/POLE2EFuzz.t.sol index 0f5b830..94c5264 100644 --- a/test/pol/e2e/POLE2EFuzz.t.sol +++ b/test/pol/e2e/POLE2EFuzz.t.sol @@ -28,23 +28,34 @@ contract POLE2EFuzz is POLGasSimulationSimple { RewardVault[] memory vaults = setUpFuzz(weights, numVaults, incentivesCount); // Setup the // environment with fuzzed data - // Total rewards for the block are set by super.setUp() to TEST_BGT_PER_BLOCK - uint256 totalRewards = TEST_BGT_PER_BLOCK; + // Total rewards for the block are set by super.setUp() to blockRewardController.rewardRate() + uint256 totalRewards = blockRewardController.rewardRate(); - // Calculate expected rewards for each vault and expect the RewardAdded event + // Fund the BlockRewardController so processRewards can wrap rewards directly into WBERA, + // skipping the legacy BGT mint/redeem fallback path (BGT is being deprecated). + vm.deal(address(blockRewardController), blockRewardController.getMaxBGTPerBlock()); + + uint256[] memory undistributedBefore = new uint256[](numVaults); for (uint256 i; i < numVaults; ++i) { - uint256 expectedReward = (totalRewards * weights[i]) * PRECISION / 10_000; - vm.expectEmit(true, true, true, true, address(vaults[i])); - emit RewardAdded(expectedReward); + undistributedBefore[i] = vaults[i].undistributedRewards(); } - deal(address(bgt), address(bgt).balance + TEST_BGT_PER_BLOCK); // simulate native token distribution - distributor.distributeFor( lastProcessedTimestamp, valData.index, valData.pubkey, valData.proposerIndexProof, valData.pubkeyProof ); lastProcessedTimestamp++; + // Each vault should have received its weighted share of the block rewards, scaled by PRECISION + // (StakingRewards._notifyRewardAmount stores undistributedRewards = reward * PRECISION). + for (uint256 i; i < numVaults; ++i) { + uint256 expectedReward = totalRewards * weights[i] * PRECISION / 10_000; + assertEq( + vaults[i].undistributedRewards() - undistributedBefore[i], + expectedReward, + "Mismatch in distributed reward" + ); + } + // Verification verifyWeights(numVaults, weights); } diff --git a/test/pol/e2e/POLGasSim.t.sol b/test/pol/e2e/POLGasSim.t.sol index fab0c1f..e448b20 100644 --- a/test/pol/e2e/POLGasSim.t.sol +++ b/test/pol/e2e/POLGasSim.t.sol @@ -60,7 +60,12 @@ contract POLGasSimulationSimple is GovernanceBaseTest { // Deploy and initialize POL-related contracts deployPOL(address(timelock)); + deployCodeTo("WBERA.sol", address(wbera)); wbera = new WBERA(); + + vm.prank(address(timelock)); + blockRewardController.initialize(); + deployBGTFees(address(timelock)); // NOTE: for gov.getVotes to work, the block timestamp must be a realistic one (greater than zero). @@ -81,32 +86,21 @@ contract POLGasSimulationSimple is GovernanceBaseTest { vm.roll(100); // Setup proposal actions, encoded call data for governance actions - address[] memory targets = new address[](11); - targets[0] = address(blockRewardController); - targets[1] = address(blockRewardController); - targets[2] = address(blockRewardController); - targets[3] = address(blockRewardController); - targets[4] = address(bgt); + address[] memory targets = new address[](6); + targets[0] = address(bgt); + targets[1] = address(beraChef); + targets[2] = address(beraChef); + targets[3] = address(bgt); + targets[4] = address(beraChef); targets[5] = address(beraChef); - targets[6] = address(beraChef); - targets[7] = address(bgt); - targets[8] = address(factory); - targets[9] = address(beraChef); - targets[10] = address(beraChef); - - bytes[] memory calldatas = new bytes[](11); - calldatas[0] = abi.encodeCall(BlockRewardController.setRewardRate, (5 ether)); - calldatas[1] = abi.encodeCall(BlockRewardController.setMinBoostedRewardRate, (5 ether)); - calldatas[2] = abi.encodeCall(BlockRewardController.setBoostMultiplier, (3 ether)); - calldatas[3] = abi.encodeCall(BlockRewardController.setRewardConvexity, (0.5 ether)); - calldatas[4] = abi.encodeCall(BGT.whitelistSender, (address(distributor), true)); - calldatas[5] = abi.encodeCall(BeraChef.setRewardAllocationBlockDelay, (0)); - calldatas[6] = abi.encodeCall(BeraChef.setMaxWeightPerVault, (1e4)); - calldatas[7] = abi.encodeCall(BGT.setMinter, (address(blockRewardController))); - calldatas[8] = - abi.encodeCall(RewardVaultFactory.setBGTIncentiveDistributor, (address(bgtIncentiveDistributor))); - calldatas[9] = abi.encodeCall(BeraChef.setRewardAllocatorFactory, (address(rewardAllocatorFactory))); - calldatas[10] = abi.encodeCall(BeraChef.setRewardAllocationInactivityBlockSpan, (86_400)); + + bytes[] memory calldatas = new bytes[](6); + calldatas[0] = abi.encodeCall(BGT.whitelistSender, (address(distributor), true)); + calldatas[1] = abi.encodeCall(BeraChef.setRewardAllocationBlockDelay, (0)); + calldatas[2] = abi.encodeCall(BeraChef.setMaxWeightPerVault, (1e4)); + calldatas[3] = abi.encodeCall(BGT.setMinter, (address(blockRewardController))); + calldatas[4] = abi.encodeCall(BeraChef.setRewardAllocatorFactory, (address(rewardAllocatorFactory))); + calldatas[5] = abi.encodeCall(BeraChef.setRewardAllocationInactivityBlockSpan, (86_400)); // Create and execute governance proposals governanceHelper(targets, calldatas); @@ -146,7 +140,11 @@ contract POLGasSimulationSimple is GovernanceBaseTest { require(ECDSA.recover(_proof, _signature) == signer, "POLGasSimulationSimple: Invalid signature"); - deal(address(bgt), address(bgt).balance + 100 ether); // simulate native token distribution + deal(address(bgt), address(bgt).balance + 100 ether); + vm.deal(address(blockRewardController), address(blockRewardController).balance + 100 ether); + // Pre-fund the distributor with BGT so it can distribute to vaults. + vm.prank(address(blockRewardController)); + bgt.mint(address(distributor), 100 ether); distributor.distributeFor( lastProcessedTimestamp, valData.index, valData.pubkey, valData.proposerIndexProof, valData.pubkeyProof );