refactor: migrate KarmaTiers to contiguous array-based tier management (#228)

* refactor: migrate KarmaTiers to contiguous array-based tier management

- Replaced mapping-based tier storage with a contiguous Tier[] array
- Introduced batch update via updateTiers(), enforcing non-overlapping, gapless ranges
- Removed individual tier operations (add, update, deactivate, activate)
- Simplified tier resolution with linear lookup

test: rewrite test suite for new KarmaTiers structure

- Added tests for ownership, empty input, non-contiguous tiers, invalid names
- Validated proper behavior for unlimited tiers (maxKarma = type(uint256).max)
- Included edge case checks for getTierIdByKarmaBalance
- Added event verification for TiersUpdated

* ci: update python version to fix bug in certora

---------

Co-authored-by: r4bbit <445106+0x-r4bbit@users.noreply.github.com>
This commit is contained in:
Ricardo Guilherme Schmidt
2025-07-28 10:39:49 -03:00
committed by GitHub
parent 22ba4ff130
commit 7d2e4a810d
5 changed files with 167 additions and 592 deletions

View File

@@ -46,13 +46,13 @@
+=========================================================================================================================+
| Deployment Cost | Deployment Size | | | | |
|---------------------------------------------------------------+-----------------+---------+---------+---------+---------|
| 3542177 | 17392 | | | | |
| 3233361 | 15956 | | | | |
|---------------------------------------------------------------+-----------------+---------+---------+---------+---------|
| | | | | | |
|---------------------------------------------------------------+-----------------+---------+---------+---------+---------|
| Function Name | Min | Avg | Median | Max | # Calls |
|---------------------------------------------------------------+-----------------+---------+---------+---------+---------|
| run | 2968362 | 2968362 | 2968362 | 2968362 | 33 |
| run | 2680741 | 2680741 | 2680741 | 2680741 | 9 |
╰---------------------------------------------------------------+-----------------+---------+---------+---------+---------╯
╭-------------------------------------------------------------------+-----------------+---------+---------+---------+---------╮
@@ -80,7 +80,7 @@
|---------------------------------------------------------+-----------------+------+--------+------+---------|
| Function Name | Min | Avg | Median | Max | # Calls |
|---------------------------------------------------------+-----------------+------+--------+------+---------|
| activeNetworkConfig | 455 | 2084 | 455 | 4455 | 540 |
| activeNetworkConfig | 455 | 2048 | 455 | 4455 | 492 |
╰---------------------------------------------------------+-----------------+------+--------+------+---------╯
╭---------------------------------------------------------------------+-----------------+---------+---------+---------+---------╮
@@ -144,7 +144,7 @@
|------------------------------+-----------------+--------+--------+--------+---------|
| setReward | 4845 | 144102 | 166754 | 166754 | 319 |
|------------------------------+-----------------+--------+--------+--------+---------|
| slash | 4803 | 103655 | 85757 | 123125 | 520 |
| slash | 4803 | 103656 | 85757 | 123125 | 520 |
|------------------------------+-----------------+--------+--------+--------+---------|
| slashedAmountOf | 17682 | 28099 | 28120 | 28120 | 516 |
|------------------------------+-----------------+--------+--------+--------+---------|
@@ -192,29 +192,17 @@
+===============================================================================================+
| Deployment Cost | Deployment Size | | | | |
|----------------------------------------+-----------------+--------+--------+--------+---------|
| 0 | 5856 | | | | |
| 0 | 4420 | | | | |
|----------------------------------------+-----------------+--------+--------+--------+---------|
| | | | | | |
|----------------------------------------+-----------------+--------+--------+--------+---------|
| Function Name | Min | Avg | Median | Max | # Calls |
|----------------------------------------+-----------------+--------+--------+--------+---------|
| MAX_TIER_NAME_LENGTH | 240 | 240 | 240 | 240 | 1 |
| getTierCount | 2337 | 2337 | 2337 | 2337 | 2 |
|----------------------------------------+-----------------+--------+--------+--------+---------|
| activateTier | 23752 | 27234 | 25870 | 32081 | 3 |
| getTierIdByKarmaBalance | 7460 | 7942 | 7784 | 10020 | 8 |
|----------------------------------------+-----------------+--------+--------+--------+---------|
| 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 |
| updateTiers | 23978 | 107929 | 28296 | 301092 | 9 |
╰----------------------------------------+-----------------+--------+--------+--------+---------╯
╭--------------------------------------------+-----------------+--------+--------+--------+---------╮
@@ -252,7 +240,7 @@
|--------------------------------------------+-----------------+--------+--------+--------+---------|
| leave | 66348 | 66348 | 66348 | 66348 | 2 |
|--------------------------------------------+-----------------+--------+--------+--------+---------|
| lock | 7040 | 43275 | 46713 | 87673 | 1034 |
| lock | 7040 | 43229 | 46713 | 87673 | 1034 |
|--------------------------------------------+-----------------+--------+--------+--------+---------|
| migrateToVault | 9294 | 53513 | 17021 | 170715 | 4 |
|--------------------------------------------+-----------------+--------+--------+--------+---------|
@@ -280,7 +268,7 @@
|--------------------------------------------+-----------------+--------+--------+--------+---------|
| setTrustedCodehash | 24238 | 24238 | 24238 | 24238 | 95 |
|--------------------------------------------+-----------------+--------+--------+--------+---------|
| stake | 2639 | 130108 | 60725 | 228623 | 2670 |
| stake | 2639 | 129983 | 60725 | 228623 | 2670 |
|--------------------------------------------+-----------------+--------+--------+--------+---------|
| stakedBalanceOf | 2622 | 2622 | 2622 | 2622 | 1 |
|--------------------------------------------+-----------------+--------+--------+--------+---------|
@@ -306,7 +294,7 @@
|--------------------------------------------+-----------------+--------+--------+--------+---------|
| updateGlobalState | 15820 | 25876 | 29230 | 29230 | 8 |
|--------------------------------------------+-----------------+--------+--------+--------+---------|
| updateVault | 31948 | 34036 | 31948 | 110579 | 1023 |
| updateVault | 31948 | 34013 | 31948 | 110579 | 1023 |
|--------------------------------------------+-----------------+--------+--------+--------+---------|
| upgradeTo | 10279 | 10772 | 10279 | 12745 | 5 |
|--------------------------------------------+-----------------+--------+--------+--------+---------|
@@ -334,7 +322,7 @@
|----------------------------------------+-----------------+--------+--------+--------+---------|
| leave | 12223 | 113137 | 84120 | 356508 | 5 |
|----------------------------------------+-----------------+--------+--------+--------+---------|
| lock | 12151 | 58767 | 62251 | 103208 | 1035 |
| lock | 12151 | 58722 | 62251 | 103208 | 1035 |
|----------------------------------------+-----------------+--------+--------+--------+---------|
| lockUntil | 2363 | 2363 | 2363 | 2363 | 7768 |
|----------------------------------------+-----------------+--------+--------+--------+---------|
@@ -344,7 +332,7 @@
|----------------------------------------+-----------------+--------+--------+--------+---------|
| register | 12742 | 78218 | 78761 | 78761 | 374 |
|----------------------------------------+-----------------+--------+--------+--------+---------|
| stake | 12131 | 164075 | 76290 | 284275 | 2671 |
| stake | 12131 | 163920 | 76290 | 284275 | 2671 |
|----------------------------------------+-----------------+--------+--------+--------+---------|
| stakeManager | 393 | 393 | 393 | 393 | 373 |
|----------------------------------------+-----------------+--------+--------+--------+---------|
@@ -352,7 +340,7 @@
|----------------------------------------+-----------------+--------+--------+--------+---------|
| unstake | 12108 | 58192 | 55296 | 110656 | 272 |
|----------------------------------------+-----------------+--------+--------+--------+---------|
| updateLockUntil | 4432 | 20802 | 21532 | 21532 | 488 |
| updateLockUntil | 4432 | 20798 | 21532 | 21532 | 485 |
|----------------------------------------+-----------------+--------+--------+--------+---------|
| withdraw | 20817 | 20817 | 20817 | 20817 | 1 |
|----------------------------------------+-----------------+--------+--------+--------+---------|
@@ -370,9 +358,9 @@
|----------------------------------------------------+-----------------+-------+--------+--------+---------|
| Function Name | Min | Avg | Median | Max | # Calls |
|----------------------------------------------------+-----------------+-------+--------+--------+---------|
| fallback | 5208 | 12818 | 7353 | 374054 | 23161 |
| fallback | 5208 | 12817 | 7353 | 374054 | 23161 |
|----------------------------------------------------+-----------------+-------+--------+--------+---------|
| implementation | 346 | 2144 | 2346 | 2346 | 4850 |
| implementation | 346 | 2145 | 2346 | 2346 | 4847 |
╰----------------------------------------------------+-----------------+-------+--------+--------+---------╯
╭--------------------------------------------+-----------------+--------+--------+--------+---------╮
@@ -396,7 +384,7 @@
+===============================================================================================================================================+
| Deployment Cost | Deployment Size | | | | |
|------------------------------------------------------------------------------------------+-----------------+-------+--------+-------+---------|
| 1204853 | 6207 | | | | |
| 1204853 | 6015 | | | | |
|------------------------------------------------------------------------------------------+-----------------+-------+--------+-------+---------|
| | | | | | |
|------------------------------------------------------------------------------------------+-----------------+-------+--------+-------+---------|

View File

@@ -1,6 +1,3 @@
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)
@@ -11,20 +8,6 @@ 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)
@@ -33,21 +16,15 @@ 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: 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_AccrueMP(uint128,uint64,uint64) (runs: 1006, μ: 581521, ~: 549037)
FuzzTests:testFuzz_AccrueMP_Relock(uint128,uint64,uint64,uint64) (runs: 1006, μ: 806405, ~: 777234)
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_Lock(uint256,uint64) (runs: 1006, μ: 961274, ~: 961235)
FuzzTests:testFuzz_Relock(uint256,uint64,uint64) (runs: 1006, μ: 598321, ~: 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)
FuzzTests:testFuzz_Stake(uint256,uint64) (runs: 1006, μ: 376307, ~: 346086)
FuzzTests:testFuzz_Unstake(uint128,uint64,uint16,uint128) (runs: 1006, μ: 801126, ~: 780593)
FuzzTests:testFuzz_UpdateVault(uint128,uint64,uint64) (runs: 1006, μ: 581544, ~: 549060)
IntegrationTest:testStakeFoo() (gas: 2348931)
KarmaNFTTest:testApproveNotAllowed() (gas: 10507)
KarmaNFTTest:testGetApproved() (gas: 10531)
@@ -77,11 +54,20 @@ KarmaTest:testRemoveKarmaDistributorOnlyOwner() (gas: 163437)
KarmaTest:testRemoveUnknownKarmaDistributor() (gas: 41654)
KarmaTest:testTotalSupply() (gas: 359391)
KarmaTest:testTransfersNotAllowed() (gas: 61925)
KarmaTiersTest:test_GetTierIdByKarmaBalance_EdgeCases() (gas: 297868)
KarmaTiersTest:test_Revert_When_TierNameEmpty() (gas: 39361)
KarmaTiersTest:test_Revert_When_TierNameTooLong() (gas: 40510)
KarmaTiersTest:test_Revert_When_TiersAreEmpty() (gas: 34673)
KarmaTiersTest:test_Revert_When_TiersNotContiguous() (gas: 132711)
KarmaTiersTest:test_Revert_When_TiersNotStartingAtZero() (gas: 37667)
KarmaTiersTest:test_Revert_When_UpdateTiersCalledByNonOwner() (gas: 36642)
KarmaTiersTest:test_Success_When_LastTierIsUnlimited() (gas: 242295)
KarmaTiersTest:test_Success_When_TiersAreContiguous() (gas: 336294)
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: 1002, μ: 384561, ~: 384588)
LockTest:test_LockFailsWithInvalidPeriod(uint256) (runs: 1006, μ: 384561, ~: 384588)
LockTest:test_LockFailsWithNoStake() (gas: 89700)
LockTest:test_LockFailsWithZero() (gas: 343310)
LockTest:test_LockMultipleTimesExceedMaxLock() (gas: 746921)
@@ -145,7 +131,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: 1000, μ: 408297, ~: 409081)
SlashAmountOfTest:testFuzz_SlashAmountOf(uint256,uint256,uint256) (runs: 1002, μ: 408298, ~: 409081)
SlashAmountOfTest:testMintOnlyAdmin() (gas: 429075)
SlashAmountOfTest:testRemoveKarmaDistributorOnlyOwner() (gas: 163437)
SlashAmountOfTest:testRemoveUnknownKarmaDistributor() (gas: 41654)
@@ -155,7 +141,7 @@ SlashAmountOfTest:test_SlashAmountOf() (gas: 327608)
SlashTest:testAddKarmaDistributorOnlyAdmin() (gas: 438270)
SlashTest:testBalanceOf() (gas: 456648)
SlashTest:testBalanceOfWithNoSystemTotalKarma() (gas: 83827)
SlashTest:testFuzz_Slash(uint256) (runs: 1002, μ: 280204, ~: 280146)
SlashTest:testFuzz_Slash(uint256) (runs: 1006, μ: 280204, ~: 280146)
SlashTest:testMintOnlyAdmin() (gas: 429131)
SlashTest:testRemoveKarmaDistributorOnlyOwner() (gas: 163461)
SlashTest:testRemoveRewardDistributorShouldReduceSlashAmount() (gas: 610762)
@@ -218,19 +204,9 @@ 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)

View File

@@ -133,9 +133,8 @@ jobs:
submodules: recursive
- name: Install Python
uses: actions/setup-python@v2
with: { python-version: 3.9 }
uses: actions/setup-python@v4
with: { python-version: "3.10" }
- name: Install Java
uses: actions/setup-java@v1
with: { java-version: "11", java-package: jre }

View File

@@ -9,25 +9,21 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
* @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 tier list is updated
event TiersUpdated();
/// @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 array is empty
error EmptyTiersArray();
/// @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 tiers are not contiguous
error NonContiguousTiers(uint8 index, uint256 expectedMinKarma, uint256 actualMinKarma);
/// @notice Emitted when a tier's minKarma is greater than or equal to maxKarma
error InvalidTierRange(uint256 minKarma, uint256 maxKarma);
@@ -36,11 +32,10 @@ contract KarmaTiers is Ownable {
uint256 maxKarma;
string name;
uint32 txPerEpoch;
bool active;
}
modifier onlyValidTierId(uint8 tierId) {
if (tierId == 0 || tierId > currentTierId) {
if (tierId >= tiers.length) {
revert TierNotFound();
}
_;
@@ -57,10 +52,8 @@ contract KarmaTiers is Ownable {
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;
/// @notice Array of tiers
Tier[] public tiers;
/*//////////////////////////////////////////////////////////////////////////
CONSTRUCTOR
@@ -74,77 +67,37 @@ contract KarmaTiers is Ownable {
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();
function updateTiers(Tier[] calldata newTiers) external onlyOwner {
if (newTiers.length == 0) {
revert EmptyTiersArray();
}
// Ensure the first tier starts at minKarma = 0
if (newTiers[0].minKarma != 0) {
revert NonContiguousTiers(0, 0, newTiers[0].minKarma);
}
// Check for overlaps with existing tiers
_validateNoOverlap(minKarma, maxKarma, type(uint8).max);
_validateTierName(name);
delete tiers; // Clear existing tiers
currentTierId++;
uint256 lastMaxKarma = 0;
for (uint8 i = 0; i < newTiers.length; i++) {
Tier calldata input = newTiers[i];
tiers[currentTierId] =
Tier({ minKarma: minKarma, maxKarma: maxKarma, name: name, active: true, txPerEpoch: txPerEpoch });
_validateTierName(input.name);
if (input.maxKarma <= input.minKarma) {
revert InvalidTierRange(input.minKarma, input.maxKarma);
}
emit TierAdded(currentTierId, name, minKarma, maxKarma, txPerEpoch);
}
if (i > 0) {
uint256 expectedMinKarma = lastMaxKarma + 1;
if (input.minKarma != expectedMinKarma) {
revert NonContiguousTiers(i, expectedMinKarma, input.minKarma);
}
}
lastMaxKarma = input.maxKarma;
tiers.push(input);
}
/**
* @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);
emit TiersUpdated();
}
/*//////////////////////////////////////////////////////////////////////////
@@ -174,23 +127,14 @@ contract KarmaTiers is Ownable {
* @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;
for (uint8 i = 0; i < tiers.length; i++) {
// 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;
}
if (karmaBalance < tiers[i].minKarma) {
// Can't run into underflow here because `karmaBalance == 0 => karmaBalance < tiers[0].minKarma`
return i - 1; // Return the previous tier if this one is not met
}
}
return bestTierId;
return uint8(tiers.length - 1); // If all tiers are met, return the highest tier
}
/**
@@ -198,7 +142,7 @@ contract KarmaTiers is Ownable {
* @return count Total number of tiers (including inactive)
*/
function getTierCount() external view returns (uint256 count) {
return currentTierId;
return tiers.length;
}
/**
@@ -209,25 +153,4 @@ contract KarmaTiers is Ownable {
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);
}
}
}
}

View File

@@ -8,429 +8,118 @@ 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);
address public owner;
address public nonOwner = address(0xBEEF);
function setUp() public virtual {
DeployKarmaTiersScript deployment = new DeployKarmaTiersScript();
(KarmaTiers _karmaTiers, DeploymentConfig deploymentConfig) = deployment.run();
(address _deployer,) = deploymentConfig.activeNetworkConfig();
deployer = _deployer;
owner = _deployer;
karmaTiers = _karmaTiers;
}
}
contract AddTierTests is KarmaTiersTest {
function setUp() public override {
super.setUp();
function test_Revert_When_UpdateTiersCalledByNonOwner() public {
KarmaTiers.Tier[] memory newTiers = new KarmaTiers.Tier[](1);
newTiers[0] = KarmaTiers.Tier({ minKarma: 0, maxKarma: 100, name: "Bronze", txPerEpoch: 5 });
vm.prank(nonOwner);
vm.expectRevert("Ownable: caller is not the owner");
karmaTiers.updateTiers(newTiers);
}
function test_AddTier_RevertWhen_EmptyName() public {
vm.prank(deployer);
function test_Revert_When_TiersAreEmpty() public {
KarmaTiers.Tier[] memory newTiers = new KarmaTiers.Tier[](0);
vm.prank(owner);
vm.expectRevert(); // generic revert
karmaTiers.updateTiers(newTiers);
}
function test_Revert_When_TiersNotStartingAtZero() public {
KarmaTiers.Tier[] memory newTiers = new KarmaTiers.Tier[](1);
newTiers[0] = KarmaTiers.Tier({ minKarma: 1, maxKarma: 100, name: "Bronze", txPerEpoch: 5 });
vm.prank(owner);
vm.expectRevert(abi.encodeWithSelector(KarmaTiers.NonContiguousTiers.selector, 0, 0, 1));
karmaTiers.updateTiers(newTiers);
}
function test_Revert_When_TierNameEmpty() public {
KarmaTiers.Tier[] memory newTiers = new KarmaTiers.Tier[](1);
newTiers[0] = KarmaTiers.Tier({ minKarma: 0, maxKarma: 100, name: "", txPerEpoch: 5 });
vm.prank(owner);
vm.expectRevert(KarmaTiers.EmptyTierName.selector);
karmaTiers.addTier("", 100, 500, 5);
karmaTiers.updateTiers(newTiers);
}
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_Revert_When_TierNameTooLong() public {
string memory longName = new string(33);
KarmaTiers.Tier[] memory newTiers = new KarmaTiers.Tier[](1);
newTiers[0] = KarmaTiers.Tier({ minKarma: 0, maxKarma: 100, name: longName, txPerEpoch: 5 });
vm.prank(owner);
vm.expectRevert(abi.encodeWithSelector(KarmaTiers.TierNameTooLong.selector, 33, 32));
karmaTiers.updateTiers(newTiers);
}
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_Revert_When_TiersNotContiguous() public {
KarmaTiers.Tier[] memory newTiers = new KarmaTiers.Tier[](2);
newTiers[0] = KarmaTiers.Tier({ minKarma: 0, maxKarma: 100, name: "Bronze", txPerEpoch: 5 });
newTiers[1] = KarmaTiers.Tier({ minKarma: 102, maxKarma: 200, name: "Silver", txPerEpoch: 5 });
vm.prank(owner);
vm.expectRevert(abi.encodeWithSelector(KarmaTiers.NonContiguousTiers.selector, 1, 101, 102));
karmaTiers.updateTiers(newTiers);
}
function test_AddTier_RevertWhen_OverlappingTiers() public {
vm.prank(deployer);
karmaTiers.addTier("Bronze", 100, 500, 5);
function test_Success_When_TiersAreContiguous() public {
KarmaTiers.Tier[] memory newTiers = new KarmaTiers.Tier[](3);
newTiers[0] = KarmaTiers.Tier({ minKarma: 0, maxKarma: 100, name: "Bronze", txPerEpoch: 5 });
newTiers[1] = KarmaTiers.Tier({ minKarma: 101, maxKarma: 200, name: "Silver", txPerEpoch: 10 });
newTiers[2] = KarmaTiers.Tier({ minKarma: 201, maxKarma: 999, name: "Gold", txPerEpoch: 15 });
vm.prank(deployer);
vm.expectRevert(abi.encodeWithSelector(KarmaTiers.OverlappingTiers.selector, 1, 400, 600));
karmaTiers.addTier("Silver", 400, 600, 5);
}
vm.expectEmit(false, false, false, true);
emit KarmaTiers.TiersUpdated();
function test_AddTier_RevertWhen_NotOwner() public {
vm.prank(nonOwner);
vm.expectRevert("Ownable: caller is not the owner");
karmaTiers.addTier("Bronze", 100, 500, 5);
}
vm.prank(owner);
karmaTiers.updateTiers(newTiers);
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);
(uint8 tierId) = karmaTiers.getTierIdByKarmaBalance(150);
assertEq(tierId, 1);
}
function test_GetTierIdByKarmaBalance_InSilverTier() public {
uint256 karmaBalance = 800;
uint8 tierId = karmaTiers.getTierIdByKarmaBalance(karmaBalance);
assertEq(tierId, 2);
function test_Success_When_LastTierIsUnlimited() public {
KarmaTiers.Tier[] memory newTiers = new KarmaTiers.Tier[](2);
newTiers[0] = KarmaTiers.Tier({ minKarma: 0, maxKarma: 100, name: "Bronze", txPerEpoch: 5 });
newTiers[1] =
KarmaTiers.Tier({ minKarma: 101, maxKarma: type(uint256).max, name: "Unlimited", txPerEpoch: 100 });
vm.expectEmit(false, false, false, true);
emit KarmaTiers.TiersUpdated();
vm.prank(owner);
karmaTiers.updateTiers(newTiers);
assertEq(karmaTiers.getTierCount(), 2);
assertEq(karmaTiers.getTierIdByKarmaBalance(500_000), 1);
}
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);
function test_GetTierIdByKarmaBalance_EdgeCases() public {
KarmaTiers.Tier[] memory newTiers = new KarmaTiers.Tier[](2);
newTiers[0] = KarmaTiers.Tier({ minKarma: 0, maxKarma: 100, name: "Bronze", txPerEpoch: 5 });
newTiers[1] = KarmaTiers.Tier({ minKarma: 101, maxKarma: 200, name: "Silver", txPerEpoch: 5 });
vm.prank(owner);
karmaTiers.updateTiers(newTiers);
assertEq(karmaTiers.getTierIdByKarmaBalance(0), 0);
assertEq(karmaTiers.getTierIdByKarmaBalance(99), 0);
assertEq(karmaTiers.getTierIdByKarmaBalance(100), 0);
assertEq(karmaTiers.getTierIdByKarmaBalance(101), 1);
assertEq(karmaTiers.getTierIdByKarmaBalance(200), 1);
assertEq(karmaTiers.getTierIdByKarmaBalance(250), 1);
}
}