From ed3577f8c4bc52bf85c508d692e2088f50e2a9e7 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Wed, 26 Feb 2025 11:43:58 +0100 Subject: [PATCH] feat(Karma): allocate external rewards using the Karma contract --- .gas-report | 204 ++++++++++-------- .gas-snapshot | 128 +++++------ certora/specs/EmergencyMode.spec | 1 + docs/xp-token.md | 1 - script/DeployKarma.s.sol | 20 ++ script/DeploymentConfig.s.sol | 1 + src/Karma.sol | 128 +++++++++-- src/RewardsStreamerMP.sol | 29 ++- ...ardProvider.sol => IRewardDistributor.sol} | 9 +- src/interfaces/IStakeManager.sol | 1 + test/Karma.t.sol | 172 +++++++++------ test/RewardsStreamerMP.t.sol | 43 ++-- ...viderMock.sol => KarmaDistributorMock.sol} | 6 +- 13 files changed, 468 insertions(+), 275 deletions(-) create mode 100644 script/DeployKarma.s.sol rename src/interfaces/{IRewardProvider.sol => IRewardDistributor.sol} (76%) rename test/mocks/{KarmaProviderMock.sol => KarmaDistributorMock.sol} (79%) diff --git a/.gas-report b/.gas-report index ac8e228..1b8a222 100644 --- a/.gas-report +++ b/.gas-report @@ -1,16 +1,30 @@ +╭-----------------------------------------------------+-----------------+---------+---------+---------+---------╮ +| script/DeployKarma.s.sol:DeployKarmaScript Contract | | | | | | ++===============================================================================================================+ +| Deployment Cost | Deployment Size | | | | | +|-----------------------------------------------------+-----------------+---------+---------+---------+---------| +| 3089188 | 15186 | | | | | +|-----------------------------------------------------+-----------------+---------+---------+---------+---------| +| | | | | | | +|-----------------------------------------------------+-----------------+---------+---------+---------+---------| +| Function Name | Min | Avg | Median | Max | # Calls | +|-----------------------------------------------------+-----------------+---------+---------+---------+---------| +| run | 2533527 | 2533527 | 2533527 | 2533527 | 75 | +╰-----------------------------------------------------+-----------------+---------+---------+---------+---------╯ + ╭-----------------------------------------------------------------------------+-----------------+---------+---------+---------+---------╮ | script/DeployRewardsStreamerMP.s.sol:DeployRewardsStreamerMPScript Contract | | | | | | +=======================================================================================================================================+ | Deployment Cost | Deployment Size | | | | | |-----------------------------------------------------------------------------+-----------------+---------+---------+---------+---------| -| 7645817 | 36435 | | | | | +| 7717029 | 36770 | | | | | |-----------------------------------------------------------------------------+-----------------+---------+---------+---------+---------| | | | | | | | |-----------------------------------------------------------------------------+-----------------+---------+---------+---------+---------| | Function Name | Min | Avg | Median | Max | # Calls | |-----------------------------------------------------------------------------+-----------------+---------+---------+---------+---------| -| run | 6708235 | 6708235 | 6708235 | 6708235 | 77 | +| run | 6775418 | 6775418 | 6775418 | 6775418 | 77 | ╰-----------------------------------------------------------------------------+-----------------+---------+---------+---------+---------╯ ╭---------------------------------------------------------+-----------------+-----+--------+-----+---------╮ @@ -24,7 +38,7 @@ |---------------------------------------------------------+-----------------+-----+--------+-----+---------| | Function Name | Min | Avg | Median | Max | # Calls | |---------------------------------------------------------+-----------------+-----+--------+-----+---------| -| activeNetworkConfig | 454 | 454 | 454 | 454 | 154 | +| activeNetworkConfig | 454 | 454 | 454 | 454 | 229 | ╰---------------------------------------------------------+-----------------+-----+--------+-----+---------╯ ╭-------------------------------------------------------------------------------+-----------------+---------+---------+---------+---------╮ @@ -32,54 +46,56 @@ +=========================================================================================================================================+ | Deployment Cost | Deployment Size | | | | | |-------------------------------------------------------------------------------+-----------------+---------+---------+---------+---------| -| 3570391 | 17446 | | | | | +| 3641587 | 17781 | | | | | |-------------------------------------------------------------------------------+-----------------+---------+---------+---------+---------| | | | | | | | |-------------------------------------------------------------------------------+-----------------+---------+---------+---------+---------| | Function Name | Min | Avg | Median | Max | # Calls | |-------------------------------------------------------------------------------+-----------------+---------+---------+---------+---------| -| run | 3058619 | 3058619 | 3058619 | 3058619 | 3 | +| run | 3125807 | 3125807 | 3125807 | 3125807 | 3 | ╰-------------------------------------------------------------------------------+-----------------+---------+---------+---------+---------╯ -╭------------------------------+-----------------+-------+--------+-------+---------╮ -| src/Karma.sol:Karma Contract | | | | | | -+===================================================================================+ -| Deployment Cost | Deployment Size | | | | | -|------------------------------+-----------------+-------+--------+-------+---------| -| 1040918 | 4850 | | | | | -|------------------------------+-----------------+-------+--------+-------+---------| -| | | | | | | -|------------------------------+-----------------+-------+--------+-------+---------| -| Function Name | Min | Avg | Median | Max | # Calls | -|------------------------------+-----------------+-------+--------+-------+---------| -| acceptOwnership | 28260 | 28260 | 28260 | 28260 | 1 | -|------------------------------+-----------------+-------+--------+-------+---------| -| addRewardProvider | 23967 | 57628 | 51089 | 68189 | 44 | -|------------------------------+-----------------+-------+--------+-------+---------| -| allowance | 530 | 530 | 530 | 530 | 2 | -|------------------------------+-----------------+-------+--------+-------+---------| -| approve | 410 | 410 | 410 | 410 | 2 | -|------------------------------+-----------------+-------+--------+-------+---------| -| balanceOf | 3691 | 11357 | 9691 | 20691 | 6 | -|------------------------------+-----------------+-------+--------+-------+---------| -| getRewardProviders | 1051 | 3304 | 3304 | 5557 | 4 | -|------------------------------+-----------------+-------+--------+-------+---------| -| mint | 24199 | 75906 | 91068 | 91080 | 14 | -|------------------------------+-----------------+-------+--------+-------+---------| -| mintAllowance | 5714 | 5751 | 5751 | 5788 | 2 | -|------------------------------+-----------------+-------+--------+-------+---------| -| owner | 340 | 1006 | 340 | 2340 | 3 | -|------------------------------+-----------------+-------+--------+-------+---------| -| removeRewardProvider | 23685 | 28092 | 25800 | 34792 | 6 | -|------------------------------+-----------------+-------+--------+-------+---------| -| totalSupply | 3018 | 5018 | 3018 | 11018 | 8 | -|------------------------------+-----------------+-------+--------+-------+---------| -| transfer | 408 | 408 | 408 | 408 | 2 | -|------------------------------+-----------------+-------+--------+-------+---------| -| transferFrom | 517 | 517 | 517 | 517 | 2 | -|------------------------------+-----------------+-------+--------+-------+---------| -| transferOwnership | 47730 | 47730 | 47730 | 47730 | 1 | -╰------------------------------+-----------------+-------+--------+-------+---------╯ +╭------------------------------+-----------------+--------+--------+--------+---------╮ +| src/Karma.sol:Karma Contract | | | | | | ++=====================================================================================+ +| Deployment Cost | Deployment Size | | | | | +|------------------------------+-----------------+--------+--------+--------+---------| +| 1171148 | 5453 | | | | | +|------------------------------+-----------------+--------+--------+--------+---------| +| | | | | | | +|------------------------------+-----------------+--------+--------+--------+---------| +| Function Name | Min | Avg | Median | Max | # Calls | +|------------------------------+-----------------+--------+--------+--------+---------| +| acceptOwnership | 28282 | 28282 | 28282 | 28282 | 1 | +|------------------------------+-----------------+--------+--------+--------+---------| +| addRewardDistributor | 23973 | 87707 | 92012 | 92012 | 119 | +|------------------------------+-----------------+--------+--------+--------+---------| +| allowance | 492 | 492 | 492 | 492 | 2 | +|------------------------------+-----------------+--------+--------+--------+---------| +| approve | 416 | 416 | 416 | 416 | 2 | +|------------------------------+-----------------+--------+--------+--------+---------| +| balanceOf | 3994 | 11660 | 9994 | 20994 | 6 | +|------------------------------+-----------------+--------+--------+--------+---------| +| getRewardDistributors | 1112 | 3356 | 3356 | 5600 | 4 | +|------------------------------+-----------------+--------+--------+--------+---------| +| mint | 24250 | 80685 | 96635 | 96647 | 14 | +|------------------------------+-----------------+--------+--------+--------+---------| +| mintAllowance | 7208 | 7245 | 7245 | 7282 | 2 | +|------------------------------+-----------------+--------+--------+--------+---------| +| owner | 362 | 1028 | 362 | 2362 | 3 | +|------------------------------+-----------------+--------+--------+--------+---------| +| removeRewardDistributor | 24016 | 30281 | 26025 | 40803 | 6 | +|------------------------------+-----------------+--------+--------+--------+---------| +| setReward | 43527 | 175898 | 185831 | 185843 | 283 | +|------------------------------+-----------------+--------+--------+--------+---------| +| totalSupply | 3799 | 5799 | 3799 | 11799 | 8 | +|------------------------------+-----------------+--------+--------+--------+---------| +| transfer | 414 | 414 | 414 | 414 | 2 | +|------------------------------+-----------------+--------+--------+--------+---------| +| transferFrom | 507 | 507 | 507 | 507 | 2 | +|------------------------------+-----------------+--------+--------+--------+---------| +| transferOwnership | 47758 | 47758 | 47758 | 47758 | 1 | +╰------------------------------+-----------------+--------+--------+--------+---------╯ ╭-------------------------------------------------+-----------------+-------+--------+-------+---------╮ | src/KarmaNFT.sol:KarmaNFT Contract | | | | | | @@ -118,7 +134,7 @@ +=============================================================================================================+ | Deployment Cost | Deployment Size | | | | | |------------------------------------------------------+-----------------+--------+--------+--------+---------| -| 3250446 | 15007 | | | | | +| 3321635 | 15342 | | | | | |------------------------------------------------------+-----------------+--------+--------+--------+---------| | | | | | | | |------------------------------------------------------+-----------------+--------+--------+--------+---------| @@ -152,7 +168,7 @@ |------------------------------------------------------+-----------------+--------+--------+--------+---------| | leave | 95475 | 95475 | 95475 | 95475 | 1 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| -| lock | 14216 | 52149 | 52082 | 93689 | 260 | +| lock | 14216 | 52151 | 52082 | 93689 | 260 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| | migrateToVault | 13563 | 72244 | 15769 | 187401 | 3 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| @@ -164,19 +180,21 @@ |------------------------------------------------------+-----------------+--------+--------+--------+---------| | proxiableUUID | 342 | 342 | 342 | 342 | 3 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| -| registerVault | 2562 | 74470 | 74983 | 74983 | 308 | +| registerVault | 2540 | 74448 | 74961 | 74961 | 308 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| | rewardEndTime | 362 | 1362 | 1362 | 2362 | 2 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| | rewardStartTime | 429 | 1429 | 1429 | 2429 | 2 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| -| rewardsBalanceOf | 2324 | 3514 | 3909 | 6324 | 268 | +| rewardsBalanceOf | 2324 | 3513 | 3909 | 6324 | 268 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| -| setReward | 2561 | 105435 | 106966 | 106966 | 264 | +| setReward | 2484 | 105517 | 107034 | 107034 | 264 | +|------------------------------------------------------+-----------------+--------+--------+--------+---------| +| setRewardsSupplier | 26862 | 26862 | 26862 | 26862 | 75 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| | setTrustedCodehash | 24221 | 24221 | 24221 | 24221 | 77 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| -| stake | 2681 | 237356 | 230009 | 251100 | 1607 | +| stake | 2681 | 237357 | 230009 | 251100 | 1607 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| | totalMP | 777 | 1223 | 1223 | 1669 | 6 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| @@ -194,11 +212,11 @@ |------------------------------------------------------+-----------------+--------+--------+--------+---------| | totalStaked | 385 | 385 | 385 | 385 | 1115 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| -| unstake | 56017 | 81137 | 80911 | 88301 | 269 | +| unstake | 56017 | 81037 | 80911 | 88301 | 269 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| -| updateGlobalState | 15798 | 27698 | 29189 | 29492 | 276 | +| updateGlobalState | 15809 | 27668 | 29200 | 29503 | 276 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| -| updateVaultMP | 25103 | 34596 | 36516 | 36819 | 276 | +| updateVaultMP | 25103 | 34553 | 36516 | 36819 | 276 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| | upgradeToAndCall | 3225 | 7892 | 8437 | 10925 | 5 | |------------------------------------------------------+-----------------+--------+--------+--------+---------| @@ -224,13 +242,13 @@ |----------------------------------------+-----------------+--------+--------+--------+---------| | leave | 12167 | 126461 | 69872 | 353935 | 4 | |----------------------------------------+-----------------+--------+--------+--------+---------| -| lock | 12097 | 67385 | 67531 | 109137 | 261 | +| lock | 12097 | 67387 | 67531 | 109137 | 261 | |----------------------------------------+-----------------+--------+--------+--------+---------| | migrateToVault | 29034 | 98943 | 31240 | 236555 | 3 | |----------------------------------------+-----------------+--------+--------+--------+---------| | owner | 377 | 396 | 377 | 2377 | 313 | |----------------------------------------+-----------------+--------+--------+--------+---------| -| register | 12654 | 78080 | 78572 | 78572 | 308 | +| register | 12632 | 78058 | 78550 | 78550 | 308 | |----------------------------------------+-----------------+--------+--------+--------+---------| | stake | 12077 | 291048 | 283898 | 304989 | 1608 | |----------------------------------------+-----------------+--------+--------+--------+---------| @@ -238,26 +256,26 @@ |----------------------------------------+-----------------+--------+--------+--------+---------| | trustStakeManager | 7577 | 7577 | 7577 | 7577 | 1 | |----------------------------------------+-----------------+--------+--------+--------+---------| -| unstake | 12054 | 111689 | 112021 | 119411 | 270 | +| unstake | 12054 | 111569 | 112021 | 119411 | 270 | |----------------------------------------+-----------------+--------+--------+--------+---------| | withdraw | 20705 | 20705 | 20705 | 20705 | 1 | ╰----------------------------------------+-----------------+--------+--------+--------+---------╯ -╭----------------------------------------------------+-----------------+-------+--------+--------+---------╮ -| src/TransparentProxy.sol:TransparentProxy Contract | | | | | | -+==========================================================================================================+ -| Deployment Cost | Deployment Size | | | | | -|----------------------------------------------------+-----------------+-------+--------+--------+---------| -| 0 | 1231 | | | | | -|----------------------------------------------------+-----------------+-------+--------+--------+---------| -| | | | | | | -|----------------------------------------------------+-----------------+-------+--------+--------+---------| -| Function Name | Min | Avg | Median | Max | # Calls | -|----------------------------------------------------+-----------------+-------+--------+--------+---------| -| fallback | 735 | 11980 | 833 | 142290 | 7546 | -|----------------------------------------------------+-----------------+-------+--------+--------+---------| -| implementation | 343 | 2342 | 2343 | 2343 | 2455 | -╰----------------------------------------------------+-----------------+-------+--------+--------+---------╯ +╭----------------------------------------------------+-----------------+------+--------+--------+---------╮ +| src/TransparentProxy.sol:TransparentProxy Contract | | | | | | ++=========================================================================================================+ +| Deployment Cost | Deployment Size | | | | | +|----------------------------------------------------+-----------------+------+--------+--------+---------| +| 0 | 1231 | | | | | +|----------------------------------------------------+-----------------+------+--------+--------+---------| +| | | | | | | +|----------------------------------------------------+-----------------+------+--------+--------+---------| +| Function Name | Min | Avg | Median | Max | # Calls | +|----------------------------------------------------+-----------------+------+--------+--------+---------| +| fallback | 735 | 8100 | 833 | 142290 | 7358 | +|----------------------------------------------------+-----------------+------+--------+--------+---------| +| implementation | 343 | 2342 | 2343 | 2343 | 2455 | +╰----------------------------------------------------+-----------------+------+--------+--------+---------╯ ╭--------------------------------------------+-----------------+--------+--------+--------+---------╮ | src/VaultFactory.sol:VaultFactory Contract | | | | | | @@ -270,7 +288,7 @@ |--------------------------------------------+-----------------+--------+--------+--------+---------| | Function Name | Min | Avg | Median | Max | # Calls | |--------------------------------------------+-----------------+--------+--------+--------+---------| -| createVault | 231307 | 248128 | 248407 | 248407 | 307 | +| createVault | 231285 | 248106 | 248385 | 248385 | 307 | ╰--------------------------------------------+-----------------+--------+--------+--------+---------╯ ╭------------------------------------------------------------------------------------------+-----------------+-------+--------+-------+---------╮ @@ -313,25 +331,25 @@ | urlSuffix | 1141 | 1141 | 1141 | 1141 | 1 | ╰------------------------------------------------------------------------------------------+-----------------+-------+--------+-------+---------╯ -╭-------------------------------------------------------------+-----------------+-------+--------+-------+---------╮ -| test/mocks/KarmaProviderMock.sol:KarmaProviderMock Contract | | | | | | -+==================================================================================================================+ -| Deployment Cost | Deployment Size | | | | | -|-------------------------------------------------------------+-----------------+-------+--------+-------+---------| -| 177717 | 606 | | | | | -|-------------------------------------------------------------+-----------------+-------+--------+-------+---------| -| | | | | | | -|-------------------------------------------------------------+-----------------+-------+--------+-------+---------| -| Function Name | Min | Avg | Median | Max | # Calls | -|-------------------------------------------------------------+-----------------+-------+--------+-------+---------| -| rewardsBalanceOfAccount | 546 | 1879 | 2546 | 2546 | 12 | -|-------------------------------------------------------------+-----------------+-------+--------+-------+---------| -| setTotalKarmaShares | 43587 | 43587 | 43587 | 43587 | 20 | -|-------------------------------------------------------------+-----------------+-------+--------+-------+---------| -| setUserKarmaShare | 44124 | 44124 | 44124 | 44124 | 4 | -|-------------------------------------------------------------+-----------------+-------+--------+-------+---------| -| totalRewardsSupply | 323 | 989 | 323 | 2323 | 72 | -╰-------------------------------------------------------------+-----------------+-------+--------+-------+---------╯ +╭-------------------------------------------------------------------+-----------------+-------+--------+-------+---------╮ +| test/mocks/KarmaDistributorMock.sol:KarmaDistributorMock Contract | | | | | | ++========================================================================================================================+ +| Deployment Cost | Deployment Size | | | | | +|-------------------------------------------------------------------+-----------------+-------+--------+-------+---------| +| 190903 | 667 | | | | | +|-------------------------------------------------------------------+-----------------+-------+--------+-------+---------| +| | | | | | | +|-------------------------------------------------------------------+-----------------+-------+--------+-------+---------| +| Function Name | Min | Avg | Median | Max | # Calls | +|-------------------------------------------------------------------+-----------------+-------+--------+-------+---------| +| rewardsBalanceOfAccount | 546 | 1879 | 2546 | 2546 | 12 | +|-------------------------------------------------------------------+-----------------+-------+--------+-------+---------| +| setTotalKarmaShares | 43587 | 43587 | 43587 | 43587 | 20 | +|-------------------------------------------------------------------+-----------------+-------+--------+-------+---------| +| setUserKarmaShare | 44191 | 44191 | 44191 | 44191 | 4 | +|-------------------------------------------------------------------+-----------------+-------+--------+-------+---------| +| totalRewardsSupply | 323 | 989 | 323 | 2323 | 72 | +╰-------------------------------------------------------------------+-----------------+-------+--------+-------+---------╯ ╭---------------------------------------------------------------------+-----------------+-------+--------+-------+---------╮ | test/mocks/MockMetadataGenerator.sol:MockMetadataGenerator Contract | | | | | | @@ -358,11 +376,11 @@ |---------------------------------------------+-----------------+-------+--------+-------+---------| | Function Name | Min | Avg | Median | Max | # Calls | |---------------------------------------------+-----------------+-------+--------+-------+---------| -| approve | 29158 | 32601 | 29338 | 46342 | 1586 | +| approve | 29158 | 32602 | 29338 | 46342 | 1586 | |---------------------------------------------+-----------------+-------+--------+-------+---------| | balanceOf | 558 | 1118 | 558 | 2558 | 1900 | |---------------------------------------------+-----------------+-------+--------+-------+---------| -| mint | 34095 | 38636 | 34275 | 68379 | 1598 | +| mint | 34095 | 38637 | 34287 | 68379 | 1598 | ╰---------------------------------------------+-----------------+-------+--------+-------+---------╯ ╭-----------------------------------------------------------------------------+-----------------+--------+--------+--------+---------╮ diff --git a/.gas-snapshot b/.gas-snapshot index f4c796c..bf043fe 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,30 +1,30 @@ -CompoundTest:test_RevertWhenInsufficientMPBalance() (gas: 380247) +CompoundTest:test_RevertWhenInsufficientMPBalance() (gas: 380291) EmergencyExitTest:test_CannotEnableEmergencyModeTwice() (gas: 93568) -EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 353203) +EmergencyExitTest:test_CannotLeaveBeforeEmergencyMode() (gas: 353225) EmergencyExitTest:test_EmergencyExitBasic() (gas: 444494) -EmergencyExitTest:test_EmergencyExitMultipleUsers() (gas: 764135) +EmergencyExitTest:test_EmergencyExitMultipleUsers() (gas: 764157) EmergencyExitTest:test_EmergencyExitToAlternateAddress() (gas: 450444) -EmergencyExitTest:test_EmergencyExitWithLock() (gas: 448231) +EmergencyExitTest:test_EmergencyExitWithLock() (gas: 448187) EmergencyExitTest:test_EmergencyExitWithRewards() (gas: 435625) -EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 39471) -FuzzTests:testFuzz_AccrueMP(uint256,uint256,uint16) (runs: 1006, μ: 521440, ~: 515993) -FuzzTests:testFuzz_EmergencyExit(uint256,uint256) (runs: 1006, μ: 510879, ~: 502017) -FuzzTests:testFuzz_Rewards(uint256,uint256,uint256,uint16,uint16) (runs: 1003, μ: 547584, ~: 548791) -FuzzTests:testFuzz_Stake(uint256,uint256) (runs: 1006, μ: 404288, ~: 395426) -FuzzTests:testFuzz_Unstake(uint256,uint256,uint16,uint256) (runs: 1001, μ: 530235, ~: 529926) -IntegrationTest:testStakeFoo() (gas: 1403337) -KarmaMintAllowanceTest:testAddKarmaProviderOnlyOwner() (gas: 311366) -KarmaMintAllowanceTest:testBalanceOf() (gas: 294561) -KarmaMintAllowanceTest:testBalanceOfWithNoSystemTotalKarma() (gas: 43586) -KarmaMintAllowanceTest:testMintAllowance_Available() (gas: 205175) -KarmaMintAllowanceTest:testMintAllowance_NotAvailable() (gas: 205111) -KarmaMintAllowanceTest:testMintOnlyOwner() (gas: 241951) -KarmaMintAllowanceTest:testMint_Ok() (gas: 264252) -KarmaMintAllowanceTest:testMint_RevertWithAllowanceExceeded() (gas: 246592) -KarmaMintAllowanceTest:testRemoveKarmaProviderIndexOutOfBounds() (gas: 36264) -KarmaMintAllowanceTest:testRemoveKarmaProviderOnlyOwner() (gas: 72164) -KarmaMintAllowanceTest:testTotalSupply() (gas: 202426) -KarmaMintAllowanceTest:testTransfersNotAllowed() (gas: 20698) +EmergencyExitTest:test_OnlyOwnerCanEnableEmergencyMode() (gas: 39462) +FuzzTests:testFuzz_AccrueMP(uint256,uint256,uint16) (runs: 1006, μ: 521106, ~: 515988) +FuzzTests:testFuzz_EmergencyExit(uint256,uint256) (runs: 1006, μ: 510643, ~: 501981) +FuzzTests:testFuzz_Rewards(uint256,uint256,uint256,uint16,uint16) (runs: 1003, μ: 604659, ~: 605935) +FuzzTests:testFuzz_Stake(uint256,uint256) (runs: 1006, μ: 404095, ~: 395433) +FuzzTests:testFuzz_Unstake(uint256,uint256,uint16,uint256) (runs: 1001, μ: 530264, ~: 529949) +IntegrationTest:testStakeFoo() (gas: 1403381) +KarmaMintAllowanceTest:testAddKarmaDistributorOnlyOwner() (gas: 348463) +KarmaMintAllowanceTest:testBalanceOf() (gas: 428643) +KarmaMintAllowanceTest:testBalanceOfWithNoSystemTotalKarma() (gas: 44170) +KarmaMintAllowanceTest:testMintAllowance_Available() (gas: 340314) +KarmaMintAllowanceTest:testMintAllowance_NotAvailable() (gas: 340317) +KarmaMintAllowanceTest:testMintOnlyOwner() (gas: 377209) +KarmaMintAllowanceTest:testMint_Ok() (gas: 405049) +KarmaMintAllowanceTest:testMint_RevertWithAllowanceExceeded() (gas: 385796) +KarmaMintAllowanceTest:testRemoveKarmaDistributorOnlyOwner() (gas: 76009) +KarmaMintAllowanceTest:testRemoveUnknownKarmaDistributor() (gas: 36519) +KarmaMintAllowanceTest:testTotalSupply() (gas: 336830) +KarmaMintAllowanceTest:testTransfersNotAllowed() (gas: 20640) KarmaNFTTest:testApproveNotAllowed() (gas: 10500) KarmaNFTTest:testGetApproved() (gas: 10523) KarmaNFTTest:testIsApprovedForAll() (gas: 10698) @@ -35,31 +35,31 @@ KarmaNFTTest:testSetMetadataGenerator() (gas: 966687) KarmaNFTTest:testSetMetadataGeneratorRevert() (gas: 963218) KarmaNFTTest:testTokenURI() (gas: 102963) KarmaNFTTest:testTransferNotAllowed() (gas: 10715) -KarmaOwnershipTest:testInitialOwner() (gas: 12612) -KarmaOwnershipTest:testOwnershipTransfer() (gas: 87164) -KarmaTest:testAddKarmaProviderOnlyOwner() (gas: 311376) -KarmaTest:testBalanceOf() (gas: 294499) -KarmaTest:testBalanceOfWithNoSystemTotalKarma() (gas: 43564) -KarmaTest:testMintOnlyOwner() (gas: 241882) -KarmaTest:testRemoveKarmaProviderIndexOutOfBounds() (gas: 36258) -KarmaTest:testRemoveKarmaProviderOnlyOwner() (gas: 72129) -KarmaTest:testTotalSupply() (gas: 202375) -KarmaTest:testTransfersNotAllowed() (gas: 20675) -LeaveTest:test_LeaveShouldProperlyUpdateAccounting() (gas: 7111338) -LeaveTest:test_RevertWhenStakeManagerIsTrusted() (gas: 350329) -LeaveTest:test_TrustNewStakeManager() (gas: 7164286) -LockTest:test_LockFailsWithInvalidPeriod(uint256) (runs: 1009, μ: 406916, ~: 406944) +KarmaOwnershipTest:testInitialOwner() (gas: 12634) +KarmaOwnershipTest:testOwnershipTransfer() (gas: 87258) +KarmaTest:testAddKarmaDistributorOnlyOwner() (gas: 348451) +KarmaTest:testBalanceOf() (gas: 428613) +KarmaTest:testBalanceOfWithNoSystemTotalKarma() (gas: 44214) +KarmaTest:testMintOnlyOwner() (gas: 377173) +KarmaTest:testRemoveKarmaDistributorOnlyOwner() (gas: 75952) +KarmaTest:testRemoveUnknownKarmaDistributor() (gas: 36513) +KarmaTest:testTotalSupply() (gas: 336800) +KarmaTest:testTransfersNotAllowed() (gas: 20662) +LeaveTest:test_LeaveShouldProperlyUpdateAccounting() (gas: 7249868) +LeaveTest:test_RevertWhenStakeManagerIsTrusted() (gas: 350352) +LeaveTest:test_TrustNewStakeManager() (gas: 7302750) +LockTest:test_LockFailsWithInvalidPeriod(uint256) (runs: 1009, μ: 406917, ~: 406944) LockTest:test_LockFailsWithNoStake() (gas: 114574) LockTest:test_LockFailsWithZero() (gas: 367631) LockTest:test_LockWithoutPriorLock() (gas: 465087) LockTest:test_RevertWhenVaultToLockIsEmpty() (gas: 114532) -MaliciousUpgradeTest:test_UpgradeStackOverflowStakeManager() (gas: 1817080) +MaliciousUpgradeTest:test_UpgradeStackOverflowStakeManager() (gas: 1817092) MathTest:test_CalcAbsoluteMaxTotalMP() (gas: 4997) -MathTest:test_CalcAccrueMP() (gas: 7991) +MathTest:test_CalcAccrueMP() (gas: 8013) MathTest:test_CalcBonusMP() (gas: 18665) -MathTest:test_CalcInitialMP() (gas: 5353) +MathTest:test_CalcInitialMP() (gas: 5375) MathTest:test_CalcMaxAccruedMP() (gas: 4643) -MathTest:test_CalcMaxTotalMP() (gas: 19455) +MathTest:test_CalcMaxTotalMP() (gas: 19411) MultipleVaultsStakeTest:test_StakeMultipleVaults() (gas: 852588) NFTMetadataGeneratorSVGTest:testGenerateMetadata() (gas: 84995) NFTMetadataGeneratorSVGTest:testSetImageStrings() (gas: 58332) @@ -67,49 +67,49 @@ NFTMetadataGeneratorSVGTest:testSetImageStringsRevert() (gas: 35804) NFTMetadataGeneratorURLTest:testGenerateMetadata() (gas: 101558) NFTMetadataGeneratorURLTest:testSetBaseURL() (gas: 49555) NFTMetadataGeneratorURLTest:testSetBaseURLRevert() (gas: 35979) -RewardsStreamerMP_RewardsTest:testRewardsBalanceOf() (gas: 1234228) -RewardsStreamerMP_RewardsTest:testSetRewards() (gas: 162290) -RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadAmount() (gas: 39346) -RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadDuration() (gas: 39369) -RewardsStreamerMP_RewardsTest:testSetRewards_RevertsNotAuthorized() (gas: 39359) -RewardsStreamerMP_RewardsTest:testTotalRewardsSupply() (gas: 671296) +RewardsStreamerMP_RewardsTest:testRewardsBalanceOf() (gas: 1309798) +RewardsStreamerMP_RewardsTest:testSetRewards() (gas: 219337) +RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadAmount() (gas: 56162) +RewardsStreamerMP_RewardsTest:testSetRewards_RevertsBadDuration() (gas: 95940) +RewardsStreamerMP_RewardsTest:testSetRewards_RevertsNotAuthorized() (gas: 39295) +RewardsStreamerMP_RewardsTest:testTotalRewardsSupply() (gas: 746903) StakeTest:test_StakeMultipleAccounts() (gas: 591097) StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 599025) -StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 1024278) -StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 602688) +StakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 1024300) +StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 602710) StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 624688) StakeTest:test_StakeOneAccount() (gas: 333488) StakeTest:test_StakeOneAccountAndRewards() (gas: 341412) -StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 607147) -StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 595251) +StakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 607203) +StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 595243) StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 352045) StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 352607) StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 352696) -StakeVaultMigrationTest:testMigrateToVault() (gas: 945628) -StakeVaultMigrationTest:test_RevertWhenMigrationVaultNotEmpty() (gas: 646895) -StakeVaultMigrationTest:test_RevertWhenNotOwnerOfMigrationVault() (gas: 68102) +StakeVaultMigrationTest:testMigrateToVault() (gas: 945623) +StakeVaultMigrationTest:test_RevertWhenMigrationVaultNotEmpty() (gas: 646873) +StakeVaultMigrationTest:test_RevertWhenNotOwnerOfMigrationVault() (gas: 68099) StakingTokenTest:testStakeToken() (gas: 13140) -TrustedCodehashAccessTest:test_RevertWhenProxyCloneCodehashNotTrusted() (gas: 2007845) +TrustedCodehashAccessTest:test_RevertWhenProxyCloneCodehashNotTrusted() (gas: 2007813) UnstakeTest:test_StakeMultipleAccounts() (gas: 591141) UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 599069) -UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 1024255) +UnstakeTest:test_StakeMultipleAccountsMPIncreasesMaxMPDoesNotChange() (gas: 1024277) UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 602687) UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 624665) UnstakeTest:test_StakeOneAccount() (gas: 333488) UnstakeTest:test_StakeOneAccountAndRewards() (gas: 341456) -UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 607146) -UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 595253) +UnstakeTest:test_StakeOneAccountMPIncreasesMaxMPDoesNotChange() (gas: 607180) +UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 595287) UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 352067) UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 352629) -UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 352696) -UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 634656) +UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 352674) +UnstakeTest:test_UnstakeBonusMPAndAccuredMP() (gas: 634679) UnstakeTest:test_UnstakeMultipleAccounts() (gas: 829073) UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 943644) UnstakeTest:test_UnstakeOneAccount() (gas: 570246) -UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 592767) +UnstakeTest:test_UnstakeOneAccountAndAccruedMP() (gas: 592850) UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 488388) -UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 622966) -UpgradeTest:test_RevertWhenNotOwner() (gas: 3327767) -UpgradeTest:test_UpgradeStakeManager() (gas: 6999161) +UnstakeTest:test_UnstakeOneAccountWithLockUpAndAccruedMP() (gas: 622983) +UpgradeTest:test_RevertWhenNotOwner() (gas: 3399068) +UpgradeTest:test_UpgradeStakeManager() (gas: 7137669) VaultRegistrationTest:test_VaultRegistration() (gas: 62040) WithdrawTest:test_CannotWithdrawStakedFunds() (gas: 365961) \ No newline at end of file diff --git a/certora/specs/EmergencyMode.spec b/certora/specs/EmergencyMode.spec index bb5e6f2..cd2e8bf 100644 --- a/certora/specs/EmergencyMode.spec +++ b/certora/specs/EmergencyMode.spec @@ -26,6 +26,7 @@ definition isUUPSUpgradeableFunction(method f) returns bool = ( definition noCallDuringEmergency(method f) returns bool = ( f.selector == sig:streamer.updateGlobalState().selector + || f.selector == sig:streamer.setRewardsSupplier(address).selector || f.selector == sig:streamer.registerVault().selector || f.selector == sig:streamer.migrateToVault(address).selector || f.selector == sig:streamer.compound(address).selector diff --git a/docs/xp-token.md b/docs/xp-token.md index bed222c..e90ee8e 100644 --- a/docs/xp-token.md +++ b/docs/xp-token.md @@ -32,7 +32,6 @@ providers. XP tokens are not transferrable, but they can be used as voting power - `XPToken__MintAllowanceExceeded`: Raised when minting exceeds the allowed threshold. - `XPToken__TransfersNotAllowed`: Raised when a transfer, approval, or transferFrom is attempted. -- `RewardProvider__IndexOutOfBounds`: Raised when an invalid index is used for removing a reward provider. ## Supply and Balance Calculation diff --git a/script/DeployKarma.s.sol b/script/DeployKarma.s.sol new file mode 100644 index 0000000..123d0ee --- /dev/null +++ b/script/DeployKarma.s.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { BaseScript } from "./Base.s.sol"; +import { DeploymentConfig } from "./DeploymentConfig.s.sol"; + +import { Karma } from "../src/Karma.sol"; + +contract DeployKarmaScript is BaseScript { + function run() public returns (Karma) { + DeploymentConfig deploymentConfig = new DeploymentConfig(broadcaster); + (address deployer,) = deploymentConfig.activeNetworkConfig(); + + vm.startBroadcast(deployer); + address karma = address(new Karma()); + vm.stopBroadcast(); + + return Karma(karma); + } +} diff --git a/script/DeploymentConfig.s.sol b/script/DeploymentConfig.s.sol index b6765b0..c609fd5 100644 --- a/script/DeploymentConfig.s.sol +++ b/script/DeploymentConfig.s.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.26 <=0.9.0; import { Script } from "forge-std/Script.sol"; import { MockToken } from "../test/mocks/MockToken.sol"; +import { Karma } from "../src/Karma.sol"; contract DeploymentConfig is Script { error DeploymentConfig_InvalidDeployerAddress(); diff --git a/src/Karma.sol b/src/Karma.sol index be84c07..af5d304 100644 --- a/src/Karma.sol +++ b/src/Karma.sol @@ -3,46 +3,112 @@ pragma solidity ^0.8.26; import { Ownable, Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { IRewardProvider } from "./interfaces/IRewardProvider.sol"; +import { IRewardDistributor } from "./interfaces/IRewardDistributor.sol"; +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +/** + * @title Karma + * @notice This contract allows for setting rewards for reward distributors. + * @dev Implementation of the Karma token + */ contract Karma is ERC20, Ownable2Step { - error Karma__MintAllowanceExceeded(); + using EnumerableSet for EnumerableSet.AddressSet; + /// @notice The name of the token string public constant NAME = "Karma"; + /// @notice The symbol of the token string public constant SYMBOL = "KARMA"; + /// @notice The total allocation for all reward distributors + uint256 public totalDistributorAllocation; - IRewardProvider[] public rewardProviders; + /// @notice Set of reward distributors + EnumerableSet.AddressSet private rewardDistributors; + /// @notice Mapping of reward distributor to allocation + mapping(address distributor => uint256 allocation) public rewardDistributorAllocations; error Karma__TransfersNotAllowed(); - error RewardProvider__IndexOutOfBounds(); + error Karma__MintAllowanceExceeded(); + error Karma__DistributorAlreadyAdded(); + error Karma__UnknownDistributor(); + + event RewardDistributorAdded(address distributor); constructor() ERC20(NAME, SYMBOL) Ownable(msg.sender) { } - function addRewardProvider(IRewardProvider provider) external onlyOwner { - rewardProviders.push(provider); - } - - function removeRewardProvider(uint256 index) external onlyOwner { - if (index >= rewardProviders.length) { - revert RewardProvider__IndexOutOfBounds(); + /** + * @notice Adds a reward distributor to the set of reward distributors. + * @dev Only the owner can add a reward distributor. + * @dev Emits a `RewardDistributorAdded` event when a distributor is added. + * @param distributor The address of the reward distributor. + */ + function addRewardDistributor(address distributor) external onlyOwner { + if (rewardDistributors.contains(distributor)) { + revert Karma__DistributorAlreadyAdded(); } - rewardProviders[index] = rewardProviders[rewardProviders.length - 1]; - rewardProviders.pop(); + rewardDistributors.add(address(distributor)); + emit RewardDistributorAdded(distributor); } - function getRewardProviders() external view returns (IRewardProvider[] memory) { - return rewardProviders; + /** + * @notice Removes a reward distributor from the set of reward distributors. + * @dev Only the owner can remove a reward distributor. + * @param distributor The address of the reward distributor. + */ + function removeRewardDistributor(address distributor) external onlyOwner { + if (!rewardDistributors.contains(distributor)) { + revert Karma__UnknownDistributor(); + } + + rewardDistributors.remove(distributor); + } + + /** + * @notice Sets the reward for a reward distributor. + * @dev Only the owner can set the reward for a reward distributor. + * @dev The total allocation for all reward distributors is updated. + * @param rewardsDistributor The address of the reward distributor. + * @param amount The amount of rewards to set. + * @param duration The duration of the rewards. + */ + function setReward(address rewardsDistributor, uint256 amount, uint256 duration) external onlyOwner { + if (!rewardDistributors.contains(rewardsDistributor)) { + revert Karma__UnknownDistributor(); + } + + rewardDistributorAllocations[rewardsDistributor] = amount; + totalDistributorAllocation += amount; + IRewardDistributor(rewardsDistributor).setReward(amount, duration); + } + + /** + * @notice Returns the reward distributors. + * @return The reward distributors. + */ + function getRewardDistributors() external view returns (address[] memory) { + return rewardDistributors.values(); } function _totalSupply() public view returns (uint256) { return super.totalSupply() + _externalSupply(); } + /** + * @notice Returns the total supply of the token. + * @dev The total supply is the sum of the token supply and the external supply. + * @return The total supply of the token. + */ function totalSupply() public view override returns (uint256) { return _totalSupply(); } + /** + * @notice Mints tokens to an account. + * @dev Only the owner can mint tokens. + * @dev The amount minted must not exceed the mint allowance. + * @param account The account to mint tokens to. + * @param amount The amount of tokens to mint. + */ function mint(address account, uint256 amount) external onlyOwner { if (amount > _mintAllowance()) { revert Karma__MintAllowanceExceeded(); @@ -61,26 +127,48 @@ contract Karma is ERC20, Ownable2Step { return maxSupply - fullTotalSupply; } + /** + * @notice Returns the mint allowance. + * @dev The mint allowance is the difference between the external supply and the total supply. + * @return The mint allowance. + */ function mintAllowance() public view returns (uint256) { return _mintAllowance(); } + /** + * @notice Returns the external supply of the token. + * @dev The external supply is the sum of the rewards from all reward distributors. + * @return The external supply of the token. + */ function _externalSupply() internal view returns (uint256) { uint256 externalSupply; - for (uint256 i = 0; i < rewardProviders.length; i++) { - externalSupply += rewardProviders[i].totalRewardsSupply(); + for (uint256 i = 0; i < rewardDistributors.length(); i++) { + IRewardDistributor distributor = IRewardDistributor(rewardDistributors.at(i)); + uint256 supply = distributor.totalRewardsSupply(); + if (supply > rewardDistributorAllocations[address(distributor)]) { + supply = rewardDistributorAllocations[address(distributor)]; + } + + externalSupply += supply; } return externalSupply; } + /** + * @notice Returns the balance of an account. + * @dev The balance of an account is the sum of the balance of the account and the external rewards + * @param account The account to get the balance of. + * @return The balance of the account. + */ function balanceOf(address account) public view override returns (uint256) { uint256 externalBalance; - for (uint256 i = 0; i < rewardProviders.length; i++) { - IRewardProvider provider = rewardProviders[i]; - externalBalance += provider.rewardsBalanceOfAccount(account); + for (uint256 i = 0; i < rewardDistributors.length(); i++) { + address distributor = rewardDistributors.at(i); + externalBalance += IRewardDistributor(distributor).rewardsBalanceOfAccount(account); } return super.balanceOf(account) + externalBalance; diff --git a/src/RewardsStreamerMP.sol b/src/RewardsStreamerMP.sol index b85862a..e1b7887 100644 --- a/src/RewardsStreamerMP.sol +++ b/src/RewardsStreamerMP.sol @@ -8,7 +8,7 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { IStakeManager } from "./interfaces/IStakeManager.sol"; import { IStakeVault } from "./interfaces/IStakeVault.sol"; -import { IRewardProvider } from "./interfaces/IRewardProvider.sol"; +import { IRewardDistributor } from "./interfaces/IRewardDistributor.sol"; import { TrustedCodehashAccess } from "./TrustedCodehashAccess.sol"; import { StakeMath } from "./math/StakeMath.sol"; @@ -24,7 +24,7 @@ contract RewardsStreamerMP is IStakeManager, TrustedCodehashAccess, ReentrancyGuardUpgradeable, - IRewardProvider, + IRewardDistributor, StakeMath { /// @notice Token that is staked in the vaults (SNT). @@ -74,6 +74,9 @@ contract RewardsStreamerMP is /// @notice Total amount of staked multiplier points uint256 public totalMPStaked; + /// @notice The address that can set rewards + address public rewardsSupplier; + modifier onlyRegisteredVault() { if (vaultOwners[msg.sender] == address(0)) { revert StakingManager__VaultNotRegistered(); @@ -88,6 +91,13 @@ contract RewardsStreamerMP is _; } + modifier onlyRewardsSupplier() { + if (msg.sender != rewardsSupplier) { + revert StakingManager__Unauthorized(); + } + _; + } + /** * @notice Initializes the contract. * @dev Disables initializers to prevent reinitialization. @@ -111,6 +121,15 @@ contract RewardsStreamerMP is lastMPUpdatedTime = block.timestamp; } + /** + * @notice Allows the owner to set the rewards supplier. + * @dev The supplier is going to be the `Karma` token. + * @param _rewardsSupplier The address of the rewards supplier. + */ + function setRewardsSupplier(address _rewardsSupplier) external onlyOwner onlyNotEmergencyMode { + rewardsSupplier = _rewardsSupplier; + } + /** * @notice Authorizes contract upgrades via UUPS. * @dev This function is only callable by the owner. @@ -417,7 +436,11 @@ contract RewardsStreamerMP is * @param amount The amount of rewards to distribute. * @param duration The duration of the reward period. */ - function setReward(uint256 amount, uint256 duration) external onlyOwner { + function setReward(uint256 amount, uint256 duration) external onlyRewardsSupplier { + if (rewardEndTime > block.timestamp) { + revert StakingManager__RewardPeriodNotEnded(); + } + if (duration == 0) { revert StakingManager__DurationCannotBeZero(); } diff --git a/src/interfaces/IRewardProvider.sol b/src/interfaces/IRewardDistributor.sol similarity index 76% rename from src/interfaces/IRewardProvider.sol rename to src/interfaces/IRewardDistributor.sol index 8f4ca8f..73b915c 100644 --- a/src/interfaces/IRewardProvider.sol +++ b/src/interfaces/IRewardDistributor.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.26; /** - * @title IRewardProvider - * @notice Interface for Reward Provider - * @dev This interface is necessary to unify reward provider contracts. + * @title IRewardDistributor + * @notice Interface for Reward Distributor contract. + * @dev This interface is necessary to unify reward distributor contracts. * @dev Karma token contract makes use of this to aggregate rewards. */ -interface IRewardProvider { +interface IRewardDistributor { /** * @notice Returns the total supply of rewards. * @return Total supply of rewards. @@ -27,4 +27,5 @@ interface IRewardProvider { * @return Balance of rewards for the account. */ function rewardsBalanceOfAccount(address user) external view returns (uint256); + function setReward(uint256 amount, uint256 duration) external; } diff --git a/src/interfaces/IStakeManager.sol b/src/interfaces/IStakeManager.sol index cfde2f7..f2bba0f 100644 --- a/src/interfaces/IStakeManager.sol +++ b/src/interfaces/IStakeManager.sol @@ -35,6 +35,7 @@ interface IStakeManager is ITrustedCodehashAccess, IStakeConstants { error StakingManager__DurationCannotBeZero(); /// @notice Emitted when there are insufficient funds to stake. error StakingManager__InsufficientBalance(); + error StakingManager__RewardPeriodNotEnded(); /// @notice Emitted when a vault is registered. event VaultRegistered(address indexed vault, address indexed owner); diff --git a/test/Karma.t.sol b/test/Karma.t.sol index 8a2d9a4..9604da6 100644 --- a/test/Karma.t.sol +++ b/test/Karma.t.sol @@ -3,130 +3,144 @@ pragma solidity ^0.8.26; import { Test } from "forge-std/Test.sol"; import { Karma } from "../src/Karma.sol"; -import { KarmaProviderMock } from "./mocks/KarmaProviderMock.sol"; -import { IRewardProvider } from "../src/interfaces/IRewardProvider.sol"; +import { KarmaDistributorMock } from "./mocks/KarmaDistributorMock.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; contract KarmaTest is Test { - Karma xpToken; + Karma public karma; - address owner = makeAddr("owner"); - address alice = makeAddr("alice"); - address bob = makeAddr("bob"); + address public owner = makeAddr("owner"); + address public alice = makeAddr("alice"); + address public bob = makeAddr("bob"); - KarmaProviderMock provider1; - KarmaProviderMock provider2; + KarmaDistributorMock public distributor1; + KarmaDistributorMock public distributor2; function setUp() public virtual { vm.prank(owner); - xpToken = new Karma(); + karma = new Karma(); - provider1 = new KarmaProviderMock(); - provider2 = new KarmaProviderMock(); + distributor1 = new KarmaDistributorMock(); + distributor2 = new KarmaDistributorMock(); vm.prank(owner); - xpToken.addRewardProvider(provider1); + karma.addRewardDistributor(address(distributor1)); vm.prank(owner); - xpToken.addRewardProvider(provider2); + karma.addRewardDistributor(address(distributor2)); } - function testAddKarmaProviderOnlyOwner() public { - KarmaProviderMock provider3 = new KarmaProviderMock(); + function testAddKarmaDistributorOnlyOwner() public { + KarmaDistributorMock distributor3 = new KarmaDistributorMock(); vm.prank(alice); vm.expectPartialRevert(Ownable.OwnableUnauthorizedAccount.selector); - xpToken.addRewardProvider(provider3); + karma.addRewardDistributor(address(distributor3)); vm.prank(owner); - xpToken.addRewardProvider(provider3); + karma.addRewardDistributor(address(distributor3)); - IRewardProvider[] memory providers = xpToken.getRewardProviders(); - assertEq(providers.length, 3); - assertEq(address(providers[0]), address(provider1)); - assertEq(address(providers[1]), address(provider2)); - assertEq(address(providers[2]), address(provider3)); + address[] memory distributors = karma.getRewardDistributors(); + assertEq(distributors.length, 3); + assertEq(distributors[0], address(distributor1)); + assertEq(distributors[1], address(distributor2)); + assertEq(distributors[2], address(distributor3)); } - function testRemoveKarmaProviderOnlyOwner() public { + function testRemoveKarmaDistributorOnlyOwner() public { vm.prank(alice); vm.expectPartialRevert(Ownable.OwnableUnauthorizedAccount.selector); - xpToken.removeRewardProvider(0); + karma.removeRewardDistributor(address(distributor1)); vm.prank(owner); - xpToken.removeRewardProvider(0); + karma.removeRewardDistributor(address(distributor1)); - IRewardProvider[] memory providers = xpToken.getRewardProviders(); - assertEq(providers.length, 1); - assertEq(address(providers[0]), address(provider2)); + address[] memory distributors = karma.getRewardDistributors(); + assertEq(distributors.length, 1); + assertEq(distributors[0], address(distributor2)); } - function testRemoveKarmaProviderIndexOutOfBounds() public { + function testRemoveUnknownKarmaDistributor() public { vm.prank(owner); - vm.expectRevert(Karma.RewardProvider__IndexOutOfBounds.selector); - xpToken.removeRewardProvider(10); + vm.expectRevert(Karma.Karma__UnknownDistributor.selector); + karma.removeRewardDistributor(address(1)); } function testTotalSupply() public { - provider1.setTotalKarmaShares(1000 ether); - provider2.setTotalKarmaShares(2000 ether); + vm.startBroadcast(owner); + karma.setReward(address(distributor1), 1000 ether, 1000); + karma.setReward(address(distributor2), 2000 ether, 2000); + vm.stopBroadcast(); + + distributor1.setTotalKarmaShares(1000 ether); + distributor2.setTotalKarmaShares(2000 ether); vm.prank(owner); - xpToken.mint(owner, 500 ether); + karma.mint(owner, 500 ether); - uint256 totalSupply = xpToken.totalSupply(); + uint256 totalSupply = karma.totalSupply(); assertEq(totalSupply, 3500 ether); } function testBalanceOfWithNoSystemTotalKarma() public view { - uint256 aliceBalance = xpToken.balanceOf(alice); + uint256 aliceBalance = karma.balanceOf(alice); assertEq(aliceBalance, 0); - uint256 bobBalance = xpToken.balanceOf(bob); + uint256 bobBalance = karma.balanceOf(bob); assertEq(bobBalance, 0); } function testBalanceOf() public { - provider1.setTotalKarmaShares(1000 ether); - provider2.setTotalKarmaShares(2000 ether); + vm.startBroadcast(owner); + karma.setReward(address(distributor1), 1000 ether, 1000); + karma.setReward(address(distributor2), 2000 ether, 2000); + vm.stopBroadcast(); - provider1.setUserKarmaShare(alice, 1000e18); - provider2.setUserKarmaShare(alice, 2000e18); + distributor1.setTotalKarmaShares(1000 ether); + distributor2.setTotalKarmaShares(2000 ether); + + distributor1.setUserKarmaShare(alice, 1000e18); + distributor2.setUserKarmaShare(alice, 2000e18); vm.prank(owner); - xpToken.mint(alice, 500e18); + karma.mint(alice, 500e18); uint256 expectedBalance = 3500e18; - uint256 balance = xpToken.balanceOf(alice); + uint256 balance = karma.balanceOf(alice); assertEq(balance, expectedBalance); } function testMintOnlyOwner() public { - provider1.setTotalKarmaShares(1000 ether); - provider2.setTotalKarmaShares(2000 ether); - assertEq(xpToken.totalSupply(), 3000 ether); + vm.startBroadcast(owner); + karma.setReward(address(distributor1), 1000 ether, 1000); + karma.setReward(address(distributor2), 2000 ether, 2000); + vm.stopBroadcast(); + + distributor1.setTotalKarmaShares(1000 ether); + distributor2.setTotalKarmaShares(2000 ether); + assertEq(karma.totalSupply(), 3000 ether); vm.prank(alice); vm.expectPartialRevert(Ownable.OwnableUnauthorizedAccount.selector); - xpToken.mint(alice, 1000e18); + karma.mint(alice, 1000e18); vm.prank(owner); - xpToken.mint(alice, 1000e18); - assertEq(xpToken.totalSupply(), 4000e18); + karma.mint(alice, 1000e18); + assertEq(karma.totalSupply(), 4000e18); } function testTransfersNotAllowed() public { vm.expectRevert(Karma.Karma__TransfersNotAllowed.selector); - xpToken.transfer(alice, 100e18); + karma.transfer(alice, 100e18); vm.expectRevert(Karma.Karma__TransfersNotAllowed.selector); - xpToken.approve(alice, 100e18); + karma.approve(alice, 100e18); vm.expectRevert(Karma.Karma__TransfersNotAllowed.selector); - xpToken.transferFrom(alice, bob, 100e18); + karma.transferFrom(alice, bob, 100e18); - uint256 allowance = xpToken.allowance(alice, bob); + uint256 allowance = karma.allowance(alice, bob); assertEq(allowance, 0); } } @@ -163,59 +177,75 @@ contract KarmaMintAllowanceTest is KarmaTest { } function testMintAllowance_Available() public { + vm.startBroadcast(owner); + karma.setReward(address(distributor1), 1000 ether, 1000); + karma.setReward(address(distributor2), 2000 ether, 2000); + vm.stopBroadcast(); // 3000 external => maxSupply = 9000 - provider1.setTotalKarmaShares(1000 ether); - provider2.setTotalKarmaShares(2000 ether); + distributor1.setTotalKarmaShares(1000 ether); + distributor2.setTotalKarmaShares(2000 ether); vm.prank(owner); - xpToken.mint(owner, 500 ether); + karma.mint(owner, 500 ether); // totalSupply = 3500 - uint256 mintAllowance = xpToken.mintAllowance(); + uint256 mintAllowance = karma.mintAllowance(); assertEq(mintAllowance, 5500 ether); } function testMintAllowance_NotAvailable() public { + vm.startBroadcast(owner); + karma.setReward(address(distributor1), 1000 ether, 1000); + karma.setReward(address(distributor2), 2000 ether, 2000); + vm.stopBroadcast(); // 3000 external => maxSupply = 9000 - provider1.setTotalKarmaShares(1000 ether); - provider2.setTotalKarmaShares(2000 ether); + distributor1.setTotalKarmaShares(1000 ether); + distributor2.setTotalKarmaShares(2000 ether); vm.prank(owner); - xpToken.mint(owner, 6000 ether); + karma.mint(owner, 6000 ether); // totalSupply = 9_000 - uint256 mintAllowance = xpToken.mintAllowance(); + uint256 mintAllowance = karma.mintAllowance(); assertEq(mintAllowance, 0); } function testMint_RevertWithAllowanceExceeded() public { + vm.startBroadcast(owner); + karma.setReward(address(distributor1), 1000 ether, 1000); + karma.setReward(address(distributor2), 2000 ether, 2000); + vm.stopBroadcast(); // 3000 external => maxSupply = 9000 - provider1.setTotalKarmaShares(1000 ether); - provider2.setTotalKarmaShares(2000 ether); + distributor1.setTotalKarmaShares(1000 ether); + distributor2.setTotalKarmaShares(2000 ether); vm.prank(owner); - xpToken.mint(owner, 500 ether); + karma.mint(owner, 500 ether); // totalSupply = 3500 // allowed to mint 5500 vm.prank(owner); vm.expectRevert(Karma.Karma__MintAllowanceExceeded.selector); - xpToken.mint(owner, 6000 ether); + karma.mint(owner, 6000 ether); } function testMint_Ok() public { + vm.startBroadcast(owner); + karma.setReward(address(distributor1), 1000 ether, 1000); + karma.setReward(address(distributor2), 2000 ether, 2000); + vm.stopBroadcast(); // 3000 external => maxSupply = 9000 - provider1.setTotalKarmaShares(1000 ether); - provider2.setTotalKarmaShares(2000 ether); + distributor1.setTotalKarmaShares(1000 ether); + distributor2.setTotalKarmaShares(2000 ether); vm.prank(owner); - xpToken.mint(owner, 500 ether); - assertEq(xpToken.totalSupply(), 3500 ether); + karma.mint(owner, 500 ether); + assertEq(karma.totalSupply(), 3500 ether); // totalSupply = 3500 // allowed to mint 5500 vm.prank(owner); - xpToken.mint(owner, 5500 ether); - assertEq(xpToken.totalSupply(), 9000 ether); + karma.mint(owner, 5500 ether); + assertEq(karma.totalSupply(), 9000 ether); } } diff --git a/test/RewardsStreamerMP.t.sol b/test/RewardsStreamerMP.t.sol index 5e47830..aefa336 100644 --- a/test/RewardsStreamerMP.t.sol +++ b/test/RewardsStreamerMP.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.26; import { Test, console } from "forge-std/Test.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { DeployKarmaScript } from "../script/DeployKarma.s.sol"; import { DeployRewardsStreamerMPScript } from "../script/DeployRewardsStreamerMP.s.sol"; import { UpgradeRewardsStreamerMPScript } from "../script/UpgradeRewardsStreamerMP.s.sol"; import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; @@ -16,6 +17,7 @@ import { RewardsStreamerMP } from "../src/RewardsStreamerMP.sol"; import { StakeMath } from "../src/math/StakeMath.sol"; import { StakeVault } from "../src/StakeVault.sol"; import { VaultFactory } from "../src/VaultFactory.sol"; +import { Karma } from "../src/Karma.sol"; import { MockToken } from "./mocks/MockToken.sol"; import { StackOverflowStakeManager } from "./mocks/StackOverflowStakeManager.sol"; @@ -23,6 +25,7 @@ contract RewardsStreamerMPTest is StakeMath, Test { MockToken internal stakingToken; RewardsStreamerMP public streamer; VaultFactory public vaultFactory; + Karma public karma; address internal admin; address internal alice = makeAddr("alice"); @@ -34,6 +37,7 @@ contract RewardsStreamerMPTest is StakeMath, Test { function setUp() public virtual { DeployRewardsStreamerMPScript deployment = new DeployRewardsStreamerMPScript(); + DeployKarmaScript karmaDeployment = new DeployKarmaScript(); (RewardsStreamerMP stakeManager, VaultFactory _vaultFactory, DeploymentConfig deploymentConfig) = deployment.run(); @@ -43,6 +47,13 @@ contract RewardsStreamerMPTest is StakeMath, Test { stakingToken = MockToken(_stakingToken); vaultFactory = _vaultFactory; admin = _deployer; + karma = karmaDeployment.run(); + + // set up reward distribution + vm.startPrank(admin); + karma.addRewardDistributor(address(streamer)); + streamer.setRewardsSupplier(address(karma)); + vm.stopPrank(); address[4] memory accounts = [alice, bob, charlie, dave]; for (uint256 i = 0; i < accounts.length; i++) { @@ -160,6 +171,11 @@ contract RewardsStreamerMPTest is StakeMath, Test { UpgradeRewardsStreamerMPScript upgrade = new UpgradeRewardsStreamerMPScript(); upgrade.run(admin, IStakeManagerProxy(address(streamer))); } + + function _setRewards(uint256 amount, uint256 period) internal { + vm.prank(admin); + karma.setReward(address(streamer), amount, period); + } } contract MathTest is RewardsStreamerMPTest { @@ -2198,8 +2214,7 @@ contract RewardsStreamerMP_RewardsTest is RewardsStreamerMPTest { // since we are testing that it is used for rewardStartTime currentTime += 1 days; vm.warp(currentTime); - vm.prank(admin); - streamer.setReward(1000, 10); + _setRewards(1000, 10); assertEq(streamer.rewardStartTime(), currentTime); assertEq(streamer.rewardEndTime(), currentTime + 10); @@ -2208,20 +2223,20 @@ contract RewardsStreamerMP_RewardsTest is RewardsStreamerMPTest { function testSetRewards_RevertsNotAuthorized() public { vm.prank(alice); - vm.expectPartialRevert(Ownable.OwnableUnauthorizedAccount.selector); + vm.expectPartialRevert(IStakeManager.StakingManager__Unauthorized.selector); streamer.setReward(1000, 10); } function testSetRewards_RevertsBadDuration() public { vm.prank(admin); vm.expectRevert(IStakeManager.StakingManager__DurationCannotBeZero.selector); - streamer.setReward(1000, 0); + karma.setReward(address(streamer), 1000, 0); } function testSetRewards_RevertsBadAmount() public { vm.prank(admin); vm.expectRevert(IStakeManager.StakingManager__AmountCannotBeZero.selector); - streamer.setReward(0, 10); + karma.setReward(address(streamer), 0, 10); } function testTotalRewardsSupply() public { @@ -2230,8 +2245,7 @@ contract RewardsStreamerMP_RewardsTest is RewardsStreamerMPTest { uint256 initialTime = vm.getBlockTimestamp(); - vm.prank(admin); - streamer.setReward(1000e18, 10 days); + _setRewards(1000e18, 10 days); assertEq(streamer.totalRewardsSupply(), 0); for (uint256 i = 0; i <= 10; i++) { @@ -2252,8 +2266,7 @@ contract RewardsStreamerMP_RewardsTest is RewardsStreamerMPTest { assertEq(streamer.totalRewardsAccrued(), 0); // set other 2000 rewards for other 10 days - vm.prank(admin); - streamer.setReward(2000e18, 10 days); + _setRewards(2000e18, 10 days); // accrued is 1000 from the previous reward and still 0 for the new one assertEq(streamer.totalRewardsSupply(), 1000e18, "totalRewardsSupply should be 1000"); @@ -2272,9 +2285,7 @@ contract RewardsStreamerMP_RewardsTest is RewardsStreamerMPTest { uint256 initialTime = vm.getBlockTimestamp(); _stake(alice, 100e18, 0); - - vm.prank(admin); - streamer.setReward(1000e18, year); + _setRewards(1000e18, year); assertEq(streamer.totalStaked(), 100e18); assertEq(streamer.totalMPStaked(), 100e18); @@ -2357,8 +2368,7 @@ contract RewardsStreamerMP_RewardsTest is RewardsStreamerMPTest { assertEq(streamer.vaultShares(vaults[bob]), 200e18); assertEq(streamer.rewardsBalanceOf(vaults[bob]), 250e18); - vm.prank(admin); - streamer.setReward(600e18, year); + _setRewards(600e18, year); vm.warp(initialTime + year * 3); @@ -2745,7 +2755,7 @@ contract FuzzTests is RewardsStreamerMPTest { stakeAmount = bound(stakeAmount, 1e18, 20_000_000e18); lockUpPeriod = lockUpPeriod == 0 ? 0 : bound(lockUpPeriod, MIN_LOCKUP_PERIOD, MAX_LOCKUP_PERIOD); vm.assume(rewardPeriod > 0 && rewardPeriod <= 12 weeks); // assuming max 3 months - vm.assume(rewardAmount > 1e18 && rewardAmount <= 100_000e18); // assuming max 1_000_000 Karma + vm.assume(rewardAmount > 1e18 && rewardAmount <= 1_000_000e18); // assuming max 1_000_000 Karma vm.assume(accountRewardPeriod <= rewardPeriod); // Ensure accountRewardPeriod doesn't exceed rewardPeriod uint256 initialTime = vm.getBlockTimestamp(); @@ -2758,8 +2768,7 @@ contract FuzzTests is RewardsStreamerMPTest { _stake(alice, stakeAmount, lockUpPeriod); - vm.prank(admin); - streamer.setReward(rewardAmount, rewardPeriod); + _setRewards(rewardAmount, rewardPeriod); vm.warp(initialTime + accountRewardPeriod); diff --git a/test/mocks/KarmaProviderMock.sol b/test/mocks/KarmaDistributorMock.sol similarity index 79% rename from test/mocks/KarmaProviderMock.sol rename to test/mocks/KarmaDistributorMock.sol index 687e417..a3b48a4 100644 --- a/test/mocks/KarmaProviderMock.sol +++ b/test/mocks/KarmaDistributorMock.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { IRewardProvider } from "../../src/interfaces/IRewardProvider.sol"; +import { IRewardDistributor } from "../../src/interfaces/IRewardDistributor.sol"; -contract KarmaProviderMock is IRewardProvider { +contract KarmaDistributorMock is IRewardDistributor { // solhint-disable-next-line mapping(address => uint256) public userKarmaShare; @@ -21,6 +21,8 @@ contract KarmaProviderMock is IRewardProvider { revert("Not implemented"); } + function setReward(uint256, uint256) external pure override { } + function rewardsBalanceOfAccount(address account) external view override returns (uint256) { return userKarmaShare[account]; }