Copy // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;
import { StdCheats } from "forge-std/StdCheats.sol";
import { Test, console2 } from "forge-std/Test.sol";
// Diamond Proxy & Storage
import { PlumeStaking } from "../src/PlumeStaking.sol";
import { PlumeStakingStorage } from "../src/lib/PlumeStakingStorage.sol";
// Custom Facet Contracts
import { AccessControlFacet } from "../src/facets/AccessControlFacet.sol";
import { ManagementFacet } from "../src/facets/ManagementFacet.sol";
import { RewardsFacet } from "../src/facets/RewardsFacet.sol";
import { StakingFacet } from "../src/facets/StakingFacet.sol";
import { ValidatorFacet } from "../src/facets/ValidatorFacet.sol";
import { IAccessControl } from "../src/interfaces/IAccessControl.sol";
import { IPlumeStakingRewardTreasury } from "../src/interfaces/IPlumeStakingRewardTreasury.sol";
// SolidState Diamond Interface & Cut Interface
import { IERC2535DiamondCutInternal } from "@solidstate/interfaces/IERC2535DiamondCutInternal.sol";
import { ISolidStateDiamond } from "@solidstate/proxy/diamond/ISolidStateDiamond.sol";
// Libs & Errors/Events
import { NoRewardsToRestake, NotValidatorAdmin, Unauthorized } from "../src/lib/PlumeErrors.sol";
import "../src/lib/PlumeErrors.sol";
import "../src/lib/PlumeEvents.sol";
import { PlumeRewardLogic } from "../src/lib/PlumeRewardLogic.sol";
import { PlumeRoles } from "../src/lib/PlumeRoles.sol"; // Needed for REWARD_PRECISION
// OZ Contracts
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
// Treasury Proxy
import { PlumeStakingRewardTreasury } from "../src/PlumeStakingRewardTreasury.sol";
import { PlumeStakingRewardTreasuryProxy } from "../src/proxy/PlumeStakingRewardTreasuryProxy.sol";
contract PlumeStakingStressTest is Test {
address public user1;
address public user2;
address public user3;
address public user4;
address public admin;
address public alice;
address public bob;
address public charlie;
address public dave;
address public validatorAdmin;
// Diamond Proxy Address
PlumeStaking internal diamondProxy;
PlumeStakingRewardTreasury public treasury;
// Addresses
address public constant ADMIN_ADDRESS = 0xC0A7a3AD0e5A53cEF42AB622381D0b27969c4ab5;
address payable public constant PLUME_NATIVE = payable(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); // Payable for
// treasury funding
// Constants
uint256 public constant MIN_STAKE = 1e17; // 0.1 PLUME for stress testing
uint256 public constant INITIAL_COOLDOWN = 7 days; // Keep real cooldown
uint16 public constant NUM_VALIDATORS = 15;
uint256 public constant VALIDATOR_COMMISSION = 0.005 * 1e18; // 0.5% scaled by 1e18
// Approx 5% APR for PLUME rewards per second = (0.05 * 1e18) / (365 days * 24 hours * 60 mins * 60 secs)
uint256 public constant PLUME_REWARD_RATE_PER_SECOND = 1_585_489_599; // ~5% APR (5e16 / 31536000)
// Test parameters
uint256 constant MAX_RANDOM_STAKE_AMOUNT = 5 ether; // Max amount for a single random stake action
uint256 constant TEST_STAKER_INITIAL_BALANCE = 100 ether; // Ensure test staker has plenty of funds
uint256 constant GAS_TEST_NUM_ACTIONS = 100; // Number of actions to measure gas for
// Cost calculation parameters (adjust as needed)
uint256 constant ETH_PRICE_USD = 3500; // Example ETH price
uint256 constant L2_GAS_PRICE_GWEI = 0.001 * 1e9; // Example L2 gas price (0.001 Gwei) - SCALE BY 1e9 for wei
// Unique address for the staker whose actions we measure
address constant TEST_STAKER = address(0xBADBADBAD);
function setUp() public {
console2.log("Starting Stress Test setup");
admin = ADMIN_ADDRESS;
vm.deal(admin, 10_000 ether); // Ensure admin has funds
vm.startPrank(admin);
// 1. Deploy Diamond Proxy
diamondProxy = new PlumeStaking();
assertEq(
ISolidStateDiamond(payable(address(diamondProxy))).owner(), admin, "Deployer should be owner initially"
);
// 2. Deploy Custom Facets
AccessControlFacet accessControlFacet = new AccessControlFacet();
StakingFacet stakingFacet = new StakingFacet();
RewardsFacet rewardsFacet = new RewardsFacet();
ValidatorFacet validatorFacet = new ValidatorFacet();
ManagementFacet managementFacet = new ManagementFacet();
// 3. Prepare Diamond Cut
IERC2535DiamondCutInternal.FacetCut[] memory cut = new IERC2535DiamondCutInternal.FacetCut[](5);
// --- Get Selectors (using helper or manual list) ---
bytes4[] memory accessControlSigs = new bytes4[](7);
accessControlSigs[0] = AccessControlFacet.initializeAccessControl.selector;
accessControlSigs[1] = IAccessControl.hasRole.selector;
accessControlSigs[2] = IAccessControl.getRoleAdmin.selector;
accessControlSigs[3] = IAccessControl.grantRole.selector;
accessControlSigs[4] = IAccessControl.revokeRole.selector;
accessControlSigs[5] = IAccessControl.renounceRole.selector;
accessControlSigs[6] = IAccessControl.setRoleAdmin.selector;
bytes4[] memory stakingSigs = new bytes4[](13);
stakingSigs[0] = StakingFacet.stake.selector;
stakingSigs[1] = StakingFacet.restake.selector;
stakingSigs[2] = bytes4(keccak256("unstake(uint16)"));
stakingSigs[3] = bytes4(keccak256("unstake(uint16,uint256)"));
stakingSigs[4] = StakingFacet.withdraw.selector;
stakingSigs[5] = StakingFacet.stakeOnBehalf.selector;
stakingSigs[6] = StakingFacet.stakeInfo.selector;
stakingSigs[7] = StakingFacet.amountStaked.selector;
stakingSigs[8] = StakingFacet.amountCooling.selector;
stakingSigs[9] = StakingFacet.amountWithdrawable.selector;
//stakingSigs[10] = StakingFacet.cooldownEndDate.selector;
stakingSigs[10] = StakingFacet.getUserValidatorStake.selector;
stakingSigs[11] = StakingFacet.restakeRewards.selector;
stakingSigs[12] = StakingFacet.totalAmountStaked.selector;
bytes4[] memory rewardsSigs = new bytes4[](15);
rewardsSigs[0] = RewardsFacet.addRewardToken.selector;
rewardsSigs[1] = RewardsFacet.removeRewardToken.selector;
rewardsSigs[2] = RewardsFacet.setRewardRates.selector;
rewardsSigs[3] = RewardsFacet.setMaxRewardRate.selector;
rewardsSigs[4] = bytes4(keccak256("claim(address)"));
rewardsSigs[5] = bytes4(keccak256("claim(address,uint16)"));
rewardsSigs[6] = RewardsFacet.claimAll.selector;
rewardsSigs[7] = RewardsFacet.earned.selector;
rewardsSigs[8] = RewardsFacet.getClaimableReward.selector;
rewardsSigs[9] = RewardsFacet.getRewardTokens.selector;
rewardsSigs[10] = RewardsFacet.getMaxRewardRate.selector;
rewardsSigs[11] = RewardsFacet.tokenRewardInfo.selector;
rewardsSigs[12] = RewardsFacet.setTreasury.selector;
rewardsSigs[13] = RewardsFacet.getPendingRewardForValidator.selector;
bytes4[] memory validatorSigs = new bytes4[](14);
validatorSigs[0] = ValidatorFacet.addValidator.selector;
validatorSigs[1] = ValidatorFacet.setValidatorCapacity.selector;
validatorSigs[2] = ValidatorFacet.setValidatorCommission.selector;
validatorSigs[3] = ValidatorFacet.setValidatorAddresses.selector;
validatorSigs[4] = ValidatorFacet.setValidatorStatus.selector;
validatorSigs[5] = ValidatorFacet.getValidatorInfo.selector;
validatorSigs[6] = ValidatorFacet.getValidatorStats.selector;
validatorSigs[7] = ValidatorFacet.getUserValidators.selector;
validatorSigs[8] = ValidatorFacet.getAccruedCommission.selector;
validatorSigs[9] = ValidatorFacet.getValidatorsList.selector;
validatorSigs[10] = ValidatorFacet.getActiveValidatorCount.selector;
validatorSigs[11] = ValidatorFacet.requestCommissionClaim.selector;
validatorSigs[12] = ValidatorFacet.voteToSlashValidator.selector;
validatorSigs[13] = ValidatorFacet.slashValidator.selector;
bytes4[] memory managementSigs = new bytes4[](6); // Size reduced from 7 to 6
managementSigs[0] = ManagementFacet.setMinStakeAmount.selector;
managementSigs[1] = ManagementFacet.setCooldownInterval.selector;
managementSigs[2] = ManagementFacet.adminWithdraw.selector;
managementSigs[3] = ManagementFacet.getMinStakeAmount.selector; // Index shifted from 4
managementSigs[4] = ManagementFacet.getCooldownInterval.selector; // Index shifted from 5
managementSigs[5] = ManagementFacet.setMaxSlashVoteDuration.selector; // Index shifted from 6
// Define the Facet Cuts
cut[0] = IERC2535DiamondCutInternal.FacetCut({
target: address(accessControlFacet),
action: IERC2535DiamondCutInternal.FacetCutAction.ADD,
selectors: accessControlSigs
});
cut[1] = IERC2535DiamondCutInternal.FacetCut({
target: address(managementFacet),
action: IERC2535DiamondCutInternal.FacetCutAction.ADD,
selectors: managementSigs
});
cut[2] = IERC2535DiamondCutInternal.FacetCut({
target: address(stakingFacet),
action: IERC2535DiamondCutInternal.FacetCutAction.ADD,
selectors: stakingSigs
});
cut[3] = IERC2535DiamondCutInternal.FacetCut({
target: address(validatorFacet),
action: IERC2535DiamondCutInternal.FacetCutAction.ADD,
selectors: validatorSigs
});
cut[4] = IERC2535DiamondCutInternal.FacetCut({
target: address(rewardsFacet),
action: IERC2535DiamondCutInternal.FacetCutAction.ADD,
selectors: rewardsSigs
});
// 4. Execute Diamond Cut
ISolidStateDiamond(payable(address(diamondProxy))).diamondCut(cut, address(0), "");
console2.log("Diamond cut applied.");
// 5. Initialize
diamondProxy.initializePlume(
address(0),
MIN_STAKE,
INITIAL_COOLDOWN,
1 days, // maxSlashVoteDuration
50e16 // maxAllowedValidatorCommission (50%)
);
AccessControlFacet(address(diamondProxy)).initializeAccessControl();
AccessControlFacet(address(diamondProxy)).grantRole(PlumeRoles.ADMIN_ROLE, admin);
AccessControlFacet(address(diamondProxy)).grantRole(PlumeRoles.VALIDATOR_ROLE, admin);
AccessControlFacet(address(diamondProxy)).grantRole(PlumeRoles.REWARD_MANAGER_ROLE, admin); // Grant reward
// manager
AccessControlFacet(address(diamondProxy)).grantRole(PlumeRoles.TIMELOCK_ROLE, admin);
console2.log("Diamond initialized.");
// 6. Deploy and setup reward treasury
PlumeStakingRewardTreasury treasuryImpl = new PlumeStakingRewardTreasury();
bytes memory initData =
abi.encodeWithSelector(PlumeStakingRewardTreasury.initialize.selector, admin, address(diamondProxy));
PlumeStakingRewardTreasuryProxy treasuryProxy =
new PlumeStakingRewardTreasuryProxy(address(treasuryImpl), initData);
treasury = PlumeStakingRewardTreasury(payable(address(treasuryProxy)));
RewardsFacet(address(diamondProxy)).setTreasury(address(treasury));
console2.log("Treasury deployed and set.");
// 7. Setup Validators (15)
uint256 defaultMaxCapacity = 1_000_000_000 ether; // High capacity
for (uint16 i = 0; i < NUM_VALIDATORS; i++) {
address valAdmin = vm.addr(uint256(keccak256(abi.encodePacked("validatorAdmin", i))));
vm.deal(valAdmin, 1 ether); // Give admin some gas money
ValidatorFacet(address(diamondProxy)).addValidator(
i,
VALIDATOR_COMMISSION,
valAdmin, // Use unique admin
valAdmin, // Use same address for withdraw for simplicity
string(abi.encodePacked("l1val", i)),
string(abi.encodePacked("l1acc", i)),
vm.addr(uint256(keccak256(abi.encodePacked("l1evm", i)))),
defaultMaxCapacity
);
}
console2.log("%d validators added.", NUM_VALIDATORS);
// 8. Add PLUME_NATIVE as the only reward token
RewardsFacet(address(diamondProxy)).addRewardToken(PLUME_NATIVE, PLUME_REWARD_RATE_PER_SECOND, PLUME_REWARD_RATE_PER_SECOND*2);
treasury.addRewardToken(PLUME_NATIVE); // Also add to treasury allowed list
vm.deal(address(treasury), 1_000_000 ether); // Give treasury a large amount of native ETH for rewards
console2.log("PLUME_NATIVE reward token added and treasury funded.");
/*
// 9. Set reward rates for PLUME_NATIVE
address[] memory rewardTokens = new address[](1);
rewardTokens[0] = PLUME_NATIVE;
uint256[] memory rates = new uint256[](1);
rates[0] = PLUME_REWARD_RATE_PER_SECOND;
// Set Max Rate slightly higher just in case
RewardsFacet(address(diamondProxy)).setMaxRewardRate(PLUME_NATIVE, PLUME_REWARD_RATE_PER_SECOND * 2);
RewardsFacet(address(diamondProxy)).setRewardRates(rewardTokens, rates);
*/
console2.log("PLUME reward rate set.");
vm.stopPrank();
console2.log("Stress Test setup complete.");
user1 = makeAddr("user1");
user2 = makeAddr("user2");
user3 = makeAddr("user3");
user4 = makeAddr("user4");
alice = makeAddr("alice");
bob = makeAddr("bob");
charlie = makeAddr("charlie");
dave = makeAddr("dave");
// Fund users with ETH in setUp
uint256 ethAmount = 1000 ether;
vm.deal(user1, ethAmount);
vm.deal(user2, ethAmount);
vm.deal(user3, ethAmount);
vm.deal(user4, ethAmount);
vm.deal(validatorAdmin, ethAmount);
}
function testStakeAndUnstake() public {
uint256 amount = 100e18;
vm.startPrank(user1);
//Stake
StakingFacet(address(diamondProxy)).stake{value: amount}(
0
);
// Unstake
StakingFacet(address(diamondProxy)).unstake(0);
vm.stopPrank();
vm.warp(block.timestamp + INITIAL_COOLDOWN );
//Test gas cost of regular withraw
uint256 snapshot = vm.snapshot();
vm.startPrank(user1);
uint256 gasBefore = gasleft();
StakingFacet(address(diamondProxy)).withdraw();
uint256 gasAfter = gasleft();
uint256 gasUsedRegular = gasBefore - gasAfter;
console2.log("Gas used in regular withdraw");
console2.log(gasUsedRegular);
vm.stopPrank();
//Test gas cost of inflated withdraw
vm.revertTo(snapshot);
vm.startPrank(user2);
//User2 register multiple validators to user1
for (uint16 i = 0; i < NUM_VALIDATORS; i++) {
StakingFacet(address(diamondProxy)).stakeOnBehalf{value: MIN_STAKE}(i, user1);
}
vm.stopPrank();
vm.startPrank(user1);
gasBefore = gasleft();
StakingFacet(address(diamondProxy)).withdraw();
gasAfter = gasleft();
uint256 gasUsedInflated = gasBefore - gasAfter;
console2.log("Gas used in inflated withdraw");
console2.log(gasUsedInflated);
vm.stopPrank();
assertGt(gasUsedInflated, gasUsedRegular);
}
} // End Contract