Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DEPLOYMENT_ADDRESSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
| Pauser | [`0x9163de7F22A9f3ad261B3dBfbB9A42886816adE7`](https://optimistic.etherscan.io/address/0x9163de7F22A9f3ad261B3dBfbB9A42886816adE7) |
| StakeWeight | [`0x521B4C065Bbdbe3E20B3727340730936912DfA46`](https://optimistic.etherscan.io/address/0x521B4C065Bbdbe3E20B3727340730936912DfA46) |
| StakingRewardDistributor | [`0xF368F535e329c6d08DFf0d4b2dA961C4e7F3fCAF`](https://optimistic.etherscan.io/address/0xF368F535e329c6d08DFf0d4b2dA961C4e7F3fCAF) |
| StakingRewardCalculator | [`0xc06d02f26515a56576426decddac8d7b9ca326d1`](https://optimistic.etherscan.io/address/0xc06d02f26515a56576426decddac8d7b9ca326d1) |
| StakingRewardCalculator | [`0x5581e8C58bD9Ad4B3A88a5250deBa164938dBcC3`](https://optimistic.etherscan.io/address/0x5581e8C58bD9Ad4B3A88a5250deBa164938dBcC3) |
| Airdrop | [`0x4ee97a759AACa2EdF9c1445223b6Cd17c2eD3fb4`](https://optimistic.etherscan.io/address/0x4ee97a759AACa2EdF9c1445223b6Cd17c2eD3fb4) |
| MerkleVester Reown | [`0x648bddEE207da25e19918460c1Dc9F462F657a19`](https://optimistic.etherscan.io/address/0x648bddEE207da25e19918460c1Dc9F462F657a19) |
| LockedTokenStaker Reown | [`0x5f630a47DE14e346fC28deB8fE379833A6F6B9B2`](https://optimistic.etherscan.io/address/0x5f630a47DE14e346fC28deB8fE379833A6F6B9B2) |
Expand Down
13 changes: 11 additions & 2 deletions docs/rewardCalculator.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file modified evm/deployments/10
Binary file not shown.
2 changes: 1 addition & 1 deletion evm/deployments/10.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"admin": "0x28672bf553c6AB214985868f68A3a491E227aCcB",
"implementation": "0xF6d23184E44f282883c0d145C973442fc7A33AB8"
},
"StakingRewardsCalculator": { "address": "0xC06d02F26515A56576426deCddac8d7b9Ca326D1" },
"StakingRewardsCalculator": { "address": "0x5581e8C58bD9Ad4B3A88a5250deBa164938dBcC3" },
"WalletConnectConfig": {
"address": "0xd2f149fAA66DC4448176123f850C14Ff14f978B3",
"admin": "0x5eC1CFa1f0F5191BCe755B87bde67a9Fe558Eb57",
Expand Down
5 changes: 1 addition & 4 deletions evm/remappings.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
@openzeppelin/=node_modules/@openzeppelin/
forge-std/=lib/forge-std/src/
openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/
src=./src/
script=./script/
test=./test/
openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/
6 changes: 4 additions & 2 deletions evm/src/StakingRewardsCalculator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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");
Expand Down
10 changes: 8 additions & 2 deletions evm/test/invariant/StakingRewardsCalculator.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
}
Expand Down
Loading