diff --git a/.gas-report b/.gas-report index 4063044..f9ada36 100644 --- a/.gas-report +++ b/.gas-report @@ -41,6 +41,20 @@ | runForTest | 3759238 | 3759238 | 3759238 | 3759238 | 10 | ╰-----------------------------------------------------------+-----------------+---------+---------+---------+---------╯ +╭---------------------------------------------------------------+-----------------+---------+---------+---------+---------╮ +| script/DeployKarmaTiers.s.sol:DeployKarmaTiersScript Contract | | | | | | ++=========================================================================================================================+ +| Deployment Cost | Deployment Size | | | | | +|---------------------------------------------------------------+-----------------+---------+---------+---------+---------| +| 3542177 | 17392 | | | | | +|---------------------------------------------------------------+-----------------+---------+---------+---------+---------| +| | | | | | | +|---------------------------------------------------------------+-----------------+---------+---------+---------+---------| +| Function Name | Min | Avg | Median | Max | # Calls | +|---------------------------------------------------------------+-----------------+---------+---------+---------+---------| +| run | 2968362 | 2968362 | 2968362 | 2968362 | 33 | +╰---------------------------------------------------------------+-----------------+---------+---------+---------+---------╯ + ╭-------------------------------------------------------------------+-----------------+---------+---------+---------+---------╮ | script/DeployStakeManager.s.sol:DeployStakeManagerScript Contract | | | | | | +=============================================================================================================================+ @@ -66,7 +80,7 @@ |---------------------------------------------------------+-----------------+------+--------+------+---------| | Function Name | Min | Avg | Median | Max | # Calls | |---------------------------------------------------------+-----------------+------+--------+------+---------| -| activeNetworkConfig | 455 | 2022 | 455 | 4455 | 462 | +| activeNetworkConfig | 455 | 2076 | 455 | 4455 | 528 | ╰---------------------------------------------------------+-----------------+------+--------+------+---------╯ ╭---------------------------------------------------------------------+-----------------+---------+---------+---------+---------╮ @@ -173,6 +187,36 @@ | transferFrom | 530 | 530 | 530 | 530 | 1 | ╰-------------------------------------------------+-----------------+-------+--------+-------+---------╯ +╭----------------------------------------+-----------------+--------+--------+--------+---------╮ +| src/KarmaTiers.sol:KarmaTiers Contract | | | | | | ++===============================================================================================+ +| Deployment Cost | Deployment Size | | | | | +|----------------------------------------+-----------------+--------+--------+--------+---------| +| 0 | 5856 | | | | | +|----------------------------------------+-----------------+--------+--------+--------+---------| +| | | | | | | +|----------------------------------------+-----------------+--------+--------+--------+---------| +| Function Name | Min | Avg | Median | Max | # Calls | +|----------------------------------------+-----------------+--------+--------+--------+---------| +| MAX_TIER_NAME_LENGTH | 240 | 240 | 240 | 240 | 1 | +|----------------------------------------+-----------------+--------+--------+--------+---------| +| activateTier | 23752 | 27234 | 25870 | 32081 | 3 | +|----------------------------------------+-----------------+--------+--------+--------+---------| +| addTier | 24619 | 138647 | 140518 | 163301 | 563 | +|----------------------------------------+-----------------+--------+--------+--------+---------| +| currentTierId | 2326 | 2326 | 2326 | 2326 | 2 | +|----------------------------------------+-----------------+--------+--------+--------+---------| +| deactivateTier | 23787 | 30495 | 32110 | 32110 | 9 | +|----------------------------------------+-----------------+--------+--------+--------+---------| +| getTierById | 555 | 12473 | 12448 | 14510 | 520 | +|----------------------------------------+-----------------+--------+--------+--------+---------| +| getTierCount | 2365 | 2365 | 2365 | 2365 | 5 | +|----------------------------------------+-----------------+--------+--------+--------+---------| +| getTierIdByKarmaBalance | 50511 | 50598 | 50618 | 50645 | 4 | +|----------------------------------------+-----------------+--------+--------+--------+---------| +| updateTier | 25139 | 52773 | 51895 | 74613 | 261 | +╰----------------------------------------+-----------------+--------+--------+--------+---------╯ + ╭--------------------------------------------+-----------------+--------+--------+--------+---------╮ | src/StakeManager.sol:StakeManager Contract | | | | | | +===================================================================================================+ @@ -200,7 +244,7 @@ |--------------------------------------------+-----------------+--------+--------+--------+---------| | getAccountVaults | 5230 | 5230 | 5230 | 5230 | 4 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| getVault | 13653 | 13653 | 13653 | 13653 | 4182 | +| getVault | 13653 | 13653 | 13653 | 13653 | 4181 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | initialize | 92752 | 92752 | 92752 | 92752 | 95 | |--------------------------------------------+-----------------+--------+--------+--------+---------| @@ -208,7 +252,7 @@ |--------------------------------------------+-----------------+--------+--------+--------+---------| | leave | 66348 | 66348 | 66348 | 66348 | 2 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| lock | 7040 | 43452 | 46713 | 87964 | 1034 | +| lock | 7040 | 43275 | 46713 | 87673 | 1034 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | migrateToVault | 9294 | 53513 | 17021 | 170715 | 4 | |--------------------------------------------+-----------------+--------+--------+--------+---------| @@ -236,17 +280,17 @@ |--------------------------------------------+-----------------+--------+--------+--------+---------| | setTrustedCodehash | 24238 | 24238 | 24238 | 24238 | 95 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| stake | 2639 | 131319 | 60725 | 228623 | 2670 | +| stake | 2639 | 130108 | 60725 | 228623 | 2670 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | stakedBalanceOf | 2622 | 2622 | 2622 | 2622 | 1 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | totalMP | 6805 | 8257 | 8257 | 9710 | 6 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| totalMPAccrued | 2385 | 2385 | 2385 | 2385 | 4162 | +| totalMPAccrued | 2385 | 2385 | 2385 | 2385 | 4161 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| totalMPStaked | 2429 | 2429 | 2429 | 2429 | 4165 | +| totalMPStaked | 2429 | 2429 | 2429 | 2429 | 4164 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| totalMaxMP | 2407 | 2407 | 2407 | 2407 | 4162 | +| totalMaxMP | 2407 | 2407 | 2407 | 2407 | 4161 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | totalRewardsAccrued | 2407 | 2407 | 2407 | 2407 | 3 | |--------------------------------------------+-----------------+--------+--------+--------+---------| @@ -254,15 +298,15 @@ |--------------------------------------------+-----------------+--------+--------+--------+---------| | totalShares | 4597 | 4597 | 4597 | 4597 | 6 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| totalStaked | 2408 | 2408 | 2408 | 2408 | 4169 | +| totalStaked | 2408 | 2408 | 2408 | 2408 | 4168 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| unstake | 9886 | 41365 | 39781 | 79550 | 271 | +| unstake | 9886 | 41581 | 39781 | 79550 | 271 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | updateAccount | 347677 | 347677 | 347677 | 347677 | 1 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | updateGlobalState | 15820 | 25876 | 29230 | 29230 | 8 | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| updateVault | 31948 | 34373 | 31948 | 110579 | 1024 | +| updateVault | 31948 | 34036 | 31948 | 110579 | 1023 | |--------------------------------------------+-----------------+--------+--------+--------+---------| | upgradeTo | 10279 | 10772 | 10279 | 12745 | 5 | |--------------------------------------------+-----------------+--------+--------+--------+---------| @@ -290,7 +334,7 @@ |----------------------------------------+-----------------+--------+--------+--------+---------| | leave | 12223 | 113137 | 84120 | 356508 | 5 | |----------------------------------------+-----------------+--------+--------+--------+---------| -| lock | 12151 | 58945 | 62251 | 103499 | 1035 | +| lock | 12151 | 58767 | 62251 | 103208 | 1035 | |----------------------------------------+-----------------+--------+--------+--------+---------| | lockUntil | 2363 | 2363 | 2363 | 2363 | 7768 | |----------------------------------------+-----------------+--------+--------+--------+---------| @@ -300,15 +344,15 @@ |----------------------------------------+-----------------+--------+--------+--------+---------| | register | 12742 | 78218 | 78761 | 78761 | 374 | |----------------------------------------+-----------------+--------+--------+--------+---------| -| stake | 12131 | 165586 | 76290 | 284275 | 2671 | +| stake | 12131 | 164075 | 76290 | 284275 | 2671 | |----------------------------------------+-----------------+--------+--------+--------+---------| | stakeManager | 393 | 393 | 393 | 393 | 373 | |----------------------------------------+-----------------+--------+--------+--------+---------| | trustStakeManager | 7650 | 7650 | 7650 | 7650 | 1 | |----------------------------------------+-----------------+--------+--------+--------+---------| -| unstake | 12108 | 58033 | 55296 | 110656 | 272 | +| unstake | 12108 | 58192 | 55296 | 110656 | 272 | |----------------------------------------+-----------------+--------+--------+--------+---------| -| updateLockUntil | 4432 | 20797 | 21532 | 21532 | 508 | +| updateLockUntil | 4432 | 20802 | 21532 | 21532 | 488 | |----------------------------------------+-----------------+--------+--------+--------+---------| | withdraw | 20817 | 20817 | 20817 | 20817 | 1 | |----------------------------------------+-----------------+--------+--------+--------+---------| @@ -326,9 +370,9 @@ |----------------------------------------------------+-----------------+-------+--------+--------+---------| | Function Name | Min | Avg | Median | Max | # Calls | |----------------------------------------------------+-----------------+-------+--------+--------+---------| -| fallback | 5208 | 12834 | 7353 | 374054 | 23167 | +| fallback | 5208 | 12818 | 7353 | 374054 | 23161 | |----------------------------------------------------+-----------------+-------+--------+--------+---------| -| implementation | 346 | 2137 | 2346 | 2346 | 4870 | +| implementation | 346 | 2144 | 2346 | 2346 | 4850 | ╰----------------------------------------------------+-----------------+-------+--------+--------+---------╯ ╭--------------------------------------------+-----------------+--------+--------+--------+---------╮ @@ -510,7 +554,7 @@ |---------------------------------------------+-----------------+-------+--------+-------+---------| | approve | 29075 | 31545 | 29183 | 46259 | 2676 | |---------------------------------------------+-----------------+-------+--------+-------+---------| -| balanceOf | 2561 | 2561 | 2561 | 2561 | 4960 | +| balanceOf | 2561 | 2561 | 2561 | 2561 | 4959 | |---------------------------------------------+-----------------+-------+--------+-------+---------| | mint | 33964 | 37190 | 34072 | 68248 | 2685 | ╰---------------------------------------------+-----------------+-------+--------+-------+---------╯ diff --git a/.gas-snapshot b/.gas-snapshot index 75ff4e5..640a5ef 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,3 +1,6 @@ +ActivateTierTests:test_ActivateTier_RevertWhen_InvalidTierId() (gas: 36310) +ActivateTierTests:test_ActivateTier_RevertWhen_NotOwner() (gas: 210441) +ActivateTierTests:test_ActivateTier_Success() (gas: 234495) AddRewardDistributorTest:testAddKarmaDistributorOnlyAdmin() (gas: 438258) AddRewardDistributorTest:testAddRewardDistributorAsOtherAdmin() (gas: 182935) AddRewardDistributorTest:testBalanceOf() (gas: 456642) @@ -8,6 +11,20 @@ AddRewardDistributorTest:testRemoveUnknownKarmaDistributor() (gas: 41666) AddRewardDistributorTest:testTotalSupply() (gas: 359391) AddRewardDistributorTest:testTransfersNotAllowed() (gas: 61947) AddRewardDistributorTest:test_RevertWhen_SenderIsNotDefaultAdmin() (gas: 68406) +AddTierTests:test_AddTier_MultipleSuccessiveTiers() (gas: 421808) +AddTierTests:test_AddTier_RevertWhen_EmptyName() (gas: 35216) +AddTierTests:test_AddTier_RevertWhen_InvalidRange() (gas: 36148) +AddTierTests:test_AddTier_RevertWhen_InvalidRangeEqual() (gas: 36214) +AddTierTests:test_AddTier_RevertWhen_NotOwner() (gas: 35540) +AddTierTests:test_AddTier_RevertWhen_OverlappingTiers() (gas: 189527) +AddTierTests:test_AddTier_RevertWhen_TierNameTooLong() (gas: 42578) +AddTierTests:test_AddTier_Success() (gas: 180523) +AddTierTests:test_AddTier_UnlimitedMaxKarma() (gas: 147634) +DeactivateActivateTierTests:test_DeactivateTier_RevertWhen_InvalidTierId() (gas: 36377) +DeactivateActivateTierTests:test_DeactivateTier_RevertWhen_NotOwner() (gas: 177408) +DeactivateActivateTierTests:test_DeactivateTier_Success() (gas: 201619) +EdgeCasesTest:test_OverlapValidation_EdgeCases() (gas: 312965) +EdgeCasesTest:test_UnlimitedTierOverlap() (gas: 208274) EmergencyExitTest:test_CannotEnableEmergencyModeTwice() (gas: 93554) EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 336067) EmergencyExitTest:test_EmergencyExitBasic() (gas: 524580) @@ -16,15 +33,21 @@ EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 479110) EmergencyExitTest:test_EmergencyExitWithLock() (gas: 452444) EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 484810) EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 39176) -FuzzTests:testFuzz_AccrueMP(uint128,uint64,uint64) (runs: 1009, μ: 583242, ~: 549046) -FuzzTests:testFuzz_AccrueMP_Relock(uint128,uint64,uint64,uint64) (runs: 1009, μ: 808244, ~: 777237) -FuzzTests:testFuzz_EmergencyExit(uint256,uint256) (runs: 1001, μ: 588167, ~: 578267) -FuzzTests:testFuzz_Lock(uint256,uint64) (runs: 1008, μ: 961506, ~: 961235) -FuzzTests:testFuzz_Relock(uint256,uint64,uint64) (runs: 1008, μ: 600126, ~: 574225) -FuzzTests:testFuzz_Rewards(uint256,uint256,uint256,uint16,uint16) (runs: 1001, μ: 650444, ~: 653254) -FuzzTests:testFuzz_Stake(uint256,uint64) (runs: 1008, μ: 377931, ~: 346087) -FuzzTests:testFuzz_Unstake(uint128,uint64,uint16,uint128) (runs: 1009, μ: 803049, ~: 780598) -FuzzTests:testFuzz_UpdateVault(uint128,uint64,uint64) (runs: 1009, μ: 583265, ~: 549069) +FuzzTests:testFuzz_AccrueMP(uint128,uint64,uint64) (runs: 1002, μ: 581651, ~: 549041) +FuzzTests:testFuzz_AccrueMP_Relock(uint128,uint64,uint64,uint64) (runs: 1002, μ: 806522, ~: 777234) +FuzzTests:testFuzz_AddTier_ValidInputs(string,uint256,uint256,uint32) (runs: 1001, μ: 171310, ~: 171538) +FuzzTests:testFuzz_EmergencyExit(uint256,uint256) (runs: 1000, μ: 588178, ~: 578267) +FuzzTests:testFuzz_Lock(uint256,uint64) (runs: 1002, μ: 961278, ~: 961235) +FuzzTests:testFuzz_Relock(uint256,uint64,uint64) (runs: 1002, μ: 598418, ~: 574225) +FuzzTests:testFuzz_Rewards(uint256,uint256,uint256,uint16,uint16) (runs: 1000, μ: 650441, ~: 653254) +FuzzTests:testFuzz_Stake(uint256,uint64) (runs: 1002, μ: 376429, ~: 346087) +FuzzTests:testFuzz_Unstake(uint128,uint64,uint16,uint128) (runs: 1002, μ: 801209, ~: 780598) +FuzzTests:testFuzz_UpdateTier_ValidInputs(string,uint256,uint256,string,uint256,uint256,uint32,uint32) (runs: 1000, μ: 226513, ~: 225288) +FuzzTests:testFuzz_UpdateVault(uint128,uint64,uint64) (runs: 1002, μ: 581674, ~: 549064) +GetTierIdByKarmaBalanceTest:test_GetTierIdByKarmaBalance_BelowMinKarma() (gas: 58622) +GetTierIdByKarmaBalanceTest:test_GetTierIdByKarmaBalance_InBronzeTier() (gas: 58727) +GetTierIdByKarmaBalanceTest:test_GetTierIdByKarmaBalance_InGoldTier() (gas: 58767) +GetTierIdByKarmaBalanceTest:test_GetTierIdByKarmaBalance_InSilverTier() (gas: 58759) IntegrationTest:testStakeFoo() (gas: 2348931) KarmaNFTTest:testApproveNotAllowed() (gas: 10507) KarmaNFTTest:testGetApproved() (gas: 10531) @@ -58,7 +81,7 @@ LeaveTest:test_LeaveShouldKeepFundsLockedInStakeVault() (gas: 9938411) LeaveTest:test_LeaveShouldProperlyUpdateAccounting() (gas: 10011059) LeaveTest:test_RevertWhenStakeManagerIsTrusted() (gas: 333238) LeaveTest:test_TrustNewStakeManager() (gas: 9944491) -LockTest:test_LockFailsWithInvalidPeriod(uint256) (runs: 1008, μ: 384560, ~: 384588) +LockTest:test_LockFailsWithInvalidPeriod(uint256) (runs: 1002, μ: 384561, ~: 384588) LockTest:test_LockFailsWithNoStake() (gas: 89700) LockTest:test_LockFailsWithZero() (gas: 343310) LockTest:test_LockMultipleTimesExceedMaxLock() (gas: 746921) @@ -133,7 +156,7 @@ SetRewardTest:test_RevertWhen_SenderIsNotOperator() (gas: 61893) SlashAmountOfTest:testAddKarmaDistributorOnlyAdmin() (gas: 438224) SlashAmountOfTest:testBalanceOf() (gas: 456642) SlashAmountOfTest:testBalanceOfWithNoSystemTotalKarma() (gas: 83783) -SlashAmountOfTest:testFuzz_SlashAmountOf(uint256,uint256,uint256) (runs: 1002, μ: 407788, ~: 408571) +SlashAmountOfTest:testFuzz_SlashAmountOf(uint256,uint256,uint256) (runs: 1000, μ: 407786, ~: 408571) SlashAmountOfTest:testMintOnlyAdmin() (gas: 429075) SlashAmountOfTest:testRemoveKarmaDistributorOnlyOwner() (gas: 163437) SlashAmountOfTest:testRemoveUnknownKarmaDistributor() (gas: 41654) @@ -143,7 +166,7 @@ SlashAmountOfTest:test_SlashAmountOf() (gas: 327608) SlashTest:testAddKarmaDistributorOnlyAdmin() (gas: 438270) SlashTest:testBalanceOf() (gas: 456648) SlashTest:testBalanceOfWithNoSystemTotalKarma() (gas: 83827) -SlashTest:testFuzz_Slash(uint256) (runs: 1009, μ: 280204, ~: 280146) +SlashTest:testFuzz_Slash(uint256) (runs: 1002, μ: 280204, ~: 280146) SlashTest:testMintOnlyAdmin() (gas: 429131) SlashTest:testRemoveKarmaDistributorOnlyOwner() (gas: 163461) SlashTest:testRemoveRewardDistributorShouldReduceSlashAmount() (gas: 610762) @@ -206,9 +229,19 @@ UnstakeTest:test_UnstakeOneAccount() (gas: 759178) UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 719489) UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 673681) UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 722241) +UpdateTierTests:test_UpdateTier_RevertWhen_InvalidRange() (gas: 38505) +UpdateTierTests:test_UpdateTier_RevertWhen_InvalidTierId() (gas: 37814) +UpdateTierTests:test_UpdateTier_RevertWhen_NotOwner() (gas: 35753) +UpdateTierTests:test_UpdateTier_RevertWhen_OverlapWithOtherTier() (gas: 183290) +UpdateTierTests:test_UpdateTier_Success() (gas: 83795) UpdateVaultTest:test_UpdateAccount() (gas: 2587427) UpgradeTest:test_RevertWhenNotOwner() (gas: 3696209) UpgradeTest:test_UpgradeStakeManager() (gas: 9855347) VaultRegistrationTest:test_VaultRegistration() (gas: 90138) +ViewFunctionsTest:test_GetTierById_RevertWhen_InvalidTierId() (gas: 13153) +ViewFunctionsTest:test_GetTierById_RevertWhen_TierIdZero() (gas: 11056) +ViewFunctionsTest:test_GetTierById_Success() (gas: 169210) +ViewFunctionsTest:test_GetTierCount_IncreasesWithTiers() (gas: 299987) +ViewFunctionsTest:test_GetTierCount_InitiallyZero() (gas: 10505) WithdrawTest:testOwner() (gas: 15365) WithdrawTest:test_CannotWithdrawStakedFunds() (gas: 373408) \ No newline at end of file diff --git a/script/DeployKarmaTiers.s.sol b/script/DeployKarmaTiers.s.sol new file mode 100644 index 0000000..534e75f --- /dev/null +++ b/script/DeployKarmaTiers.s.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { BaseScript } from "./Base.s.sol"; +import { DeploymentConfig } from "./DeploymentConfig.s.sol"; + +import { KarmaTiers } from "../src/KarmaTiers.sol"; + +contract DeployKarmaTiersScript is BaseScript { + function run() public returns (KarmaTiers, DeploymentConfig) { + DeploymentConfig deploymentConfig = new DeploymentConfig(broadcaster); + (address deployer,) = deploymentConfig.activeNetworkConfig(); + + vm.startBroadcast(deployer); + + KarmaTiers karmaTiers = new KarmaTiers(); + + vm.stopBroadcast(); + + return (karmaTiers, deploymentConfig); + } +} diff --git a/src/KarmaTiers.sol b/src/KarmaTiers.sol new file mode 100644 index 0000000..744585d --- /dev/null +++ b/src/KarmaTiers.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @title KarmaTiers + * @dev Manages tier system based on Karma token balance thresholds + * @notice This contract allows efficient tier lookup for L2 nodes and tier management + */ +contract KarmaTiers is Ownable { + /// @notice Emitted when a new tier is added + event TierAdded(uint8 indexed tierId, string name, uint256 minKarma, uint256 maxKarma, uint32 txPerEpoch); + /// @notice Emitted when a tier is updated + event TierUpdated(uint8 indexed tierId, string name, uint256 minKarma, uint256 maxKarma, uint32 txPerEpoch); + /// @notice Emitted when a tier is deactivated + event TierDeactivated(uint8 indexed tierId); + /// @notice Emitted when a tier is activated + event TierActivated(uint8 indexed tierId); + + /// @notice Emitted when a transaction amount is invalid + error InvalidTxAmount(); + /// @notice Emitted when a tier name is empty + error EmptyTierName(); + /// @notice Emitted when a tier is not found + error TierNotFound(); + /// @notice Emitted when a tier name exceeds maximum length + error TierNameTooLong(uint256 nameLength, uint256 maxLength); + /// @notice Emitted when a new tier overlaps with an existing one + error OverlappingTiers(uint8 existingTierId, uint256 newMinKarma, uint256 newMaxKarma); + /// @notice Emitted when a tier's minKarma is greater than or equal to maxKarma + error InvalidTierRange(uint256 minKarma, uint256 maxKarma); + + struct Tier { + uint256 minKarma; + uint256 maxKarma; + string name; + uint32 txPerEpoch; + bool active; + } + + modifier onlyValidTierId(uint8 tierId) { + if (tierId == 0 || tierId > currentTierId) { + revert TierNotFound(); + } + _; + } + + /*////////////////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Maximum length for tier names + uint256 public constant MAX_TIER_NAME_LENGTH = 32; + + /*////////////////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Mapping of tier IDs to Tier structs + mapping(uint8 id => Tier tier) public tiers; + /// @notice Current tier ID, incremented with each new tier added + uint8 public currentTierId; + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + + constructor() { + transferOwnership(msg.sender); + } + + /*////////////////////////////////////////////////////////////////////////// + USER-FACING FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /** + * @dev Add a new tier to the system + * @param name The name of the tier + * @param minKarma Minimum Karma required for this tier + * @param maxKarma Maximum Karma for this tier (0 for unlimited) + */ + function addTier(string calldata name, uint256 minKarma, uint256 maxKarma, uint32 txPerEpoch) external onlyOwner { + if (bytes(name).length == 0) revert EmptyTierName(); + if (maxKarma != 0 && maxKarma <= minKarma) revert InvalidTierRange(minKarma, maxKarma); + if (txPerEpoch == 0) revert InvalidTxAmount(); + + // Check for overlaps with existing tiers + _validateNoOverlap(minKarma, maxKarma, type(uint8).max); + _validateTierName(name); + + currentTierId++; + + tiers[currentTierId] = + Tier({ minKarma: minKarma, maxKarma: maxKarma, name: name, active: true, txPerEpoch: txPerEpoch }); + + emit TierAdded(currentTierId, name, minKarma, maxKarma, txPerEpoch); + } + + /** + * @dev Update an existing tier + * @param name The name of the tier to update + * @param newMinKarma New minimum Karma requirement + * @param newMaxKarma New maximum Karma (0 for unlimited) + */ + function updateTier( + uint8 tierId, + string calldata name, + uint256 newMinKarma, + uint256 newMaxKarma, + uint32 newTxPerEpoch + ) + external + onlyOwner + onlyValidTierId(tierId) + { + if (newMaxKarma != 0 && newMaxKarma <= newMinKarma) revert InvalidTierRange(newMinKarma, newMaxKarma); + if (newTxPerEpoch == 0) revert InvalidTxAmount(); + + // Check for overlaps with other tiers (excluding the one being updated) + _validateNoOverlap(newMinKarma, newMaxKarma, tierId); + _validateTierName(name); + + tiers[tierId].name = name; + tiers[tierId].minKarma = newMinKarma; + tiers[tierId].maxKarma = newMaxKarma; + tiers[tierId].txPerEpoch = newTxPerEpoch; + + emit TierUpdated(tierId, name, newMinKarma, newMaxKarma, newTxPerEpoch); + } + + /** + * @dev Deactivate a tier (keeps it in storage but marks as inactive) + * @param tierId The ID of the tier to deactivate + */ + function deactivateTier(uint8 tierId) external onlyOwner onlyValidTierId(tierId) { + tiers[tierId].active = false; + emit TierDeactivated(tierId); + } + + /** + * @dev Reactivate a tier + * @param tierId The ID of the tier to reactivate + */ + function activateTier(uint8 tierId) external onlyOwner onlyValidTierId(tierId) { + tiers[tierId].active = true; + emit TierActivated(tierId); + } + + /*////////////////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /** + * @dev Validate tier name length and content + * @param name The tier name to validate + */ + function _validateTierName(string calldata name) internal pure { + bytes memory nameBytes = bytes(name); + if (nameBytes.length == 0) revert EmptyTierName(); + if (nameBytes.length > MAX_TIER_NAME_LENGTH) { + revert TierNameTooLong(nameBytes.length, MAX_TIER_NAME_LENGTH); + } + } + + /*////////////////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////////////////*/ + + /** + * @notice Get tier by karma balance. + * @dev This function returns the highest tier ID that the user qualifies for based on their karma balance. + * @param karmaBalance The karma balance to check + * @return tierId The tier id that matches the karma balance + */ + function getTierIdByKarmaBalance(uint256 karmaBalance) external view returns (uint8) { + uint8 bestTierId = 0; + uint256 bestMinKarma = 0; + + for (uint8 i = 1; i <= currentTierId; i++) { + Tier memory currentTier = tiers[i]; + if (!currentTier.active) continue; + + // Check if user meets the minimum requirement for this tier + if (karmaBalance >= currentTier.minKarma) { + // Only update if this tier has a higher minKarma requirement + if (currentTier.minKarma > bestMinKarma) { + bestTierId = i; + bestMinKarma = currentTier.minKarma; + } + } + } + return bestTierId; + } + + /** + * @dev Get tier count + * @return count Total number of tiers (including inactive) + */ + function getTierCount() external view returns (uint256 count) { + return currentTierId; + } + + /** + * @dev Get tier by id + * @param tierId The ID of the tier to retrieve + * @return tier The tier information + */ + function getTierById(uint8 tierId) external view onlyValidTierId(tierId) returns (Tier memory tier) { + return tiers[tierId]; + } + + /** + * @dev Internal function to validate no overlap exists + * @param minKarma Minimum Karma for the tier + * @param maxKarma Maximum Karma for the tier (0 = unlimited) + */ + function _validateNoOverlap(uint256 minKarma, uint256 maxKarma, uint8 excludeTierId) internal view { + for (uint8 i = 1; i <= currentTierId; i++) { + if (i == excludeTierId || !tiers[i].active) continue; + + Tier memory existingTier = tiers[i]; + uint256 existingMax = existingTier.maxKarma == 0 ? type(uint256).max : existingTier.maxKarma; + uint256 newMax = maxKarma == 0 ? type(uint256).max : maxKarma; + + // Check for overlap using: NOT (no overlap) = overlap + // No overlap means: newMax < existingMin OR newMin > existingMax + if (!(newMax < existingTier.minKarma || minKarma > existingMax)) { + revert OverlappingTiers(i, minKarma, maxKarma); + } + } + } +} diff --git a/test/KarmaTiers.t.sol b/test/KarmaTiers.t.sol new file mode 100644 index 0000000..6947bd5 --- /dev/null +++ b/test/KarmaTiers.t.sol @@ -0,0 +1,436 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { Test } from "forge-std/Test.sol"; +import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; +import { DeployKarmaTiersScript } from "../script/DeployKarmaTiers.s.sol"; +import { KarmaTiers } from "../src/KarmaTiers.sol"; + +contract KarmaTiersTest is Test { + KarmaTiers public karmaTiers; + address public deployer; + address public nonOwner = makeAddr("nonOwner"); + + event TierAdded(uint8 indexed tierId, string name, uint256 minKarma, uint256 maxKarma, uint32 txPerEpoch); + event TierUpdated(uint8 indexed tierId, string name, uint256 minKarma, uint256 maxKarma, uint32 txPerEpoch); + event TierDeactivated(uint8 indexed tierId); + event TierActivated(uint8 indexed tierId); + + function setUp() public virtual { + DeployKarmaTiersScript deployment = new DeployKarmaTiersScript(); + (KarmaTiers _karmaTiers, DeploymentConfig deploymentConfig) = deployment.run(); + (address _deployer,) = deploymentConfig.activeNetworkConfig(); + deployer = _deployer; + karmaTiers = _karmaTiers; + } +} + +contract AddTierTests is KarmaTiersTest { + function setUp() public override { + super.setUp(); + } + + function test_AddTier_RevertWhen_EmptyName() public { + vm.prank(deployer); + vm.expectRevert(KarmaTiers.EmptyTierName.selector); + karmaTiers.addTier("", 100, 500, 5); + } + + function test_AddTier_RevertWhen_InvalidRange() public { + vm.prank(deployer); + vm.expectRevert(abi.encodeWithSelector(KarmaTiers.InvalidTierRange.selector, 500, 100)); + karmaTiers.addTier("Invalid", 500, 100, 5); + } + + function test_AddTier_RevertWhen_InvalidRangeEqual() public { + vm.prank(deployer); + vm.expectRevert(abi.encodeWithSelector(KarmaTiers.InvalidTierRange.selector, 500, 500)); + karmaTiers.addTier("Invalid", 500, 500, 5); + } + + function test_AddTier_RevertWhen_OverlappingTiers() public { + vm.prank(deployer); + karmaTiers.addTier("Bronze", 100, 500, 5); + + vm.prank(deployer); + vm.expectRevert(abi.encodeWithSelector(KarmaTiers.OverlappingTiers.selector, 1, 400, 600)); + karmaTiers.addTier("Silver", 400, 600, 5); + } + + function test_AddTier_RevertWhen_NotOwner() public { + vm.prank(nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + karmaTiers.addTier("Bronze", 100, 500, 5); + } + + function test_AddTier_RevertWhen_TierNameTooLong() public { + string memory longName = "ThisIsAVeryLongTierNameThatExceedsTheMaximumAllowedLength"; + + vm.expectRevert( + abi.encodeWithSelector( + KarmaTiers.TierNameTooLong.selector, bytes(longName).length, karmaTiers.MAX_TIER_NAME_LENGTH() + ) + ); + vm.prank(deployer); + karmaTiers.addTier(longName, 100, 500, 5); + } + + function test_AddTier_Success() public { + string memory tierName = "Bronze"; + uint256 minKarma = 100; + uint256 maxKarma = 500; + uint32 txPerEpoch = 5; + + vm.expectEmit(true, false, false, true); + emit TierAdded(1, tierName, minKarma, maxKarma, txPerEpoch); + + vm.prank(deployer); + karmaTiers.addTier(tierName, minKarma, maxKarma, txPerEpoch); + + assertEq(karmaTiers.currentTierId(), 1); + + KarmaTiers.Tier memory tier = karmaTiers.getTierById(1); + assertEq(tier.name, tierName); + assertEq(tier.minKarma, minKarma); + assertEq(tier.maxKarma, maxKarma); + assertTrue(tier.active); + } + + function test_AddTier_UnlimitedMaxKarma() public { + string memory tierName = "Unlimited"; + uint256 minKarma = 1000; + uint256 maxKarma = 0; // 0 means unlimited + uint32 txPerEpoch = 5; + + vm.prank(deployer); + karmaTiers.addTier(tierName, minKarma, maxKarma, txPerEpoch); + + KarmaTiers.Tier memory tier = karmaTiers.getTierById(1); + assertEq(tier.maxKarma, 0); + } + + function test_AddTier_MultipleSuccessiveTiers() public { + vm.startPrank(deployer); + karmaTiers.addTier("Bronze", 0, 100, 5); + karmaTiers.addTier("Silver", 101, 500, 5); + karmaTiers.addTier("Gold", 501, 1000, 5); + vm.stopPrank(); + + assertEq(karmaTiers.currentTierId(), 3); + assertEq(karmaTiers.getTierCount(), 3); + } +} + +contract UpdateTierTests is KarmaTiersTest { + function setUp() public override { + super.setUp(); + vm.prank(deployer); + karmaTiers.addTier("Bronze", 100, 500, 5); // Add a tier to update + } + + function test_UpdateTier_RevertWhen_InvalidTierId() public { + vm.expectRevert(KarmaTiers.TierNotFound.selector); + vm.prank(deployer); + karmaTiers.updateTier(2, "Bronze", 100, 500, 5); + } + + function test_UpdateTier_RevertWhen_InvalidRange() public { + vm.expectRevert(abi.encodeWithSelector(KarmaTiers.InvalidTierRange.selector, 600, 400)); + vm.prank(deployer); + karmaTiers.updateTier(1, "Bronze", 600, 400, 5); + } + + function test_UpdateTier_RevertWhen_OverlapWithOtherTier() public { + vm.startPrank(deployer); + karmaTiers.addTier("Silver", 600, 1000, 5); + vm.stopPrank(); + + vm.expectRevert(abi.encodeWithSelector(KarmaTiers.OverlappingTiers.selector, 2, 550, 800)); + vm.prank(deployer); + karmaTiers.updateTier(1, "Bronze", 550, 800, 5); + } + + function test_UpdateTier_RevertWhen_NotOwner() public { + vm.prank(nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + karmaTiers.updateTier(1, "Updated Bronze", 150, 600, 5); + } + + function test_UpdateTier_Success() public { + string memory newName = "Updated Bronze"; + uint256 newMinKarma = 150; + uint256 newMaxKarma = 600; + uint32 newTxPerEpoch = 10; + + vm.expectEmit(true, false, false, true); + emit TierUpdated(1, newName, newMinKarma, newMaxKarma, newTxPerEpoch); + + vm.prank(deployer); + karmaTiers.updateTier(1, newName, newMinKarma, newMaxKarma, newTxPerEpoch); + + KarmaTiers.Tier memory tier = karmaTiers.getTierById(1); + assertEq(tier.name, newName); + assertEq(tier.minKarma, newMinKarma); + assertEq(tier.maxKarma, newMaxKarma); + } +} + +contract DeactivateActivateTierTests is KarmaTiersTest { + function setUp() public override { + super.setUp(); + } + + function test_DeactivateTier_RevertWhen_InvalidTierId() public { + vm.expectRevert(KarmaTiers.TierNotFound.selector); + vm.prank(deployer); + karmaTiers.deactivateTier(1); + } + + function test_DeactivateTier_RevertWhen_NotOwner() public { + vm.prank(deployer); + karmaTiers.addTier("Bronze", 100, 500, 5); + + vm.prank(nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + karmaTiers.deactivateTier(1); + } + + function test_DeactivateTier_Success() public { + vm.prank(deployer); + karmaTiers.addTier("Bronze", 100, 500, 5); + + vm.expectEmit(true, false, false, false); + emit TierDeactivated(1); + + vm.prank(deployer); + karmaTiers.deactivateTier(1); + + KarmaTiers.Tier memory tier = karmaTiers.getTierById(1); + assertFalse(tier.active); + } +} + +contract ActivateTierTests is KarmaTiersTest { + function setUp() public override { + super.setUp(); + } + + function test_ActivateTier_Success() public { + vm.startPrank(deployer); + karmaTiers.addTier("Bronze", 100, 500, 5); + karmaTiers.deactivateTier(1); + vm.stopPrank(); + + vm.expectEmit(true, false, false, false); + emit TierActivated(1); + + vm.prank(deployer); + karmaTiers.activateTier(1); + + KarmaTiers.Tier memory tier = karmaTiers.getTierById(1); + assertTrue(tier.active); + } + + function test_ActivateTier_RevertWhen_InvalidTierId() public { + vm.expectRevert(KarmaTiers.TierNotFound.selector); + vm.prank(deployer); + karmaTiers.activateTier(1); + } + + function test_ActivateTier_RevertWhen_NotOwner() public { + vm.prank(deployer); + karmaTiers.addTier("Bronze", 100, 500, 5); + vm.prank(deployer); + karmaTiers.deactivateTier(1); + + vm.prank(nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + karmaTiers.activateTier(1); + } +} + +contract ViewFunctionsTest is KarmaTiersTest { + function setUp() public override { + super.setUp(); + } + + function test_GetTierCount_InitiallyZero() public { + assertEq(karmaTiers.getTierCount(), 0); + } + + function test_GetTierCount_IncreasesWithTiers() public { + vm.prank(deployer); + karmaTiers.addTier("Bronze", 100, 500, 5); + assertEq(karmaTiers.getTierCount(), 1); + + vm.prank(deployer); + karmaTiers.addTier("Silver", 600, 1000, 5); + assertEq(karmaTiers.getTierCount(), 2); + } + + function test_GetTierById_Success() public { + string memory tierName = "Bronze"; + uint256 minKarma = 100; + uint256 maxKarma = 500; + uint32 txPerEpoch = 5; + + vm.prank(deployer); + karmaTiers.addTier(tierName, minKarma, maxKarma, txPerEpoch); + + KarmaTiers.Tier memory tier = karmaTiers.getTierById(1); + assertEq(tier.name, tierName); + assertEq(tier.minKarma, minKarma); + assertEq(tier.maxKarma, maxKarma); + assertTrue(tier.active); + } + + function test_GetTierById_RevertWhen_InvalidTierId() public { + vm.expectRevert(KarmaTiers.TierNotFound.selector); + karmaTiers.getTierById(1); + } + + function test_GetTierById_RevertWhen_TierIdZero() public { + vm.expectRevert(KarmaTiers.TierNotFound.selector); + karmaTiers.getTierById(0); + } +} + +contract EdgeCasesTest is KarmaTiersTest { + function setUp() public override { + super.setUp(); + } + + function test_OverlapValidation_EdgeCases() public { + // Test adjacent ranges (should not overlap) + vm.startPrank(deployer); + karmaTiers.addTier("Tier1", 0, 100, 5); + karmaTiers.addTier("Tier2", 101, 200, 5); + vm.stopPrank(); + + // Test touching ranges (100 and 101 are adjacent, should not overlap) + assertEq(karmaTiers.getTierCount(), 2); + + // Test exact boundary overlap (should fail) + vm.expectRevert(abi.encodeWithSelector(KarmaTiers.OverlappingTiers.selector, 1, 100, 150)); + vm.prank(deployer); + karmaTiers.addTier("Tier3", 100, 150, 5); + } + + function test_UnlimitedTierOverlap() public { + // Add unlimited tier + vm.prank(deployer); + karmaTiers.addTier("Unlimited", 1000, 0, 5); + + // Try to add tier that overlaps with unlimited tier + vm.expectRevert(abi.encodeWithSelector(KarmaTiers.OverlappingTiers.selector, 1, 1500, 2000)); + vm.prank(deployer); + karmaTiers.addTier("Overlap", 1500, 2000, 5); + + // Try to add tier that starts before unlimited tier + vm.expectRevert(abi.encodeWithSelector(KarmaTiers.OverlappingTiers.selector, 1, 500, 1500)); + vm.prank(deployer); + karmaTiers.addTier("Before", 500, 1500, 5); + } +} + +contract GetTierIdByKarmaBalanceTest is KarmaTiersTest { + function setUp() public override { + super.setUp(); + vm.startPrank(deployer); + karmaTiers.addTier("Bronze", 100, 500, 5); + karmaTiers.addTier("Silver", 501, 1000, 5); + karmaTiers.addTier("Gold", 1001, 1500, 5); + karmaTiers.addTier("Platinum", 5001, 10_000, 5); // creating a gap + // let's also take into account that tiers aren't sorted + karmaTiers.addTier("Wood", 10, 99, 5); + + karmaTiers.deactivateTier(3); // Deactivate Gold tier for testing + vm.stopPrank(); + } + + function test_GetTierIdByKarmaBalance_BelowMinKarma() public { + uint256 karmaBalance = 5; + uint8 tierId = karmaTiers.getTierIdByKarmaBalance(karmaBalance); + assertEq(tierId, 0); // Should return 0 for no tier + } + + function test_GetTierIdByKarmaBalance_InBronzeTier() public { + uint256 karmaBalance = 300; + uint8 tierId = karmaTiers.getTierIdByKarmaBalance(karmaBalance); + assertEq(tierId, 1); + } + + function test_GetTierIdByKarmaBalance_InSilverTier() public { + uint256 karmaBalance = 800; + uint8 tierId = karmaTiers.getTierIdByKarmaBalance(karmaBalance); + assertEq(tierId, 2); + } + + function test_GetTierIdByKarmaBalance_InGoldTier() public { + uint256 karmaBalance = 1200; + uint8 tierId = karmaTiers.getTierIdByKarmaBalance(karmaBalance); + assertEq(tierId, 2); // Since Gold is deactivated, should return 2 for Silver + } +} + +contract FuzzTests is KarmaTiersTest { + function setUp() public override { + super.setUp(); + } + + function testFuzz_AddTier_ValidInputs( + string calldata name, + uint256 minKarma, + uint256 maxKarma, + uint32 txPerEpoch + ) + public + { + vm.assume(bytes(name).length > 0 && bytes(name).length <= 32); + vm.assume(maxKarma == 0 || maxKarma > minKarma); + vm.assume(minKarma < type(uint256).max); + vm.assume(txPerEpoch > 0); + + vm.prank(deployer); + karmaTiers.addTier(name, minKarma, maxKarma, txPerEpoch); + + KarmaTiers.Tier memory tier = karmaTiers.getTierById(1); + assertEq(tier.name, name); + assertEq(tier.minKarma, minKarma); + assertEq(tier.maxKarma, maxKarma); + assertTrue(tier.active); + } + + function testFuzz_UpdateTier_ValidInputs( + string calldata initialName, + uint256 initialMinKarma, + uint256 initialMaxKarma, + string calldata newName, + uint256 newMinKarma, + uint256 newMaxKarma, + uint32 initialTxPerEpoch, + uint32 newTxPerEpoch + ) + public + { + // Setup constraints for initial tier + vm.assume(bytes(initialName).length > 0 && bytes(initialName).length <= 32); + vm.assume(initialMaxKarma == 0 || initialMaxKarma > initialMinKarma); + + // Setup constraints for new tier + vm.assume(bytes(newName).length > 0 && bytes(newName).length <= 32); + vm.assume(newMaxKarma == 0 || newMaxKarma > newMinKarma); + vm.assume(initialTxPerEpoch > 0); + vm.assume(newTxPerEpoch > 0); + + vm.startPrank(deployer); + karmaTiers.addTier(initialName, initialMinKarma, initialMaxKarma, initialTxPerEpoch); + karmaTiers.updateTier(1, newName, newMinKarma, newMaxKarma, newTxPerEpoch); + vm.stopPrank(); + + KarmaTiers.Tier memory tier = karmaTiers.getTierById(1); + assertEq(tier.name, newName); + assertEq(tier.minKarma, newMinKarma); + assertEq(tier.maxKarma, newMaxKarma); + } +}