diff --git a/DEPLOYMENT_ADDRESSES.md b/DEPLOYMENT_ADDRESSES.md index 16537be..55ecf17 100644 --- a/DEPLOYMENT_ADDRESSES.md +++ b/DEPLOYMENT_ADDRESSES.md @@ -31,7 +31,7 @@ | MerkleVesterBackers | [`0x2FF1Cdf8Fe00Ae6952BAA32e37D84D31A31E2EC2`](https://optimistic.etherscan.io/address/0x2FF1Cdf8Fe00Ae6952BAA32e37D84D31A31E2EC2) | - | | MerkleVesterReown | [`0x648bddEE207da25e19918460c1Dc9F462F657a19`](https://optimistic.etherscan.io/address/0x648bddEE207da25e19918460c1Dc9F462F657a19) | - | | MerkleVesterWalletConnect | [`0x85d0964D328563D502867FF6899C6F73D2E59FD1`](https://optimistic.etherscan.io/address/0x85d0964D328563D502867FF6899C6F73D2E59FD1) | - | -| StakingRewardsCalculator | [`0xC06d02F26515A56576426deCddac8d7b9Ca326D1`](https://optimistic.etherscan.io/address/0xC06d02F26515A56576426deCddac8d7b9Ca326D1) | - | +| StakingRewardsCalculator | [`0x5581e8C58bD9Ad4B3A88a5250deBa164938dBcC3`](https://optimistic.etherscan.io/address/0x5581e8C58bD9Ad4B3A88a5250deBa164938dBcC3) | - | ## Base (Chain ID: 8453) diff --git a/docs/rewardCalculator.md b/docs/rewardCalculator.md index 3fbe664..7314395 100644 --- a/docs/rewardCalculator.md +++ b/docs/rewardCalculator.md @@ -36,15 +36,24 @@ Key properties: ### 3. Weekly Rewards Calculation ```solidity -weeklyRewards = (totalStakeWeight * targetApy) / (52 * 1e18 * 100) +weeklyRewards = (totalStakeWeight * 4 * targetApy) / (52 * 1e18 * 100) where: totalStakeWeight = current total stake weight with lock periods -targetApy = APY calculated from linear model +4 = multiplier to convert stake weight to equivalent annual staked tokens +targetApy = APY calculated from linear model (e.g., 12% = 12e18) 52 = weeks in year 100 = percentage to decimal conversion +1e18 = precision scaling factor ``` +Key properties: + +- Stake weight is multiplied by 4 to convert to equivalent annual staked tokens +- This ensures full APY distribution (e.g., 12% APY means exactly 12% annual rewards) +- Weekly distribution is 1/52 of the annual rewards +- Precision maintained through calculations using 1e18 scaling + ## Verified Mathematical Invariants ### APY Guarantees diff --git a/evm/deployments/10 b/evm/deployments/10 index 4ee07bf..1c20ea9 100644 Binary files a/evm/deployments/10 and b/evm/deployments/10 differ diff --git a/evm/deployments/10.json b/evm/deployments/10.json index 6b21ef7..3d3dfc5 100644 --- a/evm/deployments/10.json +++ b/evm/deployments/10.json @@ -67,7 +67,7 @@ } }, "StakingRewardsCalculator": { - "address": "0xC06d02F26515A56576426deCddac8d7b9Ca326D1" + "address": "0x5581e8C58bD9Ad4B3A88a5250deBa164938dBcC3" }, "WalletConnectConfig": { "address": "0xd2f149fAA66DC4448176123f850C14Ff14f978B3", diff --git a/evm/remappings.txt b/evm/remappings.txt index 9945040..4ac136a 100644 --- a/evm/remappings.txt +++ b/evm/remappings.txt @@ -4,4 +4,4 @@ openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/ safe-smart-account=lib/safe-smart-account/ src=./src/ script=./script/ -test=./test/ \ No newline at end of file +test=./test/ diff --git a/evm/src/StakingRewardsCalculator.sol b/evm/src/StakingRewardsCalculator.sol index 3f9f282..5fc340d 100644 --- a/evm/src/StakingRewardsCalculator.sol +++ b/evm/src/StakingRewardsCalculator.sol @@ -194,7 +194,8 @@ contract StakingRewardsCalculator { } /// @dev Calculate weekly rewards based on total stake weight and APY - /// @dev Formula: weeklyRewards = (totalStakeWeight * targetApy) / (52 * 1e18 * 100) + /// @dev Formula: weeklyRewards = (totalStakeWeight * 4 * targetApy) / (52 * 1e18 * 100) + /// @dev The multiplication by 4 converts stake weight to equivalent annual staked tokens /// @dev The division by 100 converts percentage to decimal /// @param actualTotalStakeWeight Current total stake weight considering lock periods (in wei) /// @param targetApy Target APY calculated from stake weight curve (in wei, e.g., 12% = 12e18) @@ -207,7 +208,8 @@ contract StakingRewardsCalculator { pure returns (uint256 weeklyRewards) { - uint256 annualRewardsWithPrecision = (actualTotalStakeWeight * uint256(targetApy)); + // Multiply by 4 to convert stake weight to equivalent annual staked tokens + uint256 annualRewardsWithPrecision = (actualTotalStakeWeight * 4 * uint256(targetApy)); // Step 4: Convert annual rewards to weekly rewards weeklyRewards = annualRewardsWithPrecision / (PRECISION * 100 * WEEKS_IN_YEAR); diff --git a/evm/test/integration/concrete/staking-rewards-calculator/inject-rewards-for-week/injectRewardsForWeek.sol b/evm/test/integration/concrete/staking-rewards-calculator/inject-rewards-for-week/injectRewardsForWeek.sol index daee27a..ca96d0a 100644 --- a/evm/test/integration/concrete/staking-rewards-calculator/inject-rewards-for-week/injectRewardsForWeek.sol +++ b/evm/test/integration/concrete/staking-rewards-calculator/inject-rewards-for-week/injectRewardsForWeek.sol @@ -9,8 +9,8 @@ contract InjectRewardsForWeek_StakingRewardsCalculator_Integration_Test is StakingRewardsCalculator_Integration_Shared_Test { uint256 constant STAKE_AMOUNT = 5_000_000 ether; - uint256 constant REWARDS_AMOUNT = 10_000 ether; - uint256 constant EXPECTED_WEEKLY_REWARD = 2816.059 ether; + uint256 constant REWARDS_AMOUNT = 50_000 ether; + uint256 constant EXPECTED_WEEKLY_REWARD = 11_264.236 ether; uint256 defaultTimestamp; @@ -210,7 +210,7 @@ contract InjectRewardsForWeek_StakingRewardsCalculator_Integration_Test is uint256 rewards = _calculateAndInjectRewards(address(walletConnectConfig), timestamp, false, bytes("")); // Then: Rewards should be around double (more flexibility for the test) - assertApproxEqAbs(rewards, EXPECTED_WEEKLY_REWARD * 2, 1e20, "Rewards should be double for two equal stakers"); + assertApproxEqAbs(rewards, EXPECTED_WEEKLY_REWARD * 2, 3e20, "Rewards should be double for two equal stakers"); // And: Should transfer tokens from caller to distributor assertEq(l2wct.balanceOf(address(stakingRewardDistributor)), rewards, "Distributor should have rewards"); diff --git a/evm/test/invariant/StakingRewardsCalculator.t.sol b/evm/test/invariant/StakingRewardsCalculator.t.sol index 0ba00f0..a13d068 100644 --- a/evm/test/invariant/StakingRewardsCalculator.t.sol +++ b/evm/test/invariant/StakingRewardsCalculator.t.sol @@ -92,11 +92,15 @@ contract StakingRewardsCalculator_Invariant_Test is Invariant_Test { } function invariant_WeeklyRewards_Bounds() public view { - // Weekly rewards should never exceed annual rewards divided by WEEKS_IN_YEAR + // Weekly rewards should never exceed annual rewards + // The calculateWeeklyRewards function uses the formula: + // weeklyRewards = (stakeWeight * 4 * apy) / (PRECISION * 100 * WEEKS_IN_YEAR) uint256 maxStakeWeight = store.maxRecordedStakeWeight(); if (maxStakeWeight > 0) { uint256 maxWeeklyRewards = calculator.calculateWeeklyRewards(maxStakeWeight, INTERCEPT); - uint256 maxAnnualRewards = (maxStakeWeight * uint256(INTERCEPT)) / (PRECISION * 100); + + // Calculate the annual rewards using the same formula but without dividing by WEEKS_IN_YEAR + uint256 maxAnnualRewards = (maxStakeWeight * 4 * uint256(INTERCEPT)) / (PRECISION * 100); assertLe( maxWeeklyRewards * WEEKS_IN_YEAR, @@ -144,6 +148,8 @@ contract StakingRewardsCalculator_Invariant_Test is Invariant_Test { uint256 largerRewards = calculator.calculateWeeklyRewards(largerStake, largerApy); // Calculate the ratio of stakes and APYs + // Note: The multiplication by 4 in calculateWeeklyRewards applies equally to both rewards, + // so it doesn't affect the ratio calculation uint256 stakeRatio = (largerStake * PRECISION) / smallerStake; uint256 apyRatio = (uint256(largerApy) * PRECISION) / uint256(smallerApy); diff --git a/evm/test/invariant/handlers/StakingRewardDistributorHandler.sol b/evm/test/invariant/handlers/StakingRewardDistributorHandler.sol index 47c4df2..3a39102 100644 --- a/evm/test/invariant/handlers/StakingRewardDistributorHandler.sol +++ b/evm/test/invariant/handlers/StakingRewardDistributorHandler.sol @@ -275,9 +275,10 @@ contract StakingRewardDistributorHandler is BaseHandler { StakeWeight.LockedBalance memory newLock = stakeWeight.locks(allocation.beneficiary); - store.addAddressWithLock(allocation.beneficiary); store.updateLockedAmount(allocation.beneficiary, SafeCast.toUint256(newLock.amount)); store.updateUnlockTime(allocation.beneficiary, newLock.end); + store.addAddressWithLock(allocation.beneficiary); + store.setUserLockStartWeek(allocation.beneficiary, _timestampToFloorWeek(block.timestamp)); } function createPermanentLock( @@ -337,9 +338,10 @@ contract StakingRewardDistributorHandler is BaseHandler { stakeWeight.createPermanentLock(amount, duration); vm.stopPrank(); - store.addAddressWithLock(user); store.updateLockedAmount(user, amount); store.updateUnlockTime(user, 0); // Permanent locks have no end time + store.addAddressWithLock(user); + store.setUserLockStartWeek(user, _timestampToFloorWeek(block.timestamp)); } function convertToPermanent( diff --git a/evm/test/unit/fuzz/staking-rewards-calculator/calculate-weekly-rewards/calculateWeeklyRewards.t.sol b/evm/test/unit/fuzz/staking-rewards-calculator/calculate-weekly-rewards/calculateWeeklyRewards.t.sol index 5b944a7..28938b5 100644 --- a/evm/test/unit/fuzz/staking-rewards-calculator/calculate-weekly-rewards/calculateWeeklyRewards.t.sol +++ b/evm/test/unit/fuzz/staking-rewards-calculator/calculate-weekly-rewards/calculateWeeklyRewards.t.sol @@ -41,8 +41,8 @@ contract CalculateWeeklyRewards_StakingRewardsCalculator_Unit_Fuzz_Test is Test uint256 rewards = calculator.calculateWeeklyRewards(stakeWeight, int256(rawApy)); - // Calculate expected rewards - uint256 expectedAnnualRewards = (stakeWeight * rawApy); + // Calculate expected rewards with 4x multiplier + uint256 expectedAnnualRewards = (stakeWeight * 4 * rawApy); uint256 expectedWeeklyRewards = (expectedAnnualRewards / WEEKS_IN_YEAR) / (PRECISION * 100); assertEq(rewards, expectedWeeklyRewards, "Weekly rewards calculation mismatch"); @@ -56,11 +56,11 @@ contract CalculateWeeklyRewards_StakingRewardsCalculator_Unit_Fuzz_Test is Test uint256 rewards = calculator.calculateWeeklyRewards(stakeWeight, INTERCEPT); // Verify no overflow occurred and rewards make sense - assertTrue(rewards > 0, "Rewards should positive for maximum APY"); - assertTrue(rewards < stakeWeight, "Weekly rewards should be less than total stake"); + assertTrue(rewards > 0, "Rewards should be positive for maximum APY"); + assertTrue(rewards < stakeWeight * 4, "Weekly rewards should be less than 4x total stake"); - // Calculate expected maximum rewards - uint256 expectedAnnualRewards = (stakeWeight * uint256(INTERCEPT)); + // Calculate expected maximum rewards with 4x multiplier + uint256 expectedAnnualRewards = (stakeWeight * 4 * uint256(INTERCEPT)); uint256 expectedWeeklyRewards = (expectedAnnualRewards / WEEKS_IN_YEAR) / (PRECISION * 100); assertEq(rewards, expectedWeeklyRewards, "Maximum rewards calculation mismatch"); }